使用IE内核开发客户端产品,系统和前端页面之间的交互,一般给开发和维护带来很大的便利性。但操做系统和前端之间的交互倒是比较复杂的。具体来讲就是脚本语言和编译语言的交互。在IE内核中html和css虽然不兼容,可是IE编程接口是彻底同样的,这得益于微软的COM组件的结构化设计和实现。因此与IE交互,必须得先说一下COM,COM全称组件对象模型(Component Object Model)。javascript
COM的基本思想很简单,全部的组件模块都提供一个最根本的接口, IUnkown,它有三个方法,AddRef和Release实现了引用计数,QueryInterface实现了根据接口id查询另外的接口,全部的接口都从IUnkown派生。基于IE内核作开发,有一个接口是最关键的,IDispatch(欲知详情移步IDispatch接口 - GetIDsOfNames和Invoke)。 IDispatch接口是COM自动化的核心。其实,IDispatch这个接口自己也很简单,只有4个方法:最关键的两个方法Invoke和 GetIDsOfNames。脚本语言和编译型语言之间进行通讯是经过IDispatch接口来行的。下面看一下这个关键的方法的原型:css
IDispatch:public IUnkown { //... HRESULT Invoke( DISPID dispIdMember,REFIID riid, LCID lcid,WORD wFlags,DISPPARAMS FAR* pDispParams,VARIANT FAR* pVarResult, EXCEPINFO FAR* pExcepInfo, unsigned int FAR* puArgErr ); //... }
这个方法每一个参数的意义msdn上有详细的阐述,就个人理解而看,它做为一个组件, 向外提供了一个万能接口,据此能够实现两个颇有用的功能:html
1. 获取和设置组件的属性变量. 对应wFlags的DISPATCH_PROPERTYGET和DISPATCH_PROPERTYPUT前端
2.以任意参数调用任意一个被支持的方法. 对应wFlags的DISPATCH_METHODjava
用面向对象的观点来看,有了这两个功能,任意一个实现该接口的组件就抽象成同一个接口实现的万能对象,能够经过指定的字符串名字获取设置属性和调用方 法。若是有过脚本编程经验开发人员必定会发现,脚本偏偏就是如此。c++书写代码的时候也是经过名字访问对象,但编译好后变成二进制代码后就没有名字的概念了,只有偏移和地址。而脚本里头书写代码的时候是经过名字,解释执行的时候也是经过名字。脚本和native语言的最大区别是脚本对象的全部属性和方法 是动态的,在执行的时候还能够修改。看到这里,很容易联想到实现了IDispatch的组件对象具备了脚本的特性,c++对象被脚本化了!这就意味着你可 以把原来用 c++写的类的全部属性和方法都经过Invoke来执行,在脚本里头能够直接访问!至关于给脚本增长了native的扩展。IDispatch接口很重要 的一个功能就是如此,微软一般所说的双接口就是这个意思。了解了这些,接下来要和JS脚本交互就比较容易了。c++
既然是IE内核里的JS与C++互相调用,咱们先来简单的了解下IE内核编程须要的几个经常使用接口。说多了很差理解,先来看图。程序员
IWebBrowser2, IHTMLWindow2,IHTMLDocument2 这三个经常使用接口都是从IDispatch 派生的。IWebBrowser2接口里主要提供浏览器常规功能如打开URL、前进、后退等功能。IHTMLWindow2主要是提供接口操做浏览器中打 开的window对象,IHTMLDocument2获取文档相关信息,以及审查和修改HTML元素和文档中文本,包括获取JS对象。web
IHTMLWindow2 对应于一个window的视图,IHTMLDocument2是IHTMLWindow2渲染文档,对应着dom树结构。在js中有两个全局对象 window和document,分别对应着IHTMlWindow2和 IHTMLDocument2。编程
想要详细了解这些,参看以下资料:windows
IWebBrowser2 interface https://msdn.microsoft.com/en-us/library/aa752127%28VS.85%29.aspx
IHTMLWindow2 interface https://msdn.microsoft.com/zh-cn/library/aa741505
IHTMLDocument2 interface https://msdn.microsoft.com/zh-cn/library/aa752574
要完成c++和js交互,能够分解成两个任务,一是c++调用js代码;二是js调用c++代码,这其实也全部脚本和natvie交互的两个基本任务。本文主要根据本身的理解从设计开发的角度去阐述为何要这么作。
每段js执行代码都有它本身的执行环境,在IE里面能够看作是IHTMLWindow2。
根 据上边所讲,咱们用先获取全局对象document,从document变成的IDispatch接口中获得 IHTMLDocument2,IHTMLDocument2接口的get_Script方法获取到了HTML文档中JS代码的IDispatch接口, 咱们用IDispatch接口,把HTML文档中的JS代码看成一个COM对象,对他进行操做。
CComPtr<IDispatch> GetScript() { CComPtr<IWebBrowser2> spWebBrowser; HRESULT hResult = QueryControl(IID_IWebBrowser2, (void**)&spWebBrowser); if (SUCCEEDED(hResult)) { CComPtr<IDispatch> spDocDisp; hResult = spWebBrowser->get_Document(&spDocDisp); if (SUCCEEDED(hResult)) { CComPtr<IHTMLDocument2> spDocDisp2; hResult = spDocDisp->QueryInterface(IID_IHTMLDocument2, (void**)&spDocDisp2); if (SUCCEEDED(hResult)) { CComPtr<IDispatch> spScript; hResult = spDocDisp2->get_Script(&spScript); if (SUCCEEDED(hResult)) { return spScript; } } } } }
有两种方案能够执行JS,一种是直接调用IHTMLWindow2的execScript方法.
HRESULT execScript( BSTR code, BSTR language, VARIANT *pvarRet);
代码示例:
wstring strJavaScript; CComVariant pvarRet; CComBSTR bstrJavaScript(strJavaScript.c_str()); CComBSTR bstrScriptType(_T("javascript")); CComQIPtr<IHTMLWindow2> spWindow2(spScriptDisp); spWindow2->execScript(bstrJavaScript, bstrScriptType, &pvarRet);
要看懂这段代码不难,咱们先来了解下CComQIPtr,用IDispatch接口调用COM对象的各类方法、设置与获取COM对象的属性、让COM对象 回调咱们,都是用IDispatch的Invoke方法来实现。一个Invoke就要实现那么多功能,用起来固然很麻烦。不过好在ATL智能指针类中的 CComDispatchDriver(即CComQIPtr<IDispatch>)封装了IDispatch接口,使用起来很是方便!先拿到IHTMLWindow2接口的智能指针,直接把js代码环境IDispatch指针的赋值给它。不过注意这里是BSTR的字符串,能够用 SysAllocString来分配。
第二种方案一样是使用IHTMLDocument的get_Script()方法。它能获得一个IDispatch指针,这个IDispatch就是IHTMLDocument里的JS。按照前面介绍的IDispatch的使用,你经过它就能够调用任意js函数了。例如要执行一个 js中的函数 function。
CComPtr<IHTMLDocument2> spDocDisp2; spDocDisp2->get_Script(&spScript); OLECHAR * Names= L"function" ; DISPID dispID=0; //先获取接受调度标示符DISPID,须要调用GetIDsOfNames来获取 spScript->GetIDsOfNames(IID_NULL,&Names,1,LOCALE_SYSTEM_DEFAULT, &dispID); //经过Invoke(援引)方法调用JS方法 spScript->Invoke(dispID,,IID_NULL ,LOCALE_SYSTEM_DEFAULT,DISPATCH_METHOD,NULL,NULL,NULL,NULL);
这里function是js里面的一个全局函数。这里能够看到 Invoke并无直接把字符串名字拿过来用,而是经过另外一个方法GetDispofNames作了一个映射,获取接受调度标示符DISPID。经过 IHTMLDocument获得的script接口对应着该页面的JS全局环境,从中能够经过屡次invoke获得任意一个全局变量,函数,从而可以获得对象的成员变量或成员方法。
第二种方案就是经过Invoke调用来实如今c++中存取js变量和调用函数。这和第一种方案的区别很明显,一个是在用c++写js代码,有点相似本身在解析执行js了,而前者更简单,再复杂的js调用序列,一个字符串所有搞定。
要作到c++和脚本交互有一个基本的问题要作好,就是脚本中的数据类型和c++中的数据类型如何对应起来。众所周知,js中有不少类型,Boolean, Number, String, Object, Array , Function等。写到这里,插一句,基本全部的语言里头都有字符串和数字这两种基本的数据类型(c/c++中仅为以\0结尾的字符数组),面向对象的 语言中还会有Object这样的复合数据类型。在Invoke调用参数中, VARAINT就表明了c中的基本数据类型,js中的数字会转换成VT_I4或者VT_R4或VT_R8。字符串会转换成VT_BSTR类型的 bstr(这是微软com标准里使用的字符串类型),其余全部的复合类型包括对象数组函数在c中都对应着VT_DISPATCh的一个IDispatch 指针。有了IDispatch指针,你就能够按照前面的方法任意存取对象的属性,也能够发起函数调用并得到返回值。了解了这些,就能够进行c与js的交互 了,它们都经过IDispatch的invoke调用来完成。CComDispatchDriver对GetIDsOfNames和Invoke进一步进 行了封装,只需更少的参数即方即可调用。
Invoke0 //调用0个参数的方法
Invoke1 //调用1个参数的方法
Invoke2 //调用2个参数的方法
InvokeN //调用多个参数的方法
说了这么多,估计有些人看得云里雾里的。下边直接给出例子:
咱们动手写一个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,World!"; 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接口来枚举这个对象的相关信息(方法名、属性名)。
C++调用JS的实例演示到此为止。
按照前面所说的IDispatch的用途,就能够推断出如何作到这一点了,自定义一个c++类,实现一个IDispatch的接口,把它的指针经过某次 js调用做为返回值返回给js,那么js代码中就持有该对象了,就能够像使用普通js对象同样的使用它。问题是,一开始js啥都没有,怎么直接调到c++ 里头从而返回c++对象呢?IE已经考虑好了这个问题,它对于每一个IWebbrowser2实例(顶层)有一个内置的IDispatch对象,该对象能够 在建立浏览器控件实例以后在c++中本身制定,而在js中则使用window.external来访问。也就是说每一个js环境都已经内置了一个全局对象 external,而且它对应的c++中的IDispatch能够由程序员本身指定。下面谈一下如何来设置这个对象实例。
在windows中要本身host一个active控件,若是用sdk本身写。其中有一个接口叫IDocHostUIHandler ,它有一个方法GetExternalDisp用以向宿主查询一个IDispatch对象,就直接对应着js中的external脚本对象。 IDocHostUIHandler 还有一个有用的方法ShowContextMenu,当要show菜单的时候这个方法会被回调,应用程序就能够自定义菜单了。MFC也能够很方便的 host一个IE控件,但它的类库太庞大了,幸好微软又出了ATL,提供了一个轻量级的方法让你能够达到一样的效果。下面直接贴代码片断.
class CWebBrowser : public CAxHostWindow { private: CComPtr<IWebBrowser2> m_pWebBrowser; //保存建立出来的浏览器控件实例 BEGIN_MSG_MAP(CWebBrowser) MESSAGE_HANDLER(WM_CREATE,OnCreate) CHAIN_MSG_MAP(CAxHostWindow) END_MSG_MAP() LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/) { // Create WebBrowser object LPOLESTR pName=NULL; StringFromCLSID(CLSID_WebBrowser,&pName); CComPtr<IDispatch>disp; CComPtr<IUnknown> p; _InternalQueryInterface(IID_IDispatch,(void**)&disp); // 建立 WebBrowser CreateControlEx(pName,m_hWnd,NULL,&p,DIID_DWebBrowserEvents2,disp); CoTaskMemFree(pName); // 查询IWebBrowser2 接口,用于控制 HRESULT hRet = QueryControl(IID_IWebBrowser2, (void**)&this->m_pWebBrowser); return m_pWebBrowser?S_OK:-1; } }
CWebBrowser 是用户本身的宿主窗口,在它的OnCreate里头建立com对象,一个浏览器窗口就出来了,这个代码是否是很简洁?CAxHostWindow为咱们作 了不少事情,包括IDocHostUIHandler也被实现,因此咱们从它派生就自然的拥有了不少控制IE控件的能力,固然都是经过com接口来完成 的。之后若是有定制需求,大可重写父类的虚函数来达到目的。CAxHostWindow还封装了一个方法SetExternalDispatch,到这里 一切均可以暂时告一段落了,你能够在CWebBrowser中实现IDispatch也能够单独用一个类来实现,而后把IDispatch接口设进去就可 以了。有兴趣研究这个寄宿控件过程的童鞋们能够看CAxHostWindow的代码实现,全在一个头文件中。
假设你的external提供了一个函数建立对象 function newMyObject,在js中
var newObject=window.external.newMyObject(); //经过external构建一个c++对象交给js持有
alert(newObject.name); //访问该对象的属性
alert(newObject.GetValue()) //调用该对象的方法
那么你须要作的事情其实仍是关注Invoke就能够了.在external的IDispatch的Invoke实现中
STDMETHODIMP CWebBrowserDisp::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult, EXCEPINFO* pExcepInfo, unsigned int* puArgErr) { HRESULT nRet = S_OK; if(wFlags&DISPATCH_METHOD) //属于方法调用 { //给newMyObject分配的id,字符串名字映射 if(dispIdmember== DISPID_newMyObject) { IDispatch* pMyObject=NULL; //建立c++对象并获取其IDispatch接口 CreateMyObject(&pMyObject); pVarResult->vt=VT_DISPATCH; pVarResult->pdispVal=pMyObject; //做为返回值传递给js } } return 0; }
这个代码也很简洁。据此能够看出,要把c++对象导出到js中,那么该对象必需要实现IDispatch接口,只须要把这个接口做为Invoke的返回值 传给js便可。它有引用计数,没必要担忧内存的释放问题,在js的垃圾回收被触发的某个时刻天然会被销毁。接下来,MyObject有哪些属性和方法能够被 js调用,那就又归它本身的IDispatch的Invoke实现来关心了。
另一种就是在webbrowser控件中,JS调用C++方法。若是你对webbrowser控件熟悉的话,这里使用起来就更简单了。Invoke接口实现基本上和上边的相似,惟一不一样的是如何让JS调用到本地的C++ 代码。在JS代码中建立了函数window.external.newMyObject()。页面渲染时,会触发浏览器的GETEXTERNAL事件,在浏览器中,经过消息过滤,当消息为WN_GETEXTERNAL时,经过IDispatch接口,获取JS代码须要调用的类。
IDispatch **ppDispatch = (IDispatch**)wParam;
*ppDispatch = &m_superCall;
综上所述,在IE中和c++与js交互,IDispatch扮演了很重要的角色,理解好了它你就能够为所欲为的c++和js的混合编程了。COM接口很不容易理 解,知道怎么用,却难以了解其内部机制。其实,在前面所讲的过程当中,IDispatch是本身的代码建立的,和系统彻底无关。从c++的语法看,它就是继 承了一个虚基类,实现其所有方法而已,还有就是引用计数。因此,咱们彻底能够用很简单的c++代码来写本身的IDispatch,没必要去理会那么多的COM特性。js执行环境老是在主线程,因此你要知道一点你的对象的方法也老是在主线程被调用。下边给出简单的实现代码:
#include "StdAfx.h" #include "SQSuperCall.h" CJSCallC::CJSCallC(void) { m_mapFunction[TEXT("FuncTest")] = DISPID_FuncTest; } CJSCallC::~CJSCallC(void) { } HRESULT STDMETHODCALLTYPE CJSCallC::GetIDsOfNames( /* [in] */ __RPC__in REFIID riid, /* [size_is][in] */ __RPC__in_ecount_full(cNames) LPOLESTR *rgszNames, /* [range][in] */ UINT cNames, /* [in] */ LCID lcid, /* [size_is][out] */ __RPC__out_ecount_full(cNames) DISPID *rgDispId) { HRESULT hr = NOERROR; for (UINT nIndex = 0; nIndex < cNames; ++nIndex) { wstring strFuntion = rgszNames[nIndex]; map<wstring, int>::iterator iter = m_mapFunction.find(strFuntion); if (m_mapFunction.end() != iter) { rgDispId[nIndex] = iter->second; } else { hr = ResultFromScode(DISP_E_UNKNOWNNAME); rgDispId[nIndex] = DISPID_UNKNOWN; } } return hr; } /* [local] */ HRESULT STDMETHODCALLTYPE CJSCallC::Invoke( /* [in] */ DISPID dispIdMember, /* [in] */ REFIID riid, /* [in] */ LCID lcid, /* [in] */ WORD wFlags, /* [out][in] */ DISPPARAMS *pDispParams, /* [out] */ VARIANT *pVarResult, /* [out] */ EXCEPINFO *pExcepInfo, /* [out] */ UINT *puArgErr) { if (dispIdMember == DISPID_FuncTest) { int paramsCount = pDispParams->cArgs; if (paramsCount < 2) return S_FALSE; VARIANTARG* cmdVar = (VARIANTARG*)(&pDispParams->rgvarg[paramsCount - 1]); if (!(cmdVar->vt == VT_I4 || cmdVar->vt == VT_BSTR)) return S_FALSE; int nCmdId = cmdVar->intVal; cmdVar = (VARIANTARG*)(&pDispParams->rgvarg[paramsCount - 2]); if( cmdVar->vt != VT_BSTR ) return S_FALSE; CString csInfos = cmdVar->bstrVal; wstring strInfos(csInfos); } return S_OK; } HRESULT STDMETHODCALLTYPE CJSCallC::QueryInterface( /* [in] */ REFIID riid, /* [iid_is][out] */ __RPC__deref_out void **ppvObject) { //*ppvObject = NULL; if (riid == IID_IUnknown) { *ppvObject = static_cast<IUnknown*>(this); } else if (riid == IID_IDispatch) { *ppvObject = static_cast<IDispatch*>(this); } else { return E_NOINTERFACE; } return S_OK; }
参考文档: