需求描述
最近多个第三方开发厂商为了快速集成软件及系统,不想基于 SDK 做费时费力的二次开发,而是希望直接从 Web 网页上启动客户端软件。类似的需求可以归纳为以下几类:
- 仅仅是从 Web 网页上将 C++ 客户端软件启动起来,用户手动操作。
- 从 Web 网页上将 C++ 客户端软件启动起来,并传递服务器地址、用户名和密码,让软件自动发起登录。
- 从 Web 网页上将 C++ 客户端软件启动起来,传递信息执行指定操作,如加入指定会议。
上述需求可归结为:将 C++ 客户端软件启动起来,并给 C++ 客户端软件传递一些命令行参数,C++ 客户端软件解析出参数,执行指定的操作。
以浏览器打开腾讯会议的会议链接为例,点击链接后系统会弹出是否要打开本地安装的腾讯会议程序的提示框,确认后启动程序并自动加入会议。
选择 URI Scheme 实现
在 C++ 程序中启动另一个 C++ 软件比较简单,只需获取目标软件的安装路径即可直接启动。但在 B/S 架构下,Web 网页出于安全原因不能直接读写注册表,无法像 C++ 程序那样直接启动二进制文件。使用 URI Scheme 技术与规范就能实现这样的需求。
何为 URI Scheme?
URI(Uniform Resource Identifier)是统一资源标志符。URI Scheme 是一种技术规范,表示 Web 上每一种可用的资源。从 Web 网页中启动本地应用程序的 URI Scheme 规范中,需要将本地应用程序的信息通过写注册表的方式注册到系统中,然后在网页中使用'SchemeName://'就可以启动本地程序了。
具体的做法是,在注册表的 HKEY_CLASSES_ROOT 下创建一个自定义的 SchemeName 注册表节点,然后再在该节点下创建多个节点,并在给相关节点设置注册表键值。
以 QQGame 为例,添加注册表信息的步骤如下:
- 在 HKEY_CLASSES_ROOT 下创建 QQGameProtocol 节点。QQGameProtocol 就是对应的 Scheme 方案名称,也是 Web 页面上启动对应程序的 URL 的前缀名称。然后给该节点添加一个 URL Protocol 名称的键值,将其 Value 设置为本地应用程序的完整路径。
- 在 QQGameProtocol 根节点下创建 DefaultIcon 节点。给 DefaultIcon 节点设置默认的字符串键值,其 Value 的格式为'应用程序全路径,图标索引'的形式,用来指定该 URI 方案使用的图标。
- 在 QQGameProtocol 下创建 shell 节点,然后在 shell 节点下创建 open 节点,然后在 open 节点创建 command 节点。command 节点需要设置键值,其键值用来指定启动目标应用程序时是否给目标程序传递命令行参数。
一般只需要设置传递一个参数即可,比如当前 Scheme 下的 "C:\Users\Public\Documents\Tencent\QQGameMicro\QQGwp.exe" "%1"。如果要传递多个参数,可以自定义一个组合格式,命令行只用一个参数即可。
当在 Web 页面上点击'SchemeName://'链接时,就会到系统注册表的 HKEY_CLASSES_ROOT 节点下查找 SchemeName 节点项,找到后取出目标应用程序的全路径,并查找传递的命令行参数个数,这样就能把本地的目标应用程序启动起来了。
如果要给目标程序传递参数,则使用'SchemeName://参数'的形式。经测试发现,如果在 command 节点中设置了%1 传递参数的标识,则 Web 网页中设置的 URL 必须要带参数。如果使用不带参数的 URL,则无法启动目标程序。
那如何既要支持不传参数启动,也要支持传参数启动呢?其实不用这么麻烦,使用一个带参数的 SchemeName 节点就够了,对于直接启动目标程序不带启动参数的,也可以携带一个标识参数,在程序中约定不传参数的标识符,比如 noparam,当程序中解析出 noparam,则表示是不带参数启动的,直接启动程序即可。
将自定义的 URL Scheme 信息写入注册表的 C++ 源码实现
下面给出将自定义的 URL Scheme 信息写入注册表的 C++ 源码实现:
BOOL WriteURISchemaReg() {
// exe 程序的完整路径
CString strExePath = m_strInstallPath + _T("xyzlink.exe");
// URI Scheme 名称
CString strProtocolName = _T("XyzlinkProtocol");
HKEY hRootKey = ;
DWORD dwKeyValue = ;
DWORD dwDisposition = ;
UCHAR szBuf[MAX_PATH] = { };
lRet = ::(HKEY_CLASSES_ROOT, strProtocolName, , , , KEY_ALL_ACCESS, , &hRootKey, &dwDisposition);
(lRet != ERROR_SUCCESS) {
FALSE;
}
lRet = ::(hRootKey, , , REG_SZ, (LPBYTE)(LPCTSTR)strProtocolName, strProtocolName.() * (TCHAR));
(lRet != ERROR_SUCCESS) {
(hRootKey);
FALSE;
}
CString strKey = _T();
lRet = (hRootKey, strKey.(), , REG_SZ, (LPBYTE)(LPCTSTR)strExePath, strExePath.() * (TCHAR));
(lRet != ERROR_SUCCESS) {
(hRootKey);
FALSE;
}
strKey = _T();
HKEY hDefaultIconKey = ;
lRet = (hRootKey, strKey, , , , KEY_ALL_ACCESS, , &hDefaultIconKey, &dwDisposition);
(lRet != ERROR_SUCCESS) {
(hRootKey);
FALSE;
}
CString strExePathPlus = strExePath + _T();
lRet = (hDefaultIconKey, , , REG_SZ, (LPBYTE)(LPCTSTR)strExePathPlus, strExePathPlus.() * (TCHAR));
(lRet != ERROR_SUCCESS) {
(hDefaultIconKey);
(hRootKey);
FALSE;
}
strKey = _T();
HKEY hShellKey = ;
lRet = (hDefaultIconKey, strKey, , , , KEY_ALL_ACCESS, , &hShellKey, &dwDisposition);
(lRet != ERROR_SUCCESS) {
(hDefaultIconKey);
(hRootKey);
FALSE;
}
strKey = _T();
HKEY hOpenKey = ;
lRet = (hShellKey, strKey, , , , KEY_ALL_ACCESS, , &hOpenKey, &dwDisposition);
(lRet != ERROR_SUCCESS) {
(hDefaultIconKey);
(hRootKey);
FALSE;
}
strKey = _T();
HKEY hCommandKey = ;
lRet = (hOpenKey, strKey, , , , KEY_ALL_ACCESS, , &hCommandKey, &dwDisposition);
(lRet != ERROR_SUCCESS) {
(hOpenKey);
(hDefaultIconKey);
(hRootKey);
FALSE;
}
CString strCmdParam;
strCmdParam.(_T(), strExePath);
lRet = (hCommandKey, , , REG_SZ, (LPBYTE)(LPCTSTR)strCmdParam, strCmdParam.() * (TCHAR));
(lRet != ERROR_SUCCESS) {
(hCommandKey);
(hOpenKey);
(hDefaultIconKey);
(hRootKey);
FALSE;
}
(hCommandKey);
(hOpenKey);
(hDefaultIconKey);
(hRootKey);
TRUE;
}


