网页html启动Windows本地应用程序

阿凡达2018-07-06 11:44

注册表简介

注册表(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是如何实现的:

  1. 首先,使用快捷键Win+R打开运行搜索框,输入regedit回车后打开注册表:

  1. 如果本机安装有QQ程序,则在HKEY_CLASSES_ROOT注册表项下,寻找Tencent,发现QQ实现网页启动本地QQ的注册表信息如下:

修改注册表

手动修改注册表

有了QQ的指点,下面就可以以此为模板,依葫芦画瓢,手动设置Stone程序的注册表:

  1. 首先,我们需要明确的是,注册表信息主要有以下几类,这里需要修改的是HKEY_CLASSES_ROOT.
  2. 在注册表项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",最终的结果如下图所示:

注册表文件修改注册表

  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\""
    

    保存后退出.

  2. 双击该文件,弹出的对话框分别点击“是”和"确定",即可实现注册表的修改。

  3. 查看注册表信息跟上面完全一致,这里不再进行展示。

  4. 编写如下的网页程序:

  • <!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

  1. 成功启动本地Stone程序:                                                 

小结:这里只是通过手动修改和编写注册表文件的方式大致了解网页启动本地应用程序的实现原理,却还不能满足我们打开相应的会话的需求,而该需要的实现需要由程序对网页传递的参数进行解析和处理。下面我将通过自己在进行Stone-win端程序实现该需要的亲身经历,详细介绍该需求的实现过程,为大家今后实现类似的需求提供一个可行的参考。

项目实战

  1. 在程序的安装和卸载工程中分别添加注册表的写入和删除操作。这样在应用程序安装的时候实现注册表信息的写入,在应用程序卸载的时候实现注册表信息的删除,不造成任何遗留的垃圾信息。关键代码如下:

#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);

  1. 下图描述的是实现网页启动本地IM应用程序Stone,并且打开相应会话的实现思路:

  • 本地已经启动一个Stone程序,为了区分,我们暂且将其称为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;
    }
}
  1. 编写网页测试程序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>
    

参考资料

  1. html网页调用本地exe程序的实现方法
  2. 利用URL Protocol实现网页调用本地应用程序
  3. 从网页Web上调用本地应用程序(.jar、.exe)的主流处理方法
  4. 自定义URL Protocol Handler 呼出应用程序
  5. Url Protocol-从网页中打开应用程序(exe)-使用小记

本文来自网易实践者社区,经作者娄修俊授权发布。