1、需求描述
近期有需求希望从 Web 页面启动 C++ 客户端软件,以下研究过程与结果供参考。类似的需求可以分以下几类:
**1)**仅仅是从 Web 网页上将 C++ 客户端软件启动起来,即将软件调起来就行了,没有后续自动操作。用户根据自己的需要,手动操作我们的软件。
**2)**从 Web 网页上将 C++ 客户端软件启动起来,并给软件传递服务器地址、用户名和密码,让软件自动发起登陆,登录成功后显示登录后的界面。
**3)**从 Web 网页上将 C++ 客户端软件启动起来,并且启动时传递一些信息,让软件执行指定的一些操作。比如给软件传递服务器地址、用户名和密码等信息,让软件自动发起登录,并加入到指定的会议中。
其实上述需求可以简单的归结为,将 C++ 客户端软件启动起来,并给 C++ 客户端软件传递一些命令行参数,C++ 客户端软件解析出参数,执行指定的操作。
以浏览器打开腾讯会议的会议链接为例,比如在 IM 软件中分享的腾讯会议的会议链接如下所示:

点击上面的会议链接,系统用系统中安装的浏览器打开链接(或者手动将上述会议链接拷贝到浏览器中打开),如下所示:

点击加入会议按钮,会弹出是否要打开本地安装的腾讯会议程序的提示框:

点击打开腾讯会议,将本地安装的腾讯会议启动起来,并自动加入到指定的会议中:

2、选择 URI Scheme 实现
如果是在 C++ 程序中启动一个 C++ 软件,会比较简单,只要获取一下目标软件的安装路径(可以从注册表中读取,安装程序时会将软件的安装路径写到注册表中),就能直接通过软件的全路径,直接将软件启动起来了。
现在越来越多的系统都转向 B/S 架构,用户可以随处随地访问到系统中去,只要有网络有电脑就行了,不用再安装各种客户端软件了。就像我们上面提到的一些客户一样,因为某些业务场景的需要,可能需要从 Web 网页上启动系统中安装的基于 C/S 架构的客户端软件。
Web 网页一般都是在浏览器中打开的,出于安全的原因,Web 浏览器既不能直接读写注册表,即无法通过访问注册表获取要启动软件的安装路径,所以无法像 C++ 程序那样直接启动二进制文件,所以在 Web 网页中想启动本地的应用程序似乎遇到了问题。其实这并不是问题,我们使用 URI Scheme 技术与规范就能实现这样的需求。
3、何为 URI Scheme?

URI,全称是 Uniform Resource Identifier,统一资源标志符。在 Web 开发领域,其表示的是 Web 上每一种可用的资源,如 HTML 文档、图片、视频等。URI Scheme,我们称之为 URI 方案,是一种技术规范,其中的 URI 是个更宽泛的概念,它可以是一个本地的文件,也可以是一个网络上的视频。
**从 Web 网页中启动本地应用程序的 URI Scheme 规范中,需要将本地应用程序的信息通过写注册表的方式注册到系统中,然后在网页中使用'SchemeName://'就可以只在启动本地程序了。**具体的做法是,在注册表的 HKEY_CLASSES_ROOT 下创建一个自定义的 SchemeName 注册表节点,然后再在该节点下创建多个节点,并在给相关节点设置注册表键值。
以 QQ 内嵌的 QQGame 为例,添加注册表信息的步骤如下:
1)在 HKEY_CLASSES_ROOT 下创建 QQGameProtocol 节点

QQGameProtocol 就是对应的 Scheme 方案名称,也是 Web 页面上启动对应程序的 URL 的前缀名称,即 QQGameProtocol://。然后给该节点添加一个 URL Protocol 名称的键值,将其 Value 设置为本地应用程序的完整路径。对于当前的 QQGameProtocol,就是 C:\Users\Public\Documents\Tencent\QQGameMicro\QQGwp.exe,如上图所示。
2)在 QQGameProtocol 根节点下创建 DefaultIcon 节点

给 DefaultIcon 节点设置默认的字符串键值(REG_SZ 类型),其 Value 的格式为'应用程 序全路径,图标索引'的形式,该键值是用来指定该 URI 方案使用的图标。本例中的 Value 为:C:\Users\Public\Documents\Tencent\QQGameMicro\QQGwp.exe,1,如上图所示。
3)在 QQGameProtocol 下创建 shell 节点

先在 QQGameProtocol 下创建 shell 节点,然后在 shell 节点下创建 open 节点,然后在 open 节点创建 command 节点。shell 节点和 open 节点不需要设置键值,command 节点需要设置键值,其键值用来指定启动目标应用程序时是否给目标程序传递命令行参数。
一般只需要设置传递一个参数即可,比如当前 Scheme 下的"C:\Users\Public\Documents\Tencent\QQGameMicro\QQGwp.exe" "%1"。如果要传递多个参数,可以自定义一个组合格式,命令行只用一个参数即可。比如我们要给目标程序传递服务器地址、用户名和密码,可以采用这样的组合格式:
#serveraddr=192.168.72.135#username=admin1#password=123456
即将要传递的多个参数按指定的格式组合起来生成一个命令行字符串参数即可。
当在 Web 页面上点击'SchemeName://'链接时,就会到系统注册表的 HKEY_CLASSES_ROOT 节点下查找 SchemeName 节点项,找到后取出目标应用程序的全路径,并查找传递的命令行参数个数,这样就能把本地的目标应用程序启动起来了。
如果要给目标程序传递参数,则使用'SchemeName://参数'的形式。经测试发现,如果在 command 节点中设置了%1 传递参数的标识,则 Web 网页中设置的 URL 必须要带参数,即'SchemeName://参数'。如果使用不带参数的 URL:'SchemeName://',则无法启动目标程序。
那如何既要支持不传参数启动,也要支持传参数启动呢?难道要在注册表中创建两个不同的 SchemeName 节点?其实不用这么麻烦,使用一个带参数的 SchemeName 节点就够了,对于直接启动目标程序不带启动参数的,也可以携带一个标识参数,在程序中约定不传参数的标识符,比如 noparam,当程序中解析出 noparam,则表示是不带参数启动的,直接启动程序即可,不用做后续的操作。
4、将自定义的 URL Scheme 信息写入注册表的 C++ 源码实现
下面给出将自定义的 URL Scheme 信息写入注册表的 C++ 源码实现:
BOOL WriteURISchemaReg() {
// exe 程序的完整路径
CString strExePath = m_strInstallPath + _T("xyzlink.exe");
// URI Scheme 名称
CString strProtocolName = _T("XyzlinkProtocol");
HKEY hRootKey = NULL;
DWORD dwKeyValue = 0;
DWORD dwDisposition = 0;
UCHAR szBuf[MAX_PATH] = { 0 };
// 1、在 HKEY_CLASSES_ROOT 下创建 URI Schema 相关注册表的根节点 RootNode
long lRet = ::RegCreateKeyEx(HKEY_CLASSES_ROOT, ProtocalNodeName, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &hRootKey, &dwDisposition);
if (lRet != ERROR_SUCCESS) {
return FALSE;
}
// 给根节点 RootNode 设置值 1
lRet = ::RegSetValueEx(hRootKey, NULL, 0, REG_SZ, (LPBYTE)(LPCTSTR)strProtocolName, strProtocolName.GetLength() * sizeof(TCHAR));
if (lRet != ERROR_SUCCESS) {
RegCloseKey(hRootKey);
return FALSE;
}
// 给根节点 RootNode 设置值 2
CString strKey = _T("URL Protocol");
lRet = RegSetValueEx(hRootKey, strKey.GetBuffer(0), 0, REG_SZ, (LPBYTE)(LPCTSTR)strExePath, strExePath.GetLength() * sizeof(TCHAR));
if (lRet != ERROR_SUCCESS) {
RegCloseKey(hRootKey);
return FALSE;
}
// 2、在根节点 RootNode 下创建 DefaultIcon 节点
strKey = _T("DefaultIcon");
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;
}
5、如何实现最开始的 3 种需求
搞清楚了使用 URI Scheme 规范实现从 Web 页面中启动本地应用程序的方法,下面我们再回到最开始提出的 3 个需求,看看如何去实现。
第一种需求不需要传递参数,后面两种需求则需要传递参数,我们使用一个带参数传递的 Scheme 节点即可。我们可以定义一个启动 type 类型标识 launchtype,对于直接启动的,type 为 noparam。对于启动后发起自动登录的,type 为 autologin;对于启动后需要执行具体操作的,可以根据具体的业务,定义具体的 type 类型,这样更灵活。
对于目标应用程序,则可以根据不同的 type 类型,解析对应的参数数据,并对参数的合法性进行校验。
下面把Web 网页的测试代码给出来,保存成.html 文件,用浏览器打开即可:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Start exe demo</title>
</head>
<body>
<a href="SchemeName://">打开目标程序</a>
</body>
</html>
6、后续需要考虑的细节问题
上面大概说了一下问题的解决办法和思路,其实还有很多细节需要去考虑。比如下面的几种场景:
1)程序可能没有安装
如果目标应用程序没有安装,肯定是启动不起来的,是不是要检测启动失败的原因,然后自动跳转到安装程序的下载页面。
2)仅将目标程序启动起来,但目标程序已经运行
一般情况下,很多程序都是单实例运行的,即只允许运行一个实例。假定目标程序是单实例运行的,点击 Web 页面中的启动程序的链接时,已经有个进程在运行了,目标程序中要弹出程序已经运行的提示,并将已经启动的程序拉到前端显示。
3)启动程序后需要有后续操作,但目标程序已经运行
启动程序后需要有后续操作,比如自动发起登录,但此时目标应用程序已经运行。如果已启动的进程还没登录,是要自动发起登录?还是搁置不管?如果已启动的进程已经登录,则提示已经启动,并将已启动的主窗口拉到最前显示。如果目标程序已经启动且已经登录成功,则需要将命令行参数发给已启动的进程,让该进程执行要执行的操作,比如加入会议。
PS. 微软官方说明连接:


