c++ 跟网页对接,VC与JavaScript交互

为什么要让VC与JavaScript交互?
1.有时候我们需要让自己的软件打开一个网页,来获取页面上的一些数据。这时,可以用mshtml解析HTML提取出数据,也可以向HTML文档动态写入我们准备好的JS代码,用JS代码获取HTML上的数据,然后用VC调用该JS代码取得数据。
2.有时候我们需要让自己的软件打开一个网页并操控该网页,填写表单,提交等动作。这时,可以用mshtml操作HTML,给文本框赋值,模拟点击按钮。也可以向HTML文档动态写入我们准备好的JS代码,用JS代码实现填单,提交等动作,然后用VC调用一下JS代码即可。
3.有时候我们需要用HTML网页做界面,用JS管理HTML页面,用VC调用JS传递进数据,JS把这些数据通过HTML显示在界面上。CHtmlDialog正是这种产物。
4.有时候我们需要基于HTTP协议与WEB服务器直接交互。比如基于HTTP协议来登录QQ空间。但是对浏览器抓包发现QQ号是明文传输,但QQ密码是密文,它是如何加密的呢?这些加密算法肯定都在页面上的JS里,因为这个加密过程是在客户浏览器上实现的。我们可以找到加密相关的JS代码,仔细阅读分析,改写为C++代码实现,但比较复杂。一个简单的办法就是直接把这些用于加密的JS代码复制出来,用VC调用JS代码,让它的JS代码完成加密过程,然后我们把密文拿过来,用HTTP协议POST发送到WEB服务器,即完成了登陆动作。
5.等等,还有很多用途。

那么,在Windows平台上用VC开发的程序,如何与JavaScript交互?
通常,我们都是用WebBrowser加载包含JS代码的HTML,然后通过WebBrowser对象获取IHTMLDocument接口(对于ActiveX的WebBrowser来说是get_Document方法,对于CHtmlView对象来说是GetHtmlDocument方法)。其中IHTMLDocument2接口有一个get_Script方法,可以获取用于控制JS代码的IDispatch接口。VC调用JS函数,都是通过这个IDispatch接口的Invoke方法来完成。使用IDispatch接口的GetIDsOfNames方法根据JS函数名获取调度标识符DISPID,使用Invoke来调用JS函数。Invoke最后两个参数用于返回错误信息,可为NULL。这个IDispatch接口调用起来很麻烦,下文将会介绍如何简便的调用。网上有个外国人写了个CWebPage类实现VC与JS交互,用的正是这种方法。
http://www.codeproject.com/Articles/2352/JavaScript-call-from-C


关于WebBrowser:
在VC中使用WebBrowser,一般有两种方法。MFC中有个CHtmlView封装了WebBrowser,用起来很方便。虽然CHtmlView派生于CView,是个视图类,但它也派生于CWnd,将其用于对话框上,完全没有问题,只是在某些地方需要小修改一下。其中需要注意的两个问题就是:
1. CHtmlView的构造函数是protected的,不允许直接构造一个CHtmlView对象。必须从CHtmlView派生后再构造。
2.如果是在栈上创建CHtmlView对象,必须重载PostNcDestroy并什么也不写。因为默认的PostNcDestroy会delete
  this;而出错。如果是在堆上创建CHtmlView对象则要注意防止二次delete。
另一种方法是使用WebBrowser的ActiveX控件,这种方法可以在MFC项目中使用,也可以在非MFC项目中使用。


