WebBrowser控件是Microsoft提供的一個用於網頁瀏覽的客戶端控件,WebBrowser控件的使用相當廣泛,例如很多郵件客戶端都是使用可編輯的WebBrowser控件作為寫郵件的工具,也有很多軟件用WebBrowser控件彈出網頁,如qq的新聞首頁。
微軟的MFC和.NET都有WebBrowser控件,這兩個控件雖然容易上手,不過由於包裝的太好,所以很難深入。因此本文介紹的WebBrowser將不使用MFC和.NET,而是使用C++實現SDK的WebBrowser。
由於本文主要探討如何實現Javascript與C++的互操作,對於如何使用SDK實現WebBrowser,本文不做詳細介紹,讀者可以參考以下這篇文章:
http://blog.csdn.net/norsd/archive/2008/09/13/2921389.aspx
不過盡管文章中介紹了SDK實現WebBrowser的要點,卻沒有提供一個可以運行的示例,如果要看到實際的運行效果,可以下載以下這份源代碼,源代碼中也包括了互操作的演示:
1、C++調用WebBrowser中的全局函數,變量等
(1) 從C++的角度看WebBrowser中的對象
WebBrowser中的對象大致可以分成兩類:DOM對象和使用Javascript創建的對象。但是無論是那種對象,從C++的角度來看,都是一些實現了IDispatch接口的對象,因此,如果用C++操作WebBrowser中的對象(全局函數,變量,DOM)等,只需要通過IDispatch即可。
(2) 3個常用的函數
當獲取了WebBrowser的對象的IDispatch接口后,就可以調用IDispatch的Invoke方法來調用對象的方法,獲取對象的屬性和設置對象的屬性。但是Invoke是通過ID判斷要調用指定對象的哪一個方法(或屬性),因此在通過方法(或屬性)名稱調用對象的方法是,必須先調用IDispatch的GetIDsOfNames方法,將方法(或屬性)名轉換成ID,然后才能通過IDispatch的Invoke方法調用對象的方法。為了方便操作,封裝了三個函數,分別用於調用WebBrowser的對象的方法,讀取對象的屬性,設置對象的屬性。
DISPID CWebBrowserBase::FindId(IDispatch *pObj, LPOLESTR pName) { DISPID id = 0; if(FAILED(pObj->GetIDsOfNames(IID_NULL,&pName,1,LOCALE_SYSTEM_DEFAULT,&id))) id = -1; return id; } HRESULT CWebBrowserBase::InvokeMethod(IDispatch *pObj, LPOLESTR pName, VARIANT *pVarResult, VARIANT *p, int cArgs) { DISPID dispid = FindId(pObj, pName); if(dispid == -1) return E_FAIL; DISPPARAMS ps; ps.cArgs = cArgs; ps.rgvarg = p; ps.cNamedArgs = 0; ps.rgdispidNamedArgs = NULL; return pObj->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_METHOD, &ps, pVarResult, NULL, NULL); } HRESULT CWebBrowserBase::GetProperty(IDispatch *pObj, LPOLESTR pName, VARIANT *pValue) { DISPID dispid = FindId(pObj, pName); if(dispid == -1) return E_FAIL; DISPPARAMS ps; ps.cArgs = 0; ps.rgvarg = NULL; ps.cNamedArgs = 0; ps.rgdispidNamedArgs = NULL; return pObj->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYGET, &ps, pValue, NULL, NULL); } HRESULT CWebBrowserBase::SetProperty(IDispatch *pObj, LPOLESTR pName, VARIANT *pValue) { DISPID dispid = FindId(pObj, pName); if(dispid == -1) return E_FAIL; DISPPARAMS ps; ps.cArgs = 1; ps.rgvarg = pValue; ps.cNamedArgs = 0; ps.rgdispidNamedArgs = NULL; return pObj->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, DISPATCH_PROPERTYPUT, &ps, NULL, NULL, NULL); }
(3)調用頁面的全局函數
在網頁中,所有的全局函數均是window的一個方法,因此,如果要調用全局函數,首先要獲取到頁面的window對象,然后用InvokeMethod調用全局函數,例如,假設頁面中有一個Test全局函數:
<script language="javascript" type="text/javascript"> function Test() { alert("你調用了Test"); } </script>
那么,您可以在C++中用以下代碼調用Test函數:
VARIANT params[10]; VARIANT ret; //獲取頁面window IDispatch *pHtmlWindow = pBrowser->GetHtmlWindow(); //頁面全局函數Test實際上是window的Test方法, CWebBrowserBase::InvokeMethod(pHtmlWindow, L"Test", &ret, params, 0);
(4)調用全局對象的方法
在網頁中,所有的全局變量均是window的一個屬性,因此,如果要調用變量的方法(或屬性),首先要獲取到頁面的window對象,然后用GetProperty獲取到全局變量,然后就可以調用這個對象的方法,或讀寫其屬性。例如,假設頁面中有一個globalObject全局變量:
<script language="javascript" type="text/javascript"> function GlobalObject() { this.Test=function() { alert("你調用了GlobalObject.Test"); } } var globalObject = new GlobalObject(); </script>
那么,您可以使用一下代碼調用globalObject的Test方法:
VARIANT params[10]; VARIANT ret; //獲取頁面window IDispatch *pHtmlWindow = pBrowser->GetHtmlWindow(); //獲取globalObject CVariant globalObject; params[0].vt = VT_BSTR; params[0].bstrVal = L"globalObject"; CWebBrowserBase::GetProperty(pHtmlWindow, L"globalObject", &globalObject); //調用globalObject.Test CWebBrowserBase::InvokeMethod(globalObject.pdispVal, L"Test", &ret, params, 0);
2、在網頁中調用客戶端的方法
上文我們已經介紹了如何在C++中調用WebBrowser中的對象,接下來,將介紹如何在頁面中調用客戶端中的函數和對象:
(1) 通過window.external調用
下面將示例如何通過window.external調用客戶端中的函數,假設在C++中定義了一個名為CppCall的函數:
void CppCall() { MessageBox(NULL, L"您調用了CppCall", L"提示(C++)", 0); }
定義一個對象,並且實現IDispatch接口:
class ClientCall:public IDispatch { long _refNum; public: ClientCall() { _refNum = 1; } ~ClientCall(void) { } public: // IUnknown Methods STDMETHODIMP QueryInterface(REFIID iid,void**ppvObject) { *ppvObject = NULL; if (iid == IID_IUnknown) *ppvObject = this; else if (iid == IID_IDispatch) *ppvObject = (IDispatch*)this; if(*ppvObject) { AddRef(); return S_OK; } return E_NOINTERFACE; } STDMETHODIMP_(ULONG) AddRef() { return ::InterlockedIncrement(&_refNum); } STDMETHODIMP_(ULONG) Release() { ::InterlockedDecrement(&_refNum); if(_refNum == 0) { delete this; } return _refNum; } // IDispatch Methods HRESULT _stdcall GetTypeInfoCount( unsigned int * pctinfo) { return E_NOTIMPL; } HRESULT _stdcall GetTypeInfo( unsigned int iTInfo, LCID lcid, ITypeInfo FAR* FAR* ppTInfo) { return E_NOTIMPL; } HRESULT _stdcall GetIDsOfNames( REFIID riid, OLECHAR FAR* FAR* rgszNames, unsigned int cNames, LCID lcid, DISPID FAR* rgDispId ) { if(lstrcmp(rgszNames[0], L"CppCall")==0) { //網頁調用window.external.CppCall時,會調用這個方法獲取CppCall的ID *rgDispId = 100; } return S_OK; } HRESULT _stdcall Invoke( DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult, EXCEPINFO* pExcepInfo, unsigned int* puArgErr ) { if(dispIdMember == 100) { //網頁調用CppCall時,或根據獲取到的ID調用Invoke方法 CppCall(); } return S_OK; } };
定義類ClientCall后,就可以創建一個ClientCall的對象,傳遞給WebBrowser,使得網頁中可以通過window.external調用CppCall,要實現這些功能,WebBrowser需要實現IDocHostUIHandler接口,並重寫GetExternal方法以返回一個ClientCall對象:
ClientCall *pClientCall; pClientCall = new ClientCall(); virtual HRESULT STDMETHODCALLTYPE GetExternal(IDispatch **ppDispatch) { //重寫GetExternal返回一個ClientCall對象 *ppDispatch = pClientCall; return S_OK; }
接下來,就可以在網頁中調用了:
window.external.CppCall()
(2)向網頁傳遞回調函數
向網頁傳遞回調函數的一個典型應用就是在客戶端中用C++處理DOM的事件(例如,處理按鈕的onclick事件),這里要注意的是,與C++不同的是,在網頁中,所謂的函數,其實就是一個具有call方法的對象,因此,向網頁傳遞一個回調函數,其實就是傳遞一個實現了call方法的對象,因此,我們必須定義一個C++類,並實現IDispatch接口:
typedef void _stdcall JsFunction_Callback(LPVOID pParam); class JsFunction:public IDispatch { long _refNum; JsFunction_Callback *m_pCallback; LPVOID m_pParam; public: JsFunction(JsFunction_Callback *pCallback, LPVOID pParam) { _refNum = 1; m_pCallback = pCallback; m_pParam = pParam; } ~JsFunction(void) { } public: // IUnknown Methods STDMETHODIMP QueryInterface(REFIID iid,void**ppvObject) { *ppvObject = NULL; if (iid == IID_IOleClientSite) *ppvObject = (IOleClientSite*)this; else if (iid == IID_IUnknown) *ppvObject = this; if(*ppvObject) { AddRef(); return S_OK; } return E_NOINTERFACE; } STDMETHODIMP_(ULONG) AddRef() { return ::InterlockedIncrement(&_refNum); } STDMETHODIMP_(ULONG) Release() { ::InterlockedDecrement(&_refNum); if(_refNum == 0) { delete this; } return _refNum; } // IDispatch Methods HRESULT _stdcall GetTypeInfoCount( unsigned int * pctinfo) { return E_NOTIMPL; } HRESULT _stdcall GetTypeInfo( unsigned int iTInfo, LCID lcid, ITypeInfo FAR* FAR* ppTInfo) { return E_NOTIMPL; } HRESULT _stdcall GetIDsOfNames( REFIID riid, OLECHAR FAR* FAR* rgszNames, unsigned int cNames, LCID lcid, DISPID FAR* rgDispId ) { //令人費解的是,網頁調用函數的call方法時,沒有調用GetIDsOfNames獲取call的ID,而是直接調用Invoke return E_NOTIMPL; } HRESULT _stdcall Invoke( DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult, EXCEPINFO* pExcepInfo, unsigned int* puArgErr ) { m_pCallback(m_pParam); return S_OK; } };
接下來,我們就可以使用JsFunction向網頁傳遞回調,以下代碼用於處理按鈕的onclick事件:
static void _stdcall button1_onclick(LPVOID pParam) { MainForm *pMainForm = (MainForm*)pParam; MessageBox(pMainForm->hWnd, L"您點擊了button1", L"提示(C++)", 0); } VARIANT params[10]; //獲取window IDispatch *pHtmlWindow = GetHtmlWindow(); //獲取document CVariant document; params[0].vt = VT_BSTR; params[0].bstrVal = L"document"; GetProperty(pHtmlWindow, L"document", &document); //獲取button1 CVariant button1; params[0].vt = VT_BSTR; params[0].bstrVal = L"button1"; InvokeMethod(document.pdispVal, L"getElementById", &button1, params, 1); //處理button1的onclick事件 params[0].vt = VT_DISPATCH; params[0].pdispVal = new JsFunction(button1_onclick, this); SetProperty(button1.pdispVal, L"onclick", params);