1. 程式人生 > >22.WebBrowser中JS和C++程式碼互相呼叫

22.WebBrowser中JS和C++程式碼互相呼叫

利用WebBrowser控制元件我們可以利用各種Web介面庫做出高大上的介面和炫酷的動畫,擴充套件性也好,甚至可以實現介面實時升級。但是有一點問題,在WebBrowser內嵌的網頁中如何訪問本地計算機硬體呢?實時上,WebBrowser內嵌的網頁中JS與本地C++程式碼可以相互呼叫,這樣就可以最大程度利用C++強大的計算能力和與本地硬體通訊。

在正式講解前,需要指出的是為什麼兩者可以相互呼叫,其核心是IE控制元件是基於COM 的自動化IDispatch介面,這樣只需要向IE控制元件傳送命令即可完成C++呼叫JS,反之JS方法執行時,IE控制元件的實現可以呼叫外部指定的一個IDispatch介面,從而呼叫C++程式碼,記住這一點即可理解本文的所有內容。

1.C++呼叫JS

C++呼叫JS就是取到IE控制元件的對應介面,呼叫即可。

a)第一種方法

利用瀏覽器DOM中的Window視窗執行指定的js串,方法封裝如下:

void CMainDlg::ExecScript1(LPCWSTR pszJs)
{
	if (!m_spWebBrowser)
	{
		return;
	}

	CComPtr<IDispatch> spDispDoc;
	m_spWebBrowser->get_Document(&spDispDoc);

	if(!spDispDoc) 
	{
		return;
	}

	CComPtr<IHTMLDocument2> spHtmlDoc;
	CComPtr<IHTMLWindow2> spHtmlWindow;
	HRESULT hr = spDispDoc->QueryInterface(IID_IHTMLDocument2,(void**)&spHtmlDoc);
	if (spHtmlDoc)
	{
		if (SUCCEEDED(spHtmlDoc->get_parentWindow(&spHtmlWindow)) && spHtmlWindow)
		{
			CComBSTR bstrJs  = pszJs;
			CComBSTR bstrlan = L"javascript";
			VARIANT varRet;
			varRet.vt = VT_EMPTY;

			spHtmlWindow->execScript(bstrJs, bstrlan, &varRet);
		}
	}
}
呼叫如下:
void CMainDlg::OnScript1(UINT uNotifyCode, int nID, CWindow wndCtl)
{
	ExecScript1(L"window.alert(\'ExecScript1\')");
}


b)第二種方法

獲取IE控制元件中的JavaScript物件,執行它的方法,封裝如下:

void CMainDlg::ExecScript2()
{
	CComPtr<IDispatch> spDisp;
	HRESULT hr = m_spWebBrowser->get_Document(&spDisp);
	if (SUCCEEDED(hr))
	{
		CComQIPtr<IHTMLDocument2> spDoc2 = spDisp;
		if (spDoc2)
		{
			CComDispatchDriver spScript;
			hr = spDoc2->get_Script(&spScript);
			if (SUCCEEDED(hr))
			{
// 				{
// 					CComVariant varRet;  
// 					spScript.Invoke0(L"test1", &varRet);  
// 					int a = 10;
// 				}

				//--Add1
				{
					CComVariant var1 = 10, var2 = 20, varRet;  
					spScript.Invoke2(L"Add1", &var1, &var2, &varRet);  

					CString strVal;
					strVal.Format(L"%d", varRet.intVal);
					OutputDebugString(strVal.GetBuffer(0));
				}

				//--Add2
				{
					CComVariant var1 = 10, var2 = 20, varRet;  
					spScript.Invoke2(L"Add2", &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]);  

					CString strVal;
					strVal.Format(L"%d %d %d", varValue[0].intVal,
											   varValue[1].intVal,
											   varValue[2].intVal);
					OutputDebugString(strVal.GetBuffer(0));
				}

				//--Add3
				{
					CComVariant var1 = 10, var2 = 20, varRet;  
					spScript.Invoke2(L"Add3", &var1, &var2, &varRet);  
					CComDispatchDriver spData = varRet.pdispVal;  

					CComVariant varValue1, varValue2;  
					spData.GetPropertyByName(L"result", &varValue1);  
					spData.GetPropertyByName(L"str", &varValue2); 

					CString strVal;
					strVal.Format(L"%d %s", varValue1.intVal, varValue2.bstrVal);
					OutputDebugString(strVal.GetBuffer(0));
				}
			}
		}
	}
}

程式碼處理了返回值是陣列或結構體的方法可供參考。

要這樣呼叫程式碼,還必須在html中定義對應的JS 函式,注意此時必須在對話方塊中輸入對應的html網頁地址,如下:
function Add1(value1, value2) {  
    window.alert('Add1');
    return value1 + value2;  
}  

function Add2(value1, value2) {  
    var array = new Array();  
    array[0] = value1;  
    array[1] = value2;  
    array[2] = value1 + value2;  

    window.alert('Add2');
    return array;  
}  

function Add3(value1, value2) {  
    var data = new Object();  
    data.result = value1 + value2;  
    data.str = "Hello,World!";  

    window.alert('Add3');
    return data;  
}  

function test1(){
    return function() {
            alert('test1');
    }
}
test1方法返回一個函式型別,稍後我們再討論它。

2.JS呼叫C++

如前文所說,js呼叫C++程式碼是因為IE控制元件實現時允許呼叫外部指定的IDispatch介面方法,俗稱“打洞”,很容易理解,本來是各玩各的,結果現在打了個洞,可以從瀏覽器調到本地了,這個洞就是IDispatch介面。

js中呼叫外部介面方法是,通過window.external,如下:

<script type="text/javascript">
function OnSayGoodBye() {
    window.alert(typeof window.external.SayGoodBye);
    window.alert(typeof window.document.getElementById);
    window.external.SayGoodBye(10,'DaGoodBye!');
}

</script> 
</head>

<body>
<p>
<button type="button" onclick="window.external.SayHello('DaHello!')">SayHello!</button>
</p>
<p>
<button type="button" onclick="OnSayGoodBye()">SayGoodBye!</button>
</p>
</body>
這裡兩個按鈕點選,都會調到我們的IDispatch介面上,那麼怎麼才能知道會調到我們的IDispatch介面上呢,答案是在InitDialog中初始化時註冊下即可。
	//設定瀏覽器內容回撥介面
	wndIE.SetExternalDispatch((IDispatch*)(&m_ExternalObject));

其中m_ExternalObject就是我們自定義的IDispatch的實現,這這個類中需要不用型別庫實現SayHello和SayGoodBye的響應如下:
HRESULT STDMETHODCALLTYPE CExternalObject::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 (0==dispIdMember ||  
		(dispIdMember!=EXTFUNC_ID_HELLO && dispIdMember!=EXTFUNC_ID_GOODBYE) ||  
		0==(DISPATCH_METHOD&wFlags || DISPATCH_PROPERTYGET&wFlags))  
	{  
		return E_NOTIMPL;  
	}

	if (pVarResult)  
	{  
		CComVariant var(true);  
		*pVarResult = var;  
	}  

	//判斷屬性
	if (DISPATCH_PROPERTYGET&wFlags)
	{
		return S_OK;
	}

	USES_CONVERSION;  

	//呼叫本地方法
	switch (dispIdMember)  
	{  
	case EXTFUNC_ID_HELLO:  
		if (pDispParams &&								//引數陣列有效  
			pDispParams->cArgs==1 &&					//引數個數為1  
			pDispParams->rgvarg[0].vt==VT_BSTR &&		//引數型別滿足  
			pDispParams->rgvarg[0].bstrVal)				//引數值有效  
		{  
			CString strVal(OLE2T(pDispParams->rgvarg[0].bstrVal));  
			AtlMessageBox(NULL, strVal.GetBuffer(0), L"SayHello");
		}  
		break;  

	case EXTFUNC_ID_GOODBYE:  
		if (pDispParams &&								//引數陣列有效  
			pDispParams->cArgs==2 &&					//引數個數為2 
			pDispParams->rgvarg[1].vt==VT_I4 &&	
			pDispParams->rgvarg[0].vt==VT_BSTR &&		//引數型別滿足  
			pDispParams->rgvarg[1].bstrVal &&
			pDispParams->rgvarg[0].bstrVal)				//引數值有效  
		{  
			CString strVal;
			strVal.Format(L"%d %s", pDispParams->rgvarg[1].bstrVal, 
									pDispParams->rgvarg[0].bstrVal);
			AtlMessageBox(NULL, strVal.GetBuffer(0), L"SayGoodBye");
		}  
		break;  
	}  

	return S_OK;  
}

這裡我們判斷對應的呼叫引數和型別,然後做出對應響應,即可完成對應本地呼叫。

值得注意的是在上文js程式碼中我們看到,

    window.alert(typeof window.external.SayGoodBye);
    window.alert(typeof window.document.getElementById);
我們分別輸出自定義介面函式和瀏覽器自身函式型別對比,前者為boolean,後者為object,這是為什麼?

看到上文C++程式碼中

	//判斷屬性
	if (DISPATCH_PROPERTYGET&wFlags)
	{
		return S_OK;
	}
Disptach呼叫引數為DISPATCH_PROPERTYGET即為取當前函式的屬性,這裡我們並沒有預設指明pVarResult的結果,所以才會出現使用預設值boolean。那麼如果想讓這個返回型別正確,應該返回什麼呢?

因為JS是允許返回函式型別的,上文我們說了test1函式返回的是函式型別,因此我們在ExecScript2中的呼叫test1,斷點看他返回的結果在C++中的佈局即可。結果如下:


可以看到,這裡的函式型別(object)對應C++中的IDispatch。結合JS中的函式物件特徵,我們調整返回值,如下處理:

	//判斷屬性
	if (DISPATCH_PROPERTYGET&wFlags)
	{
		pVarResult->vt = VT_DISPATCH;
		pVarResult->pdispVal = this;
		return S_OK;
	}
再次檢視結果=object,滿足需求,實際中有些js程式碼判斷了待呼叫函式的型別,對於自定義的外部介面函式一定要注意這裡的坑。

完整演示程式碼下載連結,注意載入目錄htmls中的js檔案進行測試