注册表(Registry,繁体中文版Windows称之为登录档)是Microsoft Windows中的一个重要的数据库,用于存储系统和应用程序的设置信息。早在Windows 3.0推出OLE技术的时候,注册表就已经出现。随后推出的Windows NT是第一个从系统级别广泛使用注册表的操作系统。但是,从Microsoft Windows 95开始,注册表才真正成为Windows用户经常接触的内容,并在其后的操作系统中继续沿用至今。
这里首先介绍一些注册表的基本概念:
文件夹/预定义项 | 说明 |
---|---|
HKEY_CLASSES_ROOT | 此项配置可以确保使用windows资源管理器时打开正确的程序 |
HKEY_CURRENT_USER | 包含当前登录用户的配置信息的根目录 |
HKEY_LOCAL_MACHINE | 包含针对该计算机的配置信息 |
HKEY_USERS | 包含计算机上所有用户的配置文件的根目录 |
HKEY_CURRENT_CONFIG | 包含本地计算机系统启动时所用的配置文件的所有信息 |
数据类型 | 说明 |
---|---|
REG_BINARY | 未处理的二进制数据 |
REG_DWORD | 4字节长的数 |
REG_SZ | 字符串 |
REG_EXPAND_SZ | 长度可变的字符串 |
从网页中的一个链接启动一个指定的windows本地IM应用程序,并且打开对应的会话窗口。
实现这个需求之前,我们不妨看一下腾讯的QQ是如何实现的:
Win+R
打开运行
搜索框,输入regedit
回车后打开注册表:
HKEY_CLASSES_ROOT
注册表项下,寻找Tencent
,发现QQ实现网页启动本地QQ的注册表信息如下:
有了QQ的指点,下面就可以以此为模板,依葫芦画瓢,手动设置Stone程序的注册表:
HKEY_CLASSES_ROOT
.HKEY_CLASSES_ROOT
右键新建——项,名字自定义,例如StoneWebshell
,双击默认数据,数值数据修改为Stone
,再在空白处右键新建->字符串值,名称为URL Protocol
,数值数据为应用程序所在地址C:\Program Files (x86)\Netease\Stone\stone.exe
。然后再在自己新建的这个节点上,右键新建两个项,分别起名为DefaultIcon,shell,将DefaultIcon默认项的数值数据同样修改为C:\Program Files (x86)\Netease\Stone\stone.exe
,然后再在shell这个节点上右键,新建项open,再在open上新建项command,将默认的数值数据修改为"C:\Program Files (x86)\Netease\Stone\stone.exe" "%1"
,最终的结果如下图所示:
新建注册表文件stone_register.reg
,写入如下的内容:
Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\StoneWebshell]
"URL Protocol"="C:\\Program Files (x86)\\Netease\\Stone\\stone.exe"
@="Stone"
[HKEY_CLASSES_ROOT\StoneWebshell\DefaultIcon]
@="C:\\Program Files (x86)\\Netease\\Stone\\stone.exe,1"
[HKEY_CLASSES_ROOT\StoneWebshell\shell]
[HKEY_CLASSES_ROOT\StoneWebshell\shell\open]
[HKEY_CLASSES_ROOT\StoneWebshell\shell\open\command]
@="\"C:\\Program Files (x86)\\Netease\\Stone\\stone.exe\" \"%1\""
保存后退出.
双击该文件,弹出的对话框分别点击“是”和"确定",即可实现注册表的修改。
查看注册表信息跟上面完全一致,这里不再进行展示。
编写如下的网页程序:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
<div>
<a href="stone://startSession?account=louxiujun&scene=p2p"> 打开Stone,立即沟通 </a>
</div>
</body>
</html>
打开Stone,立即沟通
超链接,浏览器弹出要打开stone吗?
的对话框,选择打开Stone
:
Stone
程序:
小结:这里只是通过手动修改和编写注册表文件的方式大致了解网页启动本地应用程序的实现原理,却还不能满足我们打开相应的会话的需求,而该需要的实现需要由程序对网页传递的参数进行解析和处理。下面我将通过自己在进行Stone-win端程序实现该需要的亲身经历,详细介绍该需求的实现过程,为大家今后实现类似的需求提供一个可行的参考。
#define PRODUCT_NAME1 _T("stone")
#define URL_PROTOCOL_VALUE $INSTDIR PROCESS_NAME
#define DEFAULT_ICON_VALUE URL_PROTOCOL_VALUE _T(",1")
#define COMMAND_VALUE _T("\"") URL_PROTOCOL_VALUE _T("\" \"%1\"")
//添加URL Protocol相关的注册表信息
/*
为了支持网页打开本地APP,需要使用URL Protocol,需要修改使用如下的注册表信息:
[*.reg文件]
Windows Registry Editor Version 5.00
[HKEY_CLASSES_ROOT\StoneWebshell]
"URL Protocol"="C:\\Program Files (x86)\\Netease\\Stone\\stone.exe"
@="Stone"
[HKEY_CLASSES_ROOT\StoneWebshell\DefaultIcon]
@="C:\\Program Files (x86)\\Netease\\Stone\\stone.exe,1"
[HKEY_CLASSES_ROOT\StoneWebshell\shell]
[HKEY_CLASSES_ROOT\StoneWebshell\shell\open]
[HKEY_CLASSES_ROOT\StoneWebshell\shell\open\command]
@="\"C:\\Program Files (x86)\\Netease\\Stone\\stone.exe\" \"%1\""
*/
_WriteRegValue(HKCR, PRODUCT_NAME1, _T(""), PRODUCT_NAME1);
_WriteRegValue(HKCR, PRODUCT_NAME1, _T("URL Procotol"), URL_PROTOCOL_VALUE);
_WriteRegValue(HKCR, _T("stone\\DefaultIcon"), _T(""), DEFAULT_ICON_VALUE);
_WriteRegValue(HKCR, _T("stone\\shell"), _T(""), _T(""));
_WriteRegValue(HKCR, _T("stone\\shell\\open"), _T(""), _T(""));
_WriteRegValue(HKCR, _T("stone\\shell\\open\\command"), _T(""), COMMAND_VALUE);
//删除URL Protocol相关的注册表信息
_DelRegKey(HKCR, _T("stone\\shell\\open\\command"));
_DelRegKey(HKCR, _T("stone\\shell\\open"));
_DelRegKey(HKCR, _T("stone\\shell"));
_DelRegKey(HKCR, _T("stone\\DefaultIcon"));
_DelRegKey(HKCR, PRODUCT_NAME1);
Old Stone
.<a href="stone://startSession?account=louxiujun&scene=p2p"> 打开Stone,立即沟通 </a>
超链接的文本,浏览器根据URL Protocol协议,查找Stone的注册表信息,成功启动一个新的Stone程序,我们将其称为New Stone
。New Stone
的主线程检测到Old Stone
的存在,就不会去启动UI线程,用户也就察觉不到New Stone
的存在了。New Stone
接下来需要将获取到的命令行参数"stone://startSession?account=louxiujun&scene=p2p"
写入到一个指定路径下,然后New Stone
主线程退出。Old Stone
在启动后会至少同时运行有三个线程:主线程、UI线程和文件读取线程,其中文件读取线程每隔一段时间就会检查指定路径下的文件是否存在,如果存在则读取其中的内容,并将文件删除,之后将参数解析后,传递给UI线程,由UI线程负责启动新的会话窗口,;如果没有检测到则休眠一段时间后继续进行检测。
下面是一些关键的函数代码:
文件读取的线程类头文件
...
static void OpenSession();
class FileReadThread : public nbase::Thread
{
public:
typedef std::function<void(void)> OpenSessionCallback; /**< 打开会话*/
FileReadThread(ThreadId thread_id);
~FileReadThread(void);
void Stop();
private:
virtual void Run() override;
void Close();
static void CallbackOpenSessionForm(const void *callback);
private:
ThreadId thread_id_;
std::wstring file_path_;
const int sleep_miliiseconds;
};
文件读取的线程类的实现文件fileread.cpp。文件读取的线程类会循环检测指定的文件夹中的文件是否存在,如果存在则读取中其中的内容并将文件删除,如果文件不存在则睡眠3秒后继续执行上述检测操作。这里的关键在于PostTaskToUIThread
,因为文件读取属于子线程,本身不适合执行UI线程的打开窗口的方法,因此需要通过这一函数接口,将打开会话窗口的操作交给UI主线程来完成。
...
FileReadThread::FileReadThread(ThreadId thread_id)
: thread_id_(thread_id), sleep_miliiseconds(2000)
{
nbase::ThreadManager::RegisterThread(thread_id_);
file_path_ = QPath::GetAppPath() + L"file.txt";
}
FileReadThread::~FileReadThread(void)
{
remove(nbase::UTF16ToUTF8(file_path_).c_str());
}
void FileReadThread::Run()
{
std::ifstream url_cmd_in_;
while (true)//循环检测
{
url_cmd_in_.open(file_path_, std::ios::in);
if (url_cmd_in_.is_open())//文件存在
{
std::string str((std::istreambuf_iterator<char>(url_cmd_in_)), std::istreambuf_iterator<char>());
url_cmd_in_.close();
wstring str_cmd = nbase::UTF8ToUTF16(str);
QCommand::ParseURLProtocolCommand(str_cmd);
OpenSessionCallback* open_session = new OpenSessionCallback(&OpenSession);
CallbackOpenSessionForm(open_session);
// 删除文件,成功返回0,否则返回-1
if (0 == remove(nbase::UTF16ToUTF8(file_path_).c_str()))
{
// 退出
QLOG_APP(L"delete file suceffsul");
}
else
{
QLOG_APP(L"delete file failed");
}
}
Sleep(sleep_miliiseconds);//休眠2秒后接着检测
}
}
void FileReadThread::CallbackOpenSessionForm(const void *callback)
{
if (callback != nullptr)
{
FileReadThread::OpenSessionCallback *cb = (FileReadThread::OpenSessionCallback *)callback;
nim::PostTaskToUIThread(std::bind((*cb)));
//(*cb)();
}
}
void OpenSession()
{
std::wstring session_id = QCommand::Get(L"account");
nim::NIMSessionType session_type;
try{
if (QCommand::Get(L"scene") == L"p2p")
{
session_type = static_cast<nim::NIMSessionType>(nim::kNIMSessionTypeP2P);
}
else if (QCommand::Get(L"scene") == L"team")
{
session_type = static_cast<nim::NIMSessionType>(nim::kNIMSessionTypeTeam);
}
if (!session_id.empty())
{
nim_comp::SessionManager::GetInstance()->OpenSessionForm(nbase::UTF16ToUTF8(session_id), session_type);
QCommand::Erase(L"account");
QCommand::Erase(L"scene");
}
}
catch (exception e)
{
std::cout << e.what() << endl;
}
}
void FileReadThread::Close()
{
nbase::ThreadManager::UnregisterThread();
QLOG_APP(L"FileReadThread Close");
}
void FileReadThread::Stop()
{
Terminate();
Close();
}
main程序中的lpszCmdLine
就是启动应用程序时的命令行参数列表,多个参数间以空格分割,如果是通过URL Protocol启动的应用程序,如果网页标签为<a href="stone://startSession?account=hzsunaichun&scene=p2p"> 打开Stone,立即沟通 </a>
,那么这里传递给main函数的参数即为"stone://startSession?account=hzsunaichun&scene=p2p"
,包含引号,防止传递的参数中包含有空格,与命令行参数列表中的其他参数造成混淆。
int WINAPI wWinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPWSTR lpszCmdLine, int nCmdShow)
{
...
std::wstring cmd = lpszCmdLine;
if (cmd.find(L"\"stone://") != string::npos)//检测到浏览器传入的URL Protocol参数
{
int start_pos = cmd.find(L"\"stone://");
int end_pos = cmd.rfind(L"\"");
std::wstring original_cmd = cmd.substr(0, start_pos);
std::wstring url_param = cmd.substr(start_pos + 1, end_pos - start_pos-1);
GwUtil *gw = GwUtil::GetInstance();
if (gw->getProcessCount() > 1)//检测到已经有至少一个Stone进程在运行
{
std::wstring file_path = QPath::GetAppPath() + L"file.txt";
std::ofstream file;
file.open(file_path, std::ios::out | std::ios::trunc);
if (file.is_open())//文件打开成功
{
file << nbase::UTF16ToUTF8(url_param);
file.close();
}
else{
QLOG_APP(L"file open failed in main()");
}
return -1;
}
else//当前没有stone程序在运行
{
QCommand::ParseURLProtocolCommand(url_param);//解析URL参数
cmd = original_cmd;
}
}
...
类Qcmmand中保存有一个全局可用的map结构key_value_
,可以通过指定key值的get函数获取对应的值,可以用来保存外部传递给main函数的命令行参数。此外,为了实现功能,我们还定义了几个辅助的函数:Request函数用于解析URL中的参数,例如从"stone://openSession?id=123&scene=p2p"
中解析出id的参数123
以及scene
的参数p2p
,而ParseURLProtocolCommand
则是用来分别调用Request
函数,将解析后的结果保存到全局的key_value_
中。
...
static std::map<std::wstring,std::wstring> key_value_;
...
//解析URL中的参数,例如从"stone://openSession?id=123&scene=p2p"中解析出id的参数123以及scene的参数p2p
std::wstring QCommand::Request(std::wstring &urls, const std::string &request)
{
smatch result;
string url = nbase::UTF16ToUTF8(urls);
if (regex_search(url.cbegin(),url.cend(),result,regex(request+"=(.*?)&")))
{
return nbase::UTF8ToUTF16(result[1]);
}
else if (regex_search(url.cbegin(), url.cend(), result, regex(request + "=(.*)")))
{
return nbase::UTF8ToUTF16(result[1]);
}
else
{
return std::wstring(L"");
}
}
//命令行参数解析:解析出URL Protocol信息
void QCommand::ParseURLProtocolCommand(std::wstring &cmd)
{
key_value_[L"account"] = Request(cmd, "account");
key_value_[L"scene"] = Request(cmd, "scene");
}
...
std::wstring QCommand::Get( const std::wstring &key )
{
std::map<std::wstring,std::wstring>::const_iterator i = key_value_.find(key);
if(i == key_value_.end())
return L"";
else
return i->second;
}
打开相应会话的启动函数:
//根据外部URL传入的会话参数,打开相应的聊天会话窗口
void MainForm::OnOpenFromURLSessionWindow()
{
std::wstring session_id = QCommand::Get(L"account");
nim::NIMSessionType session_type;
try{
if (QCommand::Get(L"scene") == L"p2p")
{
session_type = static_cast<nim::NIMSessionType>(nim::kNIMSessionTypeP2P);
}
else if (QCommand::Get(L"scene") == L"team")
{
session_type = static_cast<nim::NIMSessionType>(nim::kNIMSessionTypeTeam);
}
if (!session_id.empty())
{
SessionManager::GetInstance()->OpenSessionForm(nbase::UTF16ToUTF8(session_id), session_type);
QCommand::Erase(L"account");
QCommand::Erase(L"scene");
}
}
catch (exception e)
{
std::cout << e.what() << endl;
}
}
编写网页测试程序test_Webshell.html
如下:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
</head>
<body>
<div>
<a href="stone://startSession?account=louxiujun&scene=p2p"> 打开Stone,立即沟通 </a>
</div>
</body>
</html>
本文来自网易实践者社区,经作者娄修俊授权发布。