Chromium就不用说了。它的快是很出名的,即便作为控件来使用。CEF也运用了多进程技术,HTML的渲染和JavaScript的解释运行都是在格外的进程中,不会影响你的UI线程,奔溃了也不会破坏你的进程。并且CEF是用C++写的,对外提供的原生接口就是C++接口,比起WebBrowser的那套COM接口来说不知道好用多少倍。
class CJsCallCppDlg : public CDialogEx, public IDispatch
将其多重继承于IDispatch。啊!多重继承?怎么把这样的坑爹的东西搞出来?NO NO NO,不要谈多重继承就色变。这里的IDispatch里面的全部成员函数都是纯虚函数。本质上IDispatch就是个接口,C++的实现接口的方式就是多重继承,尽管不鼓舞用多重继承来继承实现代码,可是像这样用来实现接口是面向对象中很经常使用的。当然你也能够class MyIDispatch
: public IDispatch。然后把MyIDispatch实例化成一个对象后传递给JavaScript来调用。这里之所以用CxxDlg来实现IDispatch,是为了方便,由于待会儿,我仅仅要把CxxDlg的this指针传递给JavaScript。它就能够调用我的CxxDlg从IDispatch处继承来的虚函数Invoke。也就是说JavaScript就能够直接调用CxxDlg::Invoke,然后在CxxDlg::Invoke中能够非常方便的调用我CxxDlg的其他成员函数。
然后我写下了例如以下的HTML文件:
<meta charset="utf-8" />
<title></title>
<script language="javascript">
function ShowMessageBox()
if (cpp_object != null)
cpp_object.ShowMessageBox("你好,我是Javascript,你是谁?");
function GetProcessID()
if (cpp_object != null)
var id = cpp_object.GetProcessID();
document.getElementById("process_info").innerText = "本进程ID为:" + id;
function SaveCppObject(obj)
cpp_object = obj;
var cpp_object;
</script>
</head>
<p id="process_info"></p>
<button type="button" onclick="ShowMessageBox()">MessageBox</button>
<button type="button" onclick="GetProcessID()">Process ID</button>
</body>
</html>
然后我在我的
CxxDlg里写下了例如以下的两个成员函数:
DWORD CJsCallCppDlg::GetProcessID()
return GetCurrentProcessId();
void CJsCallCppDlg::ShowMessageBox(const wchar_t *msg)
MessageBox(msg, L"这是来自javascript的消息");
接来下。我要用HTML中的这两个button,分别调用这两个C++函数,当中一个是ShowMessageBox。让Javascript调用它并传递一个字符串给它,终于C++这边通过Windows API的MessageBox实现弹出一个消息框。
另外一个是GetProcessID,Javascript调用它,终于C++这边通过Windows API的GetCurrentProcessId()获取本进程ID,并给Javascript返回这个ID值。然后显示到HTML中。
因为我的CxxDlg继承了IDispatch。那么我须要实现IDispatch中的七个纯虚函数。所以在CxxDlg类的声明中加入例如以下七个虚函数的声明:
virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount(UINT *pctinfo);
virtual HRESULT STDMETHODCALLTYPE GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo);
virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId);
virtual HRESULT STDMETHODCALLTYPE Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr);
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject);
virtual ULONG STDMETHODCALLTYPE AddRef();
virtual ULONG STDMETHODCALLTYPE Release();
然后实现这七个虚函数:
//我自己给我的两个函数拟定的数字ID。这个ID能够取0-16384之间的随意数
FUNCTION_ShowMessageBox = 1,
FUNCTION_GetProcessID = 2,
//不用实现,直接返回E_NOTIMPL
HRESULT STDMETHODCALLTYPE CJsCallCppDlg::GetTypeInfoCount(UINT *pctinfo)
return E_NOTIMPL;
//不用实现,直接返回E_NOTIMPL
HRESULT STDMETHODCALLTYPE CJsCallCppDlg::GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo)
return E_NOTIMPL;
//JavaScript调用这个对象的方法时,会把方法名,放到rgszNames中,我们须要给这种方法名拟定一个唯一的数字ID。用rgDispId传回给它
//同理JavaScript存取这个对象的属性时。会把属性名放到rgszNames中,我们须要给这个属性名拟定一个唯一的数字ID,用rgDispId传回给它
//紧接着JavaScript会调用Invoke。并把这个ID作为參数传递进来
HRESULT STDMETHODCALLTYPE CJsCallCppDlg::GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId)
//rgszNames是个字符串数组。cNames指明这个数组中有几个字符串。假设不是1个字符串。忽略它
if (cNames != 1)
return E_NOTIMPL;
//假设字符串是ShowMessageBox。说明JavaScript在调用我这个对象的ShowMessageBox方法。我就把我拟定的ID通过rgDispId告诉它
if (wcscmp(rgszNames[0], L"ShowMessageBox") == 0)
*rgDispId = FUNCTION_ShowMessageBox;
return S_OK;
//同理,假设字符串是GetProcessID。说明JavaScript在调用我这个对象的GetProcessID方法
else if (wcscmp(rgszNames[0], L"GetProcessID") == 0)
*rgDispId = FUNCTION_GetProcessID;
return S_OK;
return E_NOTIMPL;
//JavaScript通过GetIDsOfNames拿到我的对象的方法的ID后。会调用Invoke。dispIdMember就是刚才我告诉它的我自己拟定的ID
//wFlags指明JavaScript对我的对象干了什么事情!
//假设是DISPATCH_METHOD,说明JavaScript在调用这个对象的方法。比方cpp_object.ShowMessageBox();
//假设是DISPATCH_PROPERTYGET。说明JavaScript在获取这个对象的属性,比方var n = cpp_object.num;
//假设是DISPATCH_PROPERTYPUT。说明JavaScript在改动这个对象的属性,比方cpp_object.num = 10;
//假设是DISPATCH_PROPERTYPUTREF,说明JavaScript在通过引用改动这个对象,详细我也不懂
//演示样例代码并没有涉及到wFlags和对象属性的使用。须要的请自行研究,使用方法是一样的
//pDispParams就是JavaScript调用我的对象的方法时传递进来的參数,里面有一个数组保存着全部參数
//pDispParams->cArgs就是数组中有多少个參数
//pDispParams->rgvarg就是保存着參数的数组,请使用[]下标来訪问。每一个參数都是VARIANT类型,能够保存各种类型的值
//详细是什么类型用VARIANT::vt来推断,不多解释了。VARIANT这东西大家都懂
//pVarResult就是我们给JavaScript的返回值
//其他不用管
HRESULT STDMETHODCALLTYPE CJsCallCppDlg::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid,
WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)
//通过ID我就知道JavaScript想调用哪个方法
if (dispIdMember == FUNCTION_ShowMessageBox)
//检查是否仅仅有一个參数
if (pDispParams->cArgs != 1)
return E_NOTIMPL;
//检查这个參数是否是字符串类型
if (pDispParams->rgvarg[0].vt != VT_BSTR)
return E_NOTIMPL;
//放心调用
ShowMessageBox(pDispParams->rgvarg[0].bstrVal);
return S_OK;
else if (dispIdMember == FUNCTION_GetProcessID)
DWORD id = GetProcessID();
*pVarResult = CComVariant(id);
return S_OK;
return E_NOTIMPL;
//JavaScript拿到我们传递给它的指针后,由于它不清楚我们的对象是什么东西,会调用QueryInterface来询问我们“你是什么鬼东西?”
//它会通过riid来问我们是什么东西。仅仅有它问到我们是不是IID_IDispatch或我们是不是IID_IUnknown时,我们才干肯定的回答它S_OK
//由于我们的对象继承于IDispatch。而IDispatch又继承于IUnknown,我们仅仅实现了这两个接口,所以仅仅能这样来回答它的询问
HRESULT STDMETHODCALLTYPE CJsCallCppDlg::QueryInterface(REFIID riid, void **ppvObject)
if (riid == IID_IDispatch || riid == IID_IUnknown)
//对的,我是一个IDispatch,把我自己(this)交给你
*ppvObject = static_cast<IDispatch*>(this);
return S_OK;
return E_NOINTERFACE;
//我们知道COM对象使用引用计数来管理对象生命周期,我们的CJsCallCppDlg对象的生命周期就是整个程序的生命周期
//我的这个对象不须要你JavaScript来管,我自己会管。所以我不用实现AddRef()和Release()。这里乱写一些。
//你要return 1;return 2;return 3;return 4;return 5;都能够
ULONG STDMETHODCALLTYPE CJsCallCppDlg::AddRef()
return 1;
//同上。不多说了
//题外话:当然假设你要new出一个c++对象来并扔给JavaScript来管,你就须要实现AddRef()和Release(),在引用计数归零时delete this;
ULONG STDMETHODCALLTYPE CJsCallCppDlg::Release()
return 1;
该讲的都在代码凝视中讲了。简单来说。当JavaScript运行如cpp_object.GetProcessID();的代码时,会先调用GetIDsOfNames,并把"GetProcessID"这个字符串传递进来,我们给它分配一个自拟的ID,紧接着JavaScript会拿着这个ID来调用Invoke。至于參数和返回值怎样传递。代码和凝视写得非常清楚了。
注意我的HTML中的JavaScript代码中,我用一个var cpp_object;全局变量来保存C++对象。然后我还写了一个SaveCppObject()函数给C++调用。在WebBrowser载入完成HTML文档后。须要先用C++调用JavaScript的这个SaveCppObject()函数,并把C++对象指针传递给JavaScript。这样JavaScript才干把它保存到var cpp_object;中,才干进行接下来的JavaScript调用C++。C++调用JavaScript的SaveCppObject()方法代码例如以下:
//调用JavaScript的SaveCppObject函数,把我自己(this)交给它。SaveCppObject会把我这个对象保存到全局变量var cpp_object;中
//以后JavaScript就能够通过cpp_object来调用我这个C++对象的方法了
void CJsCallCppDlg::OnBnClickedOk()
CComQIPtr<IHTMLDocument2> document = m_webbrowser.get_Document();
CComDispatchDriver script;
document->get_Script(&script);
CComVariant var(static_cast<IDispatch*>(this));
script.Invoke1(L"SaveCppObject", &var);
好了,至此。JavaScript调用C++已经完毕了。这样的方法,须要先把IDispatch*(演示样例代码中是this。但由于this是CJsCallCppDlg的实例。而CJsCallCppDlg多重继承于IDispatch,实际this就是IDispatch*了)传递给JavaScript。JavaScript把它保存好。然后调用它。网上另一种方法是。在C++这边再实现IDocHostUIHandler接口,然后通过一系列麻烦的操作,JavaScript那边就能够直接通过window.external来调用C++,而不用var cpp_object;了。
只是那个实现实在是太麻烦太恶心了,又会引入一大堆我解释不清楚的东西,所以还是作罢了,这样才是最简洁的实现。
最后晒上一张执行效果图:
对了。另一点,写好的HTML文件不仅能够直接和EXE放在一个文件夹下使用。也能够在VisualStudio中把HTML文件作为资源加入到项目中,这样终于写出来的程序仅仅有一个EXE。HTML文件已经在EXE里面了,至于怎样让WebBrowser载入这个HTML文件。能够在CxxDlg::OnInitDialog()中使用例如以下代码:
//载入资源文件里的HTML,IDR_HTML1就是HTML文件在资源文件里的ID
wchar_t self_path[MAX_PATH] = { 0 };
GetModuleFileName(NULL, self_path, MAX_PATH);
CString res_url;
res_url.Format(L"res://%s/%d", self_path, IDR_HTML1);
m_webbrowser.Navigate(res_url, NULL, NULL, NULL, NULL);
常见问题:
①调用m_webbrowser.Navigate()载入一个HTML文档后,不要紧接着就:
CComQIPtr<IHTMLDocument2> document = m_webbrowser.get_Document();
CComDispatchDriver script;
document->get_Script(&script);
这样获取其接口指针进行C++调用Javascript操作,这样往往会取到空指针,由于m_webbrowser.Navigate()调用完成,并不意味着HTML文档已经载入、渲染完成,m_webbrowser.Navigate()实际上是一个异步操作,调用以后仅仅是发出了一个命令,让WebBrowser去载入这个HTML文档。至于何时载入完成,能够处理WebBrowser的DocumentComplete事件来获知,仅仅有在触发DocumentComplete事件后,才干够获取其接口指针进行操作。所以在上面的演示样例中,假设想让HTML文档载入完成后就自己主动用C++调用Javascript的SaveCppObject()函数,把C++对象传递过去,仅仅需把上面演示样例程序中我写在button响应函数中的代码写到DocumentComplete事件的响应函数中就可以(Github上的演示样例代码已经更新成这样了)。
怎么加入DocumentComplete事件响应函数?看下图,先选中WebBrowser控件,再到属性对话框里找想处理的事件,全部的Activex控件的事件响应函数都能够在这里加入。