使用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; }
參考文檔: