通过 URI Scheme 从 Web 页面启动本地 C++ 应用
需求背景
近期收到多个第三方厂商的需求,希望在不进行复杂 SDK 二次开发的前提下,直接从 Web 页面启动本地的 C++ 客户端软件。这类需求通常分为三类:仅启动程序;启动并自动登录;启动并执行特定业务操作(如加入会议)。本质上,这要求 Web 端能向本地程序传递命令行参数,由程序解析并执行相应逻辑。
以常见的腾讯会议为例,点击 IM 软件中的会议链接,浏览器会询问是否打开本地客户端。确认后,本地程序启动并自动加入指定会议。这种体验的核心技术就是 URI Scheme。
什么是 URI Scheme
URI(Uniform Resource Identifier)是统一资源标识符。URI Scheme 是一种技术规范,允许通过特定的 URL 前缀(如 qqgame://)来触发本地应用程序。
要实现 Web 页面启动本地程序,必须先在系统中注册该 Scheme。具体做法是在 Windows 注册表的 HKEY_CLASSES_ROOT 下创建自定义节点,关联目标应用程序的路径和启动命令。当浏览器遇到匹配的链接时,会读取注册表信息并调用对应的可执行文件。
注册表配置与 C++ 实现
我们需要编写 C++ 代码将 Scheme 信息写入注册表。以下是关键步骤的实现逻辑:
首先,在 HKEY_CLASSES_ROOT 下创建根节点,例如 XyzlinkProtocol。设置默认值为协议名称,并添加 URL Protocol 键值指向可执行文件路径。
接着,创建 DefaultIcon 节点,设置图标路径。
最后,构建 shell\open\command 层级。这是最关键的一步,command 节点的默认值决定了程序如何被启动。为了支持参数传递,我们通常使用 %1 占位符。
下面是完整的注册表写入函数示例:
BOOL WriteURISchemaReg() {
// 获取当前安装路径
CString strExePath = m_strInstallPath + _T("xyzlink.exe");
CString strProtocolName = _T("XyzlinkProtocol");
HKEY hRootKey = NULL;
DWORD dwKeyValue = 0;
DWORD dwDisposition = 0;
UCHAR szBuf[MAX_PATH] = { 0 };
// 1. 创建根节点 RootNode
long lRet = ::RegCreateKeyEx(HKEY_CLASSES_ROOT, strProtocolName, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &hRootKey, &dwDisposition);
if (lRet != ERROR_SUCCESS) return FALSE;
// 设置协议名称
lRet = ::RegSetValueEx(hRootKey, NULL, , 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); (hShellKey); FALSE; }
strKey = _T();
HKEY hCommandKey = ;
lRet = (hOpenKey, strKey, , , , KEY_ALL_ACCESS, , &hCommandKey, &dwDisposition);
(lRet != ERROR_SUCCESS) { (hOpenKey); (hDefaultIconKey); (hRootKey); (hShellKey); FALSE; }
CString strCmdParam;
strCmdParam.(_T(), strExePath);
lRet = (hCommandKey, , , REG_SZ, (LPBYTE)(LPCTSTR)strCmdParam, strCmdParam.() * (TCHAR));
(lRet != ERROR_SUCCESS) {
(hCommandKey); (hOpenKey); (hDefaultIconKey); (hRootKey); (hShellKey);
FALSE;
}
(hCommandKey); (hOpenKey); (hDefaultIconKey); (hRootKey); (hShellKey);
TRUE;
}


