需求描述
近期收到多个第三方开发厂商的需求,希望在不进行二次 SDK 开发的前提下,直接从 Web 网页上启动本地的 C++ 客户端软件。这类需求通常可以归纳为以下三类:
- 基础启动:仅从 Web 页面将 C++ 客户端软件启动起来,后续操作由用户手动完成。
- 自动登录:启动软件并传递服务器地址、用户名和密码,让软件自动发起登录,登录后显示主界面。
- 业务联动:启动时传递特定信息(如会议 ID),让软件执行指定操作,例如自动加入指定的会议。
其实上述需求的核心逻辑是一致的:通过 Web 页面触发本地程序启动,并传递命令行参数,C++ 客户端解析参数后执行相应操作。
以浏览器打开腾讯会议的会议链接为例,点击链接后系统会弹出是否打开本地安装的腾讯会议程序的提示框,确认后即可直接加入会议。这种体验正是通过 URI Scheme 技术实现的。
选择 URI Scheme 实现
如果在 C++ 程序中启动另一个 C++ 软件,只需获取目标软件的安装路径(通常可从注册表读取)即可直接启动。但在 B/S 架构下,Web 浏览器出于安全考虑,无法直接读写注册表或访问本地文件系统,因此不能像 C++ 程序那样直接调用二进制文件。
要解决这一问题,我们需要使用 URI Scheme 技术与规范。其核心思想是将本地应用程序的信息写入系统注册表,使操作系统识别特定的 URL 协议前缀,从而在浏览器中点击链接时能够唤起本地应用。
何为 URI Scheme?
URI(Uniform Resource Identifier)是统一资源标志符。URI Scheme 是一种技术规范,用于标识资源的处理方式。对于本地应用启动场景,我们需要在注册表的 HKEY_CLASSES_ROOT 下创建一个自定义的 Scheme 节点。
具体步骤如下(以 QQGame 为例):
-
创建根节点:在
HKEY_CLASSES_ROOT下创建QQGameProtocol节点。该名称即为 Web 页面上启动对应程序的 URL 前缀(如QQGameProtocol://)。给该节点添加一个名为URL Protocol的键值,Value 设置为本地应用程序的完整路径。 -
设置图标:在根节点下创建
DefaultIcon节点,设置默认的字符串键值(REG_SZ 类型),格式为'应用程序全路径,图标索引',用于指定该 URI 方案使用的图标。 -
配置命令:在根节点下依次创建
shell->open->command节点。其中command节点的键值用于指定启动目标应用程序时的命令行参数。一般格式为"目标程序路径" "%1"。如果需要传递多个参数,可以自定义组合格式,例如#serveraddr=192.168.72.135#username=admin1#password=123456。
当在 Web 页面上点击 SchemeName:// 链接时,系统会查找注册表中对应的节点,取出目标程序路径及参数,进而启动本地应用。
注意:如果
command节点中设置了%1传递参数的标识,则 Web 网页中的 URL 必须带参数。若需支持不带参数启动,可在程序中约定不传参数的标识符(如noparam),解析到该标识时直接启动程序。
注册表写入的 C++ 源码实现
下面给出将自定义 URL Scheme 信息写入注册表的 C++ 源码实现。代码基于 MFC/ATL 风格编写,使用了 RegCreateKeyEx 和 RegSetValueEx API。
BOOL WriteURISchemaReg() {
CString strExePath = m_strInstallPath + _T();
CString strProtocolName = _T();
HKEY hRootKey = ;
DWORD dwKeyValue = ;
DWORD dwDisposition = ;
UCHAR szBuf[MAX_PATH] = { };
lRet = ::(HKEY_CLASSES_ROOT, ProtocalNodeName, , , , 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;
}


