使用URI Scheme实现从web网页上打开本地C++应用程序

目录

1、需求描述

2、选择URI Scheme实现

3、何为URI Scheme?

4、将自定义的URL Scheme信息写入注册表的C++源码实现

5、如何实现最开始的3种需求

6、后续需要考虑的细节问题


       最近接连收到一些关于从web页面上启动我们C++软件的需求,希望我们能提供一些技术上的支持与协助。于是我们大概地研究了相关的实现方法,下面把研究的过程与结果在此做一个分享,希望能给朋友们提供一个借鉴或参考。

1、需求描述

       最近很多第三方开发厂商为了快速集成我们的系统及软件(作为子系统融入到他们的大型web业务系统中),不想基于SDK做费时费力的二次开发,提出直接从web网页启动我们软件的需求。简单地归纳了一下,类似的需求可以分以下几类:

1)仅仅是从web网页上将C++软件启动起来,即将软件调起来就行了,没有后续操作;

2)从web网页上将C++软件启动起来,并且启动时传递服务器地址和账户信息,让软件自动发起登陆;

3)从web网页上将C++软件启动起来,并且启动时传递一些信息,让软件执行指定的一些操作。

       其实上述需求可以简单的归结为,将C++软件启动起来,并给C++软件传递一些命令行参数,C++软件解析出参数,执行指定的操作。

2、选择URI Scheme实现

       如果是C++程序要启动C++软件,会比较简单,只要读一下安装程序时写入的程序安装路径,就能直接通过软件的全路径,直接就可以将C++软件启动起来。

       现在越来越多的系统都转向了B/S架构,用户可以随处随地访问到系统里面去,只要有网络有电脑就行了,不用再安装各种客户端软件了。但就像我们上面提到的一些客户一样,因为某些业务的需要,需要从web网页上启动基于C/S架构的客户端软件。

       web网页一般都是在浏览器中的打开的,出于安全的原因,web浏览器既不能直接读写注册表,也不能直接启动二进制文件,所以在web网页中想启动本地的应用程序似乎遇到了问题。其实这并不是问题,我们使用URI Scheme技术就能轻松地实现这样的需求。

3、何为URI Scheme?

使用URI Scheme实现从web网页上打开本地C++应用程序_第1张图片

       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节点

使用URI Scheme实现从web网页上打开本地C++应用程序_第2张图片

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

2)在QQGameProtocol根节点下创建DefaultIcon节点

使用URI Scheme实现从web网页上打开本地C++应用程序_第3张图片

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

3)在QQGameProtocol下创建shell节点

使用URI Scheme实现从web网页上打开本地C++应用程序_第4张图片

       先在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 = NULL;
	lRet = RegCreateKeyEx(hRootKey, strKey, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &hDefaultIconKey, &dwDisposition);
	if (lRet != ERROR_SUCCESS)
	{
		RegCloseKey(hRootKey);
		return FALSE;
	}

	// 给RootNode\DefaultIcon节点设置值
	CString strExePathPlus = strExePath + _T(",1");
	lRet = RegSetValueEx(hDefaultIconKey, NULL, 0, REG_SZ, (LPBYTE)(LPCTSTR)strExePathPlus,
		strExePathPlus.GetLength() * sizeof(TCHAR));
	if (lRet != ERROR_SUCCESS)
	{
		RegCloseKey(hDefaultIconKey);
		RegCloseKey(hRootKey);
		return FALSE;
	}

	// 3、在RootNode\DefaultIcon节点下创建子节点shell
	strKey = _T("shell");
	HKEY hShellKey = NULL;
	lRet = RegCreateKeyEx(hDefaultIconKey, strKey, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &hShellKey, &dwDisposition);
	if (lRet != ERROR_SUCCESS)
	{
		RegCloseKey(hDefaultIconKey);
		RegCloseKey(hRootKey);
		return FALSE;
	}

	// 4、在RootNode\DefaultIcon\shell节点下创建子节点open
	strKey = _T("open");
	HKEY hOpenKey = NULL;
	lRet = RegCreateKeyEx(hShellKey, strKey, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &hOpenKey, &dwDisposition);
	if (lRet != ERROR_SUCCESS)
	{
		RegCloseKey(hDefaultIconKey);
		RegCloseKey(hRootKey);
		return FALSE;
	}

	// 5、在RootNode\DefaultIcon\shell\open节点下创建子节点command
	strKey = _T("command");
	HKEY hCommandKey = NULL;
	lRet = RegCreateKeyEx(hOpenKey, strKey, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &hCommandKey, &dwDisposition);
	if (lRet != ERROR_SUCCESS)
	{
		RegCloseKey(hOpenKey);
		RegCloseKey(hDefaultIconKey);
		RegCloseKey(hRootKey);
		return FALSE;
	}

	// 给command节点设置值(命令行参数)
	CString strCmdParam;
	strCmdParam.Format(_T("\"%s\" \"%%1\""), strExePath);
	lRet = RegSetValueEx(hCommandKey, NULL, 0, REG_SZ, (LPBYTE)(LPCTSTR)strCmdParam,
		strCmdParam.GetLength() * sizeof(TCHAR));
	if (lRet != ERROR_SUCCESS)
	{
		RegCloseKey(hCommandKey);
		RegCloseKey(hOpenKey);
		RegCloseKey(hDefaultIconKey);
		RegCloseKey(hRootKey);
		return FALSE;
	}

	RegCloseKey(hCommandKey);
	RegCloseKey(hOpenKey);
	RegCloseKey(hDefaultIconKey);
	RegCloseKey(hRootKey);
	return TRUE;
}

5、如何实现最开始的3种需求

       搞清楚了使用URI Scheme规范实现从web页面中启动本地应用程序的方法,下面我们再回到最开始提出的3个需求,看看如何去实现。

       第一种需求不需要传递参数,后面两种需求则需要传递参数,我们使用一个带参数传递的Scheme节点即可。我们可以定义一个启动type类型标识launchtype,对于直接启动的,type为noparam。对于启动后发起自动登录的,type为autologin;对于启动后需要执行具体操作的,可以根据具体的业务,定义具体的type类型,这样更灵活。

       对于目标应用程序,则可以根据不同的type类型,解析对应的参数数据,并对参数的合法性进行校验。

       下面把web网页的测试代码给出来,保存成.html文件,用浏览器打开即可:




    
    Start exe demo


打开目标程序 

6、后续需要考虑的细节问题

       上面大概说了一下问题的解决办法和思路,其实还有很多细节需要去考虑。比如下面的几种场景:

1)程序可能没有安装

       如果目标应用程序没有安装,肯定是启动不起来的,是不是要检测启动失败的原因,然后自动跳转到安装程序的下载页面。

2)仅将目标程序启动起来,但目标程序已经运行

       一般情况下,很多程序都是单实例运行的,即只允许运行一个实例。假定目标程序是单实例运行的,点击web页面中的启动程序的链接时,已经有个进程在运行了,目标程序中要弹出程序已经运行的提示,并将已经启动的程序拉到前端显示。

3)启动程序后需要有后续操作,但目标程序已经运行

       启动程序后需要有后续操作,比如自动发起登录,但此时目标应用程序已经运行。如果已启动的进程还没登录,是要自动发起登录?还是搁置不管?如果已启动的进程已经登录,则提示已经启动,并将已启动的主窗口拉到最前显示。如果目标程序已经启动且已经登录成功,则需要将命令行参数发给已启动的进程,让该进程执行要执行的操作,比如加入会议。

PS. 微软官方说明连接:

Registering an Application to a URI Schemehttps://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85)?redirectedfrom=MSDNhttps://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/aa767914(v=vs.85)?redirectedfrom=MSDN

你可能感兴趣的:(Windows开发相关,c++,web网页,URI,Scheme,启动)