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文件进行测试

原创,转载请注明来自http://blog.csdn.net/wenzhou1219

猜你喜欢

转载自blog.csdn.net/wenzhou1219/article/details/78311171