关于IDispatch:
我们知道IDispatch是COM双接口中的调度接口。一般用于供脚本语言调用COM组件。对于编译型的C++语言,让它调用这种接口,是很麻烦的。毕竟用IDispatch接口调用COM对象的各种方法、设置与获取COM对象的属性、让COM对象回调我们,都是用IDispatch的Invoke方法来实现。一个Invoke就要实现那么多功能,用起来当然很麻烦。不过好在ATL智能指针类中的CComDispatchDriver(即CComQIPtr<IDispatch>)封装了IDispatch接口,使用我们用起来大大的方便!CComDispatchDriver对GetIDsOfNames和Invoke进一步进行了封装,只需更少的参数即方便可调用。
获取和设置COM对象属性可以用CComDispatchDriver的这些方法:
GetProperty
GetPropertyByName
PutProperty
PutPropertyByName
其实使用IDispatch调度接口来设置、获取COM属性,调用COM方法,都是使用GetIDsOfNames和Invoke。实际上这四个方法都是对GetIDsOfNames和Invoke的封装。简化调用的复杂性。
调用COM对象的方法可以用这些方法:
Invoke0    //调用0个参数的方法
Invoke1    //调用1个参数的方法
Invoke2    //调用2个参数的方法
InvokeN    //调用多个参数的方法
这些函数都有两个版本,一个是接受调度标示符DISPID,需要自己先调用GetIDsOfNames来获取。一个是接受OLE字符串的版本,这个版本在内部会调用GetIDsOfNames来获取DISPID。这些函数用起来很方便,不需要我们自己填充DISPPARAMS结构,但是它对原始Invoke的调用时,最后两个参数都是NULL,即不需要获取错误信息。如果需要获取错误信息,我们需要自行调用原始Invoke方法。
注意,这些方法是ALT的CComDispatchDriver封装的方法,调用时应使用"."而不是"->"。因为"->"获得的是CComDispatchDriver内部的Dispatch指针。
另一个要注意的问题是,一定要等Navigate完全加载一个html文档后(触发OnDocumentComplete)。才能获取IHTMLDocument2和Script。否则会出现空指针或找不到JS函数。所以不能在调用Navigate打开HTML后就紧接着获取IHTMLDocument2和Script,要等HTML文档加载完。

上面说了这么多COM对象,和VC调用JS有什么关系?别忘了我们用IHTMLDocument2接口的get_Script方法获取到了代表HTML文档中JS代码的IDispatch接口,我们用IDispatch接口,把HTML文档中的这堆JS代码当作一个COM对象,来操控它。上面说的Invoke0,Invoke1,Invoke2,InvokeN,正是分别被我们用来调用0个参数的JS函数,1个参数的JS函数,2个参数的JS函数,N个参数的JS函数。

我们动手写一个HTML,其中包含这样一段JS代码:

<script type="text/javascript">    function Add(value1, value2) {        return value1 + value2;    }</script>然后我们用WebBrowser加载这个HTML后,在VC中这样来调用这个函数名为Add的JS函数:


//别忘了#include <MsHTML.h>//m_WebBrowser是一个WebBrowser的Activex控件对象。CComQIPtr<IHTMLDocument2> spDoc = m_WebBrowser.get_Document();CComDispatchDriver spScript;spDoc->get_Script(&spScript); CComVariant var1 = 10, var2 = 20, varRet;spScript.Invoke2(L"Add", &var1, &var2, &varRet);

spScript.Invoke2的作用是调用JS函数中名为Add的函数,传入两个参数,用varRet接收返回值。
可以看到,Invoke2调用成功后,varRet得到了返回值30。
 
但这样的话一次只能接受一个返回值。
如果要一次接受多个返回值的话,怎么办呢?
我们可以让JS返回一个JS中的Array数组或Object对象。
当JS函数return一个Array或一个Object对象时,VC这边的varRet将接受到一个代表该对象的IDispatch接口。我们仍然用CComDispatchDriver来管理这个IDispatch。用上一篇文章介绍的CComDispatchDriver的四个方法:
GetProperty
GetPropertyByName
PutProperty
PutPropertyByName
来从这个Array或Object对象中取出我们要的数据。
实践是检验真理的唯一标准,让我们再来写一个JS函数:

<script type="text/javascript">    function Add(value1, value2) {        var array = new Array();        array[0] = value1;        array[1] = value2;        array[2] = value1 + value2;        return array;    }</script>然后在VC中这样写:


CComQIPtr<IHTMLDocument2> spDoc = m_WebBrowser.get_Document();CComDispatchDriver spScript;spDoc->get_Script(&spScript); CComVariant var1 = 10, var2 = 20, varRet;spScript.Invoke2(L"Add", &var1, &var2, &varRet);    CComDispatchDriver spArray = varRet.pdispVal;//获取数组中元素个数,这个length在JS中是Array对象的属性,相信大家很熟悉CComVariant varArrayLen;spArray.GetPropertyByName(L"length", &varArrayLen);//获取数组中第0,1,2个元素的值:CComVariant varValue[3];spArray.GetPropertyByName(L"0", &varValue[0]);spArray.GetPropertyByName(L"1", &varValue[1]);spArray.GetPropertyByName(L"2", &varValue[2]);


可以看到,10,20,30,这三个JS函数返回的值已经躺在我们的varValue[3]里了。
当然,如果不知道JS返回的Array对象里面有几个元素,我们可以在VC这边获取它的length属性,然后在一个循环中取出数组中的每个值。
 
如果我们的JS函数返回一个包含有多个属性值的Object对象,VC这边该如何接收呢?
让我们再来写一个JS函数:

<script type="text/javascript">    function Add(value1, value2) {        var data = new Object();        data.result = value1 + value2;        data.str = "Hello,我是小明!";        return data;    }</script>然后在VC中我们这样接收:


CComQIPtr<IHTMLDocument2> spDoc = m_WebBrowser.get_Document();    CComDispatchDriver spScript;    spDoc->get_Script(&spScript);     CComVariant var1 = 10, var2 = 20, varRet;    spScript.Invoke2(L"Add", &var1, &var2, &varRet);        CComDispatchDriver spData = varRet.pdispVal;    CComVariant varValue1, varValue2;    spData.GetPropertyByName(L"result", &varValue1);    spData.GetPropertyByName(L"str", &varValue2);

我们从JS返回的Object对象里取出了它的两个属性,result和str,分别是一个整形数据和一个字符串。
这里JS代码是我们自己写的,在VC这边当然事先知道这个JS函数返回的对象有result和str这两个属性。
如果JS代码不是我们写的,或者它的属性是事先不能确定的,该怎么办呢?答案是使用IDispatchEx接口来枚举这个对象的相关信息(方法名、属性名)。这个现在暂时不讲,在后续的文章中会讲。

当然,JS不只可以返回Object对象,返回什么对象都可以,当返回一个对象而非基本数据类型(整形、浮点、字符串)时,VC这边收到的返回值是一个IDispatch,然后我们需要调用GetPropertyByName方法从这个IDispatch代表的对象中取出它的属性来。


JS中生成的对象和数组,传递给VC后,可以用CComDispatchDriver方便的读取,但如果要在VC这边生成一个对象或数组传递给JS该怎么办呢?

很多时候我们用VC调用JS函数的时候,需要传递很多参数给它,参数少还好办,给JS函数多写几个形参就行了,可是参数很多,多达数十个数百个怎么办?传递对象或数组才是好办法。JS中的对象和数组传到VC这边后就变成了一个IDispatch*,那么同理我们构造一个IDispatch*传给JS不就行了吗?说起来容易做起来麻烦,具体实现可以看下一章《VC与JavaScript交互(三)
 ———— JS调用C++》,但实在是太麻烦了。
用JSON就行了!VC想传递什么给JS,只需把要传递的数据放到JSON里,然后把JSON字符串扔给JS,接下来JS用JSON.stringify()就把这个JSON字符串变成JS中的数组或对象了。
如此一来,VC与JS交互只需传递字符串就行了,VC这边生成JSON的话手工拼接一下字符串也很简单,也可用一些库来实现,如RapidJSON、jsoncpp、Boost.PropertyTree等等。


这样一来,VC调用JS函数,传递参数给JS和JS返回返回值给VC,大致就都会了。
对于CComVariant包装的VARIANT这种智能型变量,不了解的可以到网上看下相关资料。《深入解析ATL》之类的书上均有介绍。
 
值得注意的是ATL提供的这些CCom开头的智能包装类,并不依赖于ATL的动态库。因为我在VC项目中并没有选择链接ATL,程序调试运行时进程加载的模块中也有没有ATL100.dll之类的模块载入。大家可以放心使用而不用担心依赖上ATL。
 
VC调用JS函数没问题了。那么JS函数如何调用VC呢?我们将在下一篇文章中慢慢道来

 

    时隔两年,VC与JavaScript交互系列的最后一篇关于JavaScript怎样调用c++的文章最终出炉了。

 

为什么会隔了那么久?由于本来打算太监的,但是看到热情的网友们的眼神,从期望变成了失望,在我的心里激起了层层波澜。

两年后的今天,还是坚持把它写了出来。事实上当时刚写完VC与JavaScript交互(二)的时候,參考网上的资料,已经把JavaScript调用c++实现了。但是实现方法太恶心了。代码写出来太复杂太麻烦了,并且还涉及到了一大堆见都没见过的COM接口,每一个接口都是一大堆函数和一大堆參数。尽管实现代码写出来了。但是为什么这么写。根本讲不清楚,怕误人子弟,便可耻的太监了。

 

    当初为了写自己主动打开网页,自己主动填单。自己主动提交的小程序,看了一下这方面的东西,因为当时仅仅涉及到了VC调用JavaScript,没有涉及到JavaScript调用VC。所以也没有花时间去深入了。

 

这两年期间,好几次想把 VC与JavaScript交互(三) 写出来,但是发现这个东西实在是太麻烦,太复杂。看不透。剪不断,理还乱。抽刀断水水更流。举杯消愁愁更愁。代码写出来以后我总是怀疑是不是搞错了。感觉是不是走了弯路,直到今天我仍然怀疑是不是有更好更简单的办法来实现JS调用C++。为什么说它很麻烦和复杂,能够看这里http://dgj0600.blog.163.com/blog/static/440604322012102325015495/

这是网上找到的一段JS调用C++的代码,密密麻麻的,根本不知道该怎么把它解释清楚。

 

    实际上关于VC与JavaScript交互,最熟悉它的人应该是开发Activex控件及IE的BHO插件的程序猿,他们一定能讲清楚当中的原理,讲清楚每个API和接口的使用方法。只是搞这些的人越来越少了。如今WEB上的Activex控件也是越来越少了,关于ATL的书都在10年前就绝版了,可想而知如今还有多少人研究这个东西。

 

吐槽WebBrowser:

 

    WebBrowser这个东西真是让人爱又让人恶心。刚開始使用认为挺简单的,导航、刷新、前进、后退、获取当中的HTML,都还比較易用。非常快就爱上了它。

 

但略微深入后便发现了这样的闭源软件的弊端,难以扩展和改造!

比方要用WebBrowser开发一个多进程浏览器,怎样在进程间共享Cookie。比方要针对不用的URL设置不同的HTTP代理来訪问。

比方要让它支持须要usernamepassword验证的HTTP/SOCKS5代理等。WebBrowser根本没有提供这样的接口来实现这些功能。仅仅能是通过API Hook等办法来实现。既麻烦又不稳定可靠。并且WebBrowser这个东西还很慢。本来IE就已经够慢了,WebBrowser作为IE的简化版,当它嵌入到我们的程序中时,WebBrowser中的HTML排版、渲染引擎、JavaScript解释器竟然都是执行在我们程序的主线程(UI线程)中!所以你能够发现,假设WebBrowser载入一个内容许多。很复杂的页面时。在载入期间,你的程序就像假死了一样。相同假设HTML页面上的JavaScript代码在进行繁杂的运算时。你的程序界面又假死了。

由于你的UI线程在执行JS解释器,你的UI线程在解释JavaScript代码并执行。在那期间它抽不出来空来去处理Windows消息循环。便假死了。

 

 

点赞CEF:

 

    在此强烈推荐CEF(Chromium Embedded Framework),即Chromium版的WebBrowser。

 

Chromium就不用说了。它的快是很出名的,即便作为控件来使用。CEF也运用了多进程技术,HTML的渲染和JavaScript的解释运行都是在格外的进程中,不会影响你的UI线程,奔溃了也不会破坏你的进程。并且CEF是用C++写的,对外提供的原生接口就是C++接口,比起WebBrowser的那套COM接口来说不知道好用多少倍。

 

 

JavaScript调用C++的一个相对简单的实现:

 

简述:

    上一章说到。一个JavaScript对象传到了C++这边以后。就变成了一个IDispatch*。然后我们用CComDispatchDriver接管这个IDispatch*后。就能够调用这个JavaScript对象的方法。获取这个JavaScript对象的属性,实际上CComDispatchDriver就是对IDispatch的包装。终于都是调用IDispatch::Invoke。同理,假设我们在C++这边构造出一个IDispatch*并传递给JavaScript,那么JavaScript就能够把这个IDispatch*当做一个JavaScript对象来使用,自然它就能够调用这个对象的方法,改动这个对象的属性。终于就能够实现调用C++函数。改动C++对象的成员变量,实际上JavaScript调用C++也是通过IDispatch::Invoke来调用。

 

那么怎样构造这个IDispatch就是问题的关键点。

 

实现:

    直接上代码,首先我建的是一个MFC对话框项目,WebBrowser已经拖上去了,加入为成员变量m_webbrowser。

 

然后改动MFC为我们生成的对话框类CxxDlg(我的项目名为JsCallCpp,所以我的演示样例代码中就是CJsCallCppDlg):

class CJsCallCppDlg : public CDialogEx, public IDispatch
{
...
}

    将其多重继承于IDispatch。啊!多重继承?怎么把这样的坑爹的东西搞出来?NO NO NO,不要谈多重继承就色变。这里的IDispatch里面的全部成员函数都是纯虚函数。本质上IDispatch就是个接口,C++的实现接口的方式就是多重继承,尽管不鼓舞用多重继承来继承实现代码,可是像这样用来实现接口是面向对象中很经常使用的。当然你也能够class MyIDispatch : public IDispatch。然后把MyIDispatch实例化成一个对象后传递给JavaScript来调用。这里之所以用CxxDlg来实现IDispatch,是为了方便,由于待会儿,我仅仅要把CxxDlg的this指针传递给JavaScript。它就能够调用我的CxxDlg从IDispatch处继承来的虚函数Invoke。也就是说JavaScript就能够直接调用CxxDlg::Invoke,然后在CxxDlg::Invoke中能够非常方便的调用我CxxDlg的其他成员函数。

    然后我写下了例如以下的HTML文件:

 

<html>
<head>
    <meta charset="utf-8" />
    <title></title>
    <script language="javascript">
        function ShowMessageBox()
        {
            if (cpp_object != null)
                cpp_object.ShowMessageBox("你好,我是Javascript,你是谁?");
        }
        function GetProcessID()
        {
            if (cpp_object != null)
            {
                var id = cpp_object.GetProcessID();
                document.getElementById("process_info").innerText = "本进程ID为:" + id;
            }
        }
        function SaveCppObject(obj)
        {
            cpp_object = obj;
        }
        var cpp_object;
    </script>
</head>
<body>
    <p id="process_info"></p>
    <button type="button" οnclick="ShowMessageBox()">MessageBox</button>
    <button type="button" οnclick="GetProcessID()">Process ID</button>
</body>
</html>

    然后我在我的CxxDlg里写下了例如以下的两个成员函数:

 

DWORD CJsCallCppDlg::GetProcessID()
{
    return GetCurrentProcessId();
}

void CJsCallCppDlg::ShowMessageBox(const wchar_t *msg)
{
    MessageBox(msg, L"这是来自javascript的消息");
}

    接来下。我要用HTML中的这两个button,分别调用这两个C++函数,当中一个是ShowMessageBox。让Javascript调用它并传递一个字符串给它,终于C++这边通过Windows API的MessageBox实现弹出一个消息框。

另外一个是GetProcessID,Javascript调用它,终于C++这边通过Windows API的GetCurrentProcessId()获取本进程ID,并给Javascript返回这个ID值。然后显示到HTML中。

    因为我的CxxDlg继承了IDispatch。那么我须要实现IDispatch中的七个纯虚函数。所以在CxxDlg类的声明中加入例如以下七个虚函数的声明:

virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount(UINT *pctinfo);

virtual HRESULT STDMETHODCALLTYPE GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo);

virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId);

virtual HRESULT STDMETHODCALLTYPE Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr);

virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject);

virtual ULONG STDMETHODCALLTYPE AddRef();

virtual ULONG STDMETHODCALLTYPE Release();

    然后实现这七个虚函数:

 

//我自己给我的两个函数拟定的数字ID。这个ID能够取0-16384之间的随意数
enum
{
    FUNCTION_ShowMessageBox = 1,
    FUNCTION_GetProcessID = 2,
};

//不用实现,直接返回E_NOTIMPL
HRESULT STDMETHODCALLTYPE CJsCallCppDlg::GetTypeInfoCount(UINT *pctinfo)
{
    return E_NOTIMPL;
}

//不用实现,直接返回E_NOTIMPL
HRESULT STDMETHODCALLTYPE CJsCallCppDlg::GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo)
{
    return E_NOTIMPL;
}

//JavaScript调用这个对象的方法时,会把方法名,放到rgszNames中,我们须要给这种方法名拟定一个唯一的数字ID。用rgDispId传回给它
//同理JavaScript存取这个对象的属性时。会把属性名放到rgszNames中,我们须要给这个属性名拟定一个唯一的数字ID,用rgDispId传回给它
//紧接着JavaScript会调用Invoke。并把这个ID作为參数传递进来
HRESULT STDMETHODCALLTYPE CJsCallCppDlg::GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId)
{
    //rgszNames是个字符串数组。cNames指明这个数组中有几个字符串。假设不是1个字符串。忽略它
    if (cNames != 1)
        return E_NOTIMPL;
    //假设字符串是ShowMessageBox。说明JavaScript在调用我这个对象的ShowMessageBox方法。我就把我拟定的ID通过rgDispId告诉它
    if (wcscmp(rgszNames[0], L"ShowMessageBox") == 0)
    {
        *rgDispId = FUNCTION_ShowMessageBox;
        return S_OK;
    }
    //同理,假设字符串是GetProcessID。说明JavaScript在调用我这个对象的GetProcessID方法
    else if (wcscmp(rgszNames[0], L"GetProcessID") == 0)
    {
        *rgDispId = FUNCTION_GetProcessID;
        return S_OK;
    }
    else
        return E_NOTIMPL;
}

//JavaScript通过GetIDsOfNames拿到我的对象的方法的ID后。会调用Invoke。dispIdMember就是刚才我告诉它的我自己拟定的ID
//wFlags指明JavaScript对我的对象干了什么事情!

 

//假设是DISPATCH_METHOD,说明JavaScript在调用这个对象的方法。比方cpp_object.ShowMessageBox(); //假设是DISPATCH_PROPERTYGET。说明JavaScript在获取这个对象的属性,比方var n = cpp_object.num; //假设是DISPATCH_PROPERTYPUT。说明JavaScript在改动这个对象的属性,比方cpp_object.num = 10; //假设是DISPATCH_PROPERTYPUTREF,说明JavaScript在通过引用改动这个对象,详细我也不懂 //演示样例代码并没有涉及到wFlags和对象属性的使用。须要的请自行研究,使用方法是一样的 //pDispParams就是JavaScript调用我的对象的方法时传递进来的參数,里面有一个数组保存着全部參数 //pDispParams->cArgs就是数组中有多少个參数 //pDispParams->rgvarg就是保存着參数的数组,请使用[]下标来訪问。每一个參数都是VARIANT类型,能够保存各种类型的值 //详细是什么类型用VARIANT::vt来推断,不多解释了。VARIANT这东西大家都懂 //pVarResult就是我们给JavaScript的返回值 //其他不用管 HRESULT STDMETHODCALLTYPE CJsCallCppDlg::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr) { //通过ID我就知道JavaScript想调用哪个方法 if (dispIdMember == FUNCTION_ShowMessageBox) { //检查是否仅仅有一个參数 if (pDispParams->cArgs != 1) return E_NOTIMPL; //检查这个參数是否是字符串类型 if (pDispParams->rgvarg[0].vt != VT_BSTR) return E_NOTIMPL; //放心调用 ShowMessageBox(pDispParams->rgvarg[0].bstrVal); return S_OK; } else if (dispIdMember == FUNCTION_GetProcessID) { DWORD id = GetProcessID(); *pVarResult = CComVariant(id); return S_OK; } else return E_NOTIMPL; } //JavaScript拿到我们传递给它的指针后,由于它不清楚我们的对象是什么东西,会调用QueryInterface来询问我们“你是什么鬼东西?” //它会通过riid来问我们是什么东西。仅仅有它问到我们是不是IID_IDispatch或我们是不是IID_IUnknown时,我们才干肯定的回答它S_OK //由于我们的对象继承于IDispatch。而IDispatch又继承于IUnknown,我们仅仅实现了这两个接口,所以仅仅能这样来回答它的询问 HRESULT STDMETHODCALLTYPE CJsCallCppDlg::QueryInterface(REFIID riid, void **ppvObject) { if (riid == IID_IDispatch || riid == IID_IUnknown) { //对的,我是一个IDispatch,把我自己(this)交给你 *ppvObject = static_cast<IDispatch*>(this); return S_OK; } else return E_NOINTERFACE; } //我们知道COM对象使用引用计数来管理对象生命周期,我们的CJsCallCppDlg对象的生命周期就是整个程序的生命周期 //我的这个对象不须要你JavaScript来管,我自己会管。所以我不用实现AddRef()和Release()。这里乱写一些。

//你要return 1;return 2;return 3;return 4;return 5;都能够 ULONG STDMETHODCALLTYPE CJsCallCppDlg::AddRef() { return 1; } //同上。不多说了 //题外话:当然假设你要new出一个c++对象来并扔给JavaScript来管,你就须要实现AddRef()和Release(),在引用计数归零时delete this; ULONG STDMETHODCALLTYPE CJsCallCppDlg::Release() { return 1; }

    该讲的都在代码凝视中讲了。简单来说。当JavaScript运行如cpp_object.GetProcessID();的代码时,会先调用GetIDsOfNames,并把"GetProcessID"这个字符串传递进来,我们给它分配一个自拟的ID,紧接着JavaScript会拿着这个ID来调用Invoke。至于參数和返回值怎样传递。代码和凝视写得非常清楚了。

 

    注意我的HTML中的JavaScript代码中,我用一个var cpp_object;全局变量来保存C++对象。然后我还写了一个SaveCppObject()函数给C++调用。在WebBrowser载入完成HTML文档后。须要先用C++调用JavaScript的这个SaveCppObject()函数,并把C++对象指针传递给JavaScript。这样JavaScript才干把它保存到var cpp_object;中,才干进行接下来的JavaScript调用C++。C++调用JavaScript的SaveCppObject()方法代码例如以下:

 

//调用JavaScript的SaveCppObject函数,把我自己(this)交给它。SaveCppObject会把我这个对象保存到全局变量var cpp_object;中
//以后JavaScript就能够通过cpp_object来调用我这个C++对象的方法了
void CJsCallCppDlg::OnBnClickedOk()
{
    CComQIPtr<IHTMLDocument2> document = m_webbrowser.get_Document();
    CComDispatchDriver script;
    document->get_Script(&script);
    CComVariant var(static_cast<IDispatch*>(this));
    script.Invoke1(L"SaveCppObject", &var);
}

    好了,至此。JavaScript调用C++已经完毕了。这样的方法,须要先把IDispatch*(演示样例代码中是this。但由于this是CJsCallCppDlg的实例。而CJsCallCppDlg多重继承于IDispatch,实际this就是IDispatch*了)传递给JavaScript。JavaScript把它保存好。然后调用它。网上另一种方法是。在C++这边再实现IDocHostUIHandler接口,然后通过一系列麻烦的操作,JavaScript那边就能够直接通过window.external来调用C++,而不用var cpp_object;了。

 

只是那个实现实在是太麻烦太恶心了,又会引入一大堆我解释不清楚的东西,所以还是作罢了,这样才是最简洁的实现。

 

    最后晒上一张执行效果图:

 

 

 

 

演示样例代码的整个VisualStudio项目文件能够到这里下载和查看(版本号VS2015):https://github.com/charlessimonyi/javascript_call_cpp

 

MSDN上关于IDispatch接口的文档:https://msdn.microsoft.com/en-us/library/windows/desktop/ms221608(v=vs.85).aspx

 

    对了。另一点,写好的HTML文件不仅能够直接和EXE放在一个文件夹下使用。也能够在VisualStudio中把HTML文件作为资源加入到项目中,这样终于写出来的程序仅仅有一个EXE。HTML文件已经在EXE里面了,至于怎样让WebBrowser载入这个HTML文件。能够在CxxDlg::OnInitDialog()中使用例如以下代码:

 

 

//载入资源文件里的HTML,IDR_HTML1就是HTML文件在资源文件里的ID
wchar_t self_path[MAX_PATH] = { 0 };
GetModuleFileName(NULL, self_path, MAX_PATH);
CString res_url;
res_url.Format(L"res://%s/%d", self_path, IDR_HTML1);
m_webbrowser.Navigate(res_url, NULL, NULL, NULL, NULL);

 

 

常见问题:

①调用m_webbrowser.Navigate()载入一个HTML文档后,不要紧接着就:

CComQIPtr<IHTMLDocument2> document = m_webbrowser.get_Document();
CComDispatchDriver script;
document->get_Script(&script);

这样获取其接口指针进行C++调用Javascript操作,这样往往会取到空指针,由于m_webbrowser.Navigate()调用完成,并不意味着HTML文档已经载入、渲染完成,m_webbrowser.Navigate()实际上是一个异步操作,调用以后仅仅是发出了一个命令,让WebBrowser去载入这个HTML文档。至于何时载入完成,能够处理WebBrowser的DocumentComplete事件来获知,仅仅有在触发DocumentComplete事件后,才干够获取其接口指针进行操作。所以在上面的演示样例中,假设想让HTML文档载入完成后就自己主动用C++调用Javascript的SaveCppObject()函数,把C++对象传递过去,仅仅需把上面演示样例程序中我写在button响应函数中的代码写到DocumentComplete事件的响应函数中就可以(Github上的演示样例代码已经更新成这样了)。

 

 

怎么加入DocumentComplete事件响应函数?看下图,先选中WebBrowser控件,再到属性对话框里找想处理的事件,全部的Activex控件的事件响应函数都能够在这里加入。

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页