最近收到不少需求,希望从 Web 网页直接启动本地的 C++ 客户端软件。这类场景通常出现在第三方厂商需要将我们的系统作为子系统集成到他们的大型业务中时,不想费时费力做 SDK 二次开发,而是希望通过链接唤起客户端。
需求分析
归纳起来,这类需求主要分为三类:
- 纯唤起:仅从 Web 将 C++ 客户端启动,用户手动操作后续流程。
- 自动登录:启动并传递服务器地址、用户名和密码,让软件自动发起登录。
- 执行特定操作:启动时传递信息(如会议 ID),让软件自动加入指定会议或执行其他指令。
核心逻辑其实很明确:通过 Web 触发本地程序,并传递命令行参数,由 C++ 客户端解析参数执行对应操作。
以腾讯会议为例,在 IM 软件中分享会议链接,点击后浏览器会弹出是否打开本地程序的提示。确认后,本地客户端启动并自动加入会议。这种体验正是我们希望通过 URI Scheme 实现的。
为什么选择 URI Scheme
如果在 C++ 程序中启动另一个程序,只需获取安装路径即可。但在 B/S 架构下,Web 浏览器出于安全考虑,无法直接读写注册表获取安装路径,也不能直接调用二进制文件。URI Scheme 技术恰好能解决这个痛点。
URI(Uniform Resource Identifier)是统一资源标志符。URI Scheme 是一种技术规范,允许我们在系统中注册自定义协议。一旦注册成功,网页中使用 SchemeName:// 格式的链接即可启动对应的本地应用程序。
具体做法是在注册表的 HKEY_CLASSES_ROOT 下创建自定义节点,配置目标程序路径及命令行参数格式。当浏览器遇到该协议链接时,会查找注册表中的配置,进而启动程序。
注册表结构详解
以 QQGame 为例,配置步骤如下:
-
创建根节点:在
HKEY_CLASSES_ROOT下创建QQGameProtocol节点。这是 Scheme 的名称,也是 URL 的前缀(如QQGameProtocol://)。为该节点添加URL Protocol键值,值为空字符串,表示这是一个协议。 -
设置默认图标:创建
DefaultIcon子节点,设置其默认字符串值,格式为'程序全路径,图标索引'。例如:C:\Users\Public\Documents\Tencent\QQGameMicro\QQGwp.exe,1。 -
配置命令执行:依次创建
shell->open->command节点。其中command节点的键值用于指定启动命令。一般格式为"程序路径" "%1"。%1代表从 URL 中传递的参数。
如果需要传递多个参数,可以组合成一个字符串,例如:#serveraddr=192.168.72.135#username=admin1#password=123456。这样无论传多少个参数,命令行都只接收一个字符串,由程序内部解析。
注意:如果 command 节点中设置了 %1,则 URL 必须带参数;若不带参数则无法启动。为了兼容不传参数的情况,可以在程序中约定特殊标识(如 noparam),检测到该标识时跳过参数解析逻辑。
注册表写入源码实现
下面给出将自定义 URL Scheme 信息写入注册表的 C++ 源码实现。这段代码展示了如何遍历创建所需的注册表项并设置键值。
BOOL WriteURISchemaReg() {
// exe 程序的完整路径
CString strExePath = _T("C:\\Program Files\\MyApp\\app.exe");
CString strProtocolName = _T();
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), );
(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) {
(hShellKey);
(hDefaultIconKey);
(hRootKey);
FALSE;
}
strKey = _T();
HKEY hCommandKey = ;
lRet = (hOpenKey, strKey, , , , KEY_ALL_ACCESS, , &hCommandKey, &dwDisposition);
(lRet != ERROR_SUCCESS) {
(hOpenKey);
(hShellKey);
(hDefaultIconKey);
(hRootKey);
FALSE;
}
CString strCmdParam;
strCmdParam.(_T(), strExePath);
lRet = (hCommandKey, , , REG_SZ, (LPBYTE)(LPCTSTR)strCmdParam, strCmdParam.() * (TCHAR));
(lRet != ERROR_SUCCESS) {
(hCommandKey);
(hOpenKey);
(hShellKey);
(hDefaultIconKey);
(hRootKey);
FALSE;
}
(hCommandKey);
(hOpenKey);
(hShellKey);
(hDefaultIconKey);
(hRootKey);
TRUE;
}


