Javascript與C++之間的通訊 —— 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
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);
在網頁中,所有的全域性變數均是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);