首先还是谈一下IE,虽然IE已被很多人深恶痛绝,但毕竟是微软已经出了这么多年的东东,并且绝大多数人的PC电脑上都有IE控件,使得基于IE发布程序的体积很小,但使用用户电脑上自带的IE控件带来的问题是烦琐的IE版本兼容问题,在这里鄙视一下微软,同一个系列的产品竟然不能做到完全向前兼容。html和css虽然不兼容,但是IE编程接口是完全一样的,这得益于微软的com库的结构化设计和实现。所以与IE交互,必须得先说一下com,因为所有的接口都是com式接口。
com的基本思想很简单,所有的组件模块都提供一个最根本的接口, IUnkown,它有三个方法,AddRef和Release实现了引用计数,QueryInterface实现了根据接口id查询另外的接口,所有的接口都从IUnkown派生。详细的com理论有很多专业的书籍论述,这里不再赘述。与IE交互,有一个接口是最关键的,IDispatch。它有一个方法是最关键的Invoke,只要弄清楚了IDispatch的Invoke方法,和IE交互的绝大部分任务都可以完成了。下面看一下这个关键的方法的原型:
IDispatch:public IUnkown
{
...
HRESULT Invoke( DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS FAR* pDispParams,
VARIANT FAR* pVarResult, EXCEPINFO FAR* pExcepInfo, unsigned int FAR* puArgErr );
...
}
这个方法每个参数的意义msdn上有详细的阐述,就我的理解而看,它作为一个组件, 向外提供了一个万能接口,据此可以实现两个很有用的功能:
1. 获取和设置组件的属性变量. 对应wFlags的DISPATCH_PROPERTYGET和DISPATCH_PROPERTYPUT
2.以任意参数调用任意一个被支持的方法..对应wFlags的DISPATCH_METHOD
用面向对象的观点来看,有了这两个功能,任意一个实现该接口的组件就抽象成同一个接口实现的万能对象,可以通过指定的字符串名字获取设置属性和调用方法。如果有过脚本编程经验的c++开发人员一定会发现,脚本恰恰就是如此,c++书写代码的时候也是通过名字访问对象,但编译好后变成二进制代码后就没有名字的概念了,只有偏移和地址。而脚本里头书写代码的时候是通过名字,解释执行的时候也是通过名字。脚本和native语言的最大区别是脚本对象的所有属性和方法是动态的,在执行的时候还可以修改。看到这里,很容易联想到实现了IDispatch的组件对象具有了脚本的特性,c++对象被脚本化了!这就意味着你可以把原来用c++写的类的所有属性和方法都通过Invoke来执行,在脚本里头可以直接访问!相当于给脚本增加了native的扩展。这里所说的脚本就指的Javascript,它是IE内置的脚本引擎。IDispatch接口很重要的一个功能就是如此,微软通常所说的双接口就是这个意思。com的设计理念是很好的,但是在实现的时候绑上了太多的windows因素,从而使得很多人对其都望而生畏,对IDispatch接口也觉得高深莫测,其实它的原理一点都不复杂。了解了这些,接下来要和JS脚本交互就比较容易了。
在谈这个之前,先说一说IE编程中经常要用到的接口。首先是IWebbrowser2接口,创建IE控件的时候就得到这个指针,它对应着浏览器的一个实例,但对于有多个iframe的页面来说,每个子iframe也有一个IWebbrowser2指针,这就有了层级之分,最顶层的IWebbrowser2只有一个,对应于创建com实例时返回的那个。IWebbrowser2控制浏览窗口的一些常见行为,最重要的就是根据一个url打开一个新页面了。通过IWebbrowser2指针我们可以得到很多其他有用的接口,IHTMLWindow2对应于一个frame的视图,IHTMLDocument2是IHTMLWindow2渲染文档,对应着dom树结构。这个层级还是很清晰的。这三个接口都是从IDispatch派生的,意味着它们已经被脚本化了。在js中有两个全局对象window和document,应该是分别对应着IHTMlWindow2和IHTMLDocument2。(这一点是推测,但可以从js代码验证,当从js中调用一个你自己实现的natvie方法时,如果用window和document作为参数在c++代码中就变成IDispatch接口,从中能QueryInterface得到IHTMLWindow2和IHTMLDocument2)。要完成c++和js交互,可以分解成两个任务,一是c++调用js代码;二是js调用c++代码,这其实也所有脚本和natvie交互的两个基本任务。其实网上也有很多示例代码,本文主要根据自己的理解从设计开发的角度去阐述为什么要这么做。
先说c++调用js。每段js执行代码都有它自己的执行环境,在IE里面可以看做是IHTMLWindow2。我用到的有两种方案,一种是直接调用IHTMLWindow2的execScript方法.
HRESULT execScript( BSTR code, BSTR language, VARIANT *pvarRet);
{
private:
CComPtr<IWebBrowser2> m_pWebBrowser; //保存创建出来的浏览器控件实例
BEGIN_MSG_MAP(CWebBrowser)
MESSAGE_HANDLER(WM_CREATE,OnCreate)
CHAIN_MSG_MAP(CAxHostWindow)
END_MSG_MAP()
LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/)
{
// Create WebBrowser object
LPOLESTR pName=NULL;
StringFromCLSID(CLSID_WebBrowser,&pName);
CComPtr<IDispatch>disp;
CComPtr<IUnknown> p;
_InternalQueryInterface(IID_IDispatch,(void**)&disp);
CreateControlEx(pName,m_hWnd,NULL,&p,DIID_DWebBrowserEvents2,disp); // 创建 WebBrowser
CoTaskMemFree(pName);
// Connect event
HRESULT hRet = QueryControl(IID_IWebBrowser2, (void**)&this->m_pWebBrowser);
return m_pWebBrowser?S_OK:-1;
}
DISPID dispIdMember,
REFIID riid,
LCID lcid,
WORD wFlags,
DISPPARAMS* pDispParams,
VARIANT* pVarResult,
EXCEPINFO* pExcepInfo,
unsigned int* puArgErr
)
{
HRESULT nRet = S_OK;
if(wFlags&DISPATCH_METHOD) //属于方法调用
{
{
volatile long _refCount; //引用计数
public:
virtual ULONG WINAPI AddRef() { return InterlockedIncrement(&_refCount);}
virtual ULONG WINAPI Release() { long refs=InterlockedDecrement(&_refCount); if(!refs)delete this;return refs;}
virtual HRESULT WINAPI QueryInterface(REFIID riid, __RPC__deref_out void **ppvObject)
{
*ppvObject=NULL;
if(IsEqualIID(riid,IID_IUnknown))
*ppvObject=(IUnknown*)this;
else if(IsEqualIID(riid,IID_IDispatch))
*ppvObject=(IDispatch*)this;
else
return E_NOINTERFACE;
AddRef();
return 0;
}
CMyJsObject():_refCount(0)
{
}
virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount(
/* [out] */ __RPC__out UINT *pctinfo)
{
return E_NOT_IMPL;
}
virtual HRESULT STDMETHODCALLTYPE GetTypeInfo(
/* [in] */ UINT iTInfo,
/* [in] */ LCID lcid,
/* [out] */ __RPC__deref_out_opt ITypeInfo **ppTInfo)
{
return E_NOT_IMPL;
}
virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames( //做字符串名字映射到自定义整数(注意要大于0,负数有特殊用途)
/* [in] */ __RPC__in REFIID riid,
/* [size_is][in] */ __RPC__in_ecount_full(cNames) LPOLESTR *rgszNames,
/* [range][in] */ UINT cNames,
/* [in] */ LCID lcid,
/* [size_is][out] */ __RPC__out_ecount_full(cNames) DISPID *rgDispId)
{
while(cNames)
{
if(wcscmp(*rgszNames,L"testName1")==0)
*rgDispId=1;
else if(wcscmp(*rgszNames,L"testName2")==0)
*rgDispId=2;
else if(wcscmp(*rgszNames,L"testFunc1")==0)
*rgDispId=3;
else if(wcscmp(*rgszNames,L"testFunc2")==0)
*rgDispId=4;
return DISP_E_UNKNOWNNAME;
rgDispId++;
rgszNames++;
cNames--;
}
return S_OK;
}
virtual /* [local] */ HRESULT STDMETHODCALLTYPE 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) ;
static int newMyJsObject(IDispatch**ppDisp); // 创建MyObject
};
CComPtr<IConnectionPoint> pCP;
HRESULT hRes = pWebBrowser2->QueryInterface(__uuidof(IConnectionPointContainer), (void**)&pCPC);
if (SUCCEEDED(hRes))
hRes = pCPC->FindConnectionPoint(DIID_DWebBrowserEvents2, &pCP);
if (SUCCEEDED(hRes))
hRes = pCP->Advise(pUnk, pdw);
{
if(pDispParams->cArgs==7)
{
string strUrl=pDispParams->rgvarg[5].pvarVal->bstrVal;
string strTargetFrame=pDispParams->rgvarg[3].pvarVal->bstrVal;
atstring strHeader=pDispParams->rgvarg[1].pvarVal->bstrVal;
BOOL bCancel=FALSE;
m_pEventSink->OnBeforeNavigate2(pDispParams->rgvarg[6].pdispVal,strUrl,pDispParams->rgvarg[4].pvarVal->intVal,
strTargetFrame,pDispParams->rgvarg[2].pvarVal,strHeader, &bCancel);
*pDispParams->rgvarg[0].pboolVal=bCancel?VARIANT_TRUE:VARIANT_FALSE;
}
break;
case DISPID_NAVIGATECOMPLETE2:
if(pDispParams->cArgs==2)
{
atstring strUrl=pDispParams->rgvarg[0].pvarVal->bstrVal;
m_pEventSink->OnNavigateComplete2(pDispParams->rgvarg[1].pdispVal,strUrl);
}
break;
case DISPID_DOCUMENTCOMPLETE:
if(pDispParams->cArgs==2)
{
string strUrl=pDispParams->rgvarg[0].pvarVal->bstrVal;
m_pEventSink->OnDocumentComplete(pDispParams->rgvarg[1].pdispVal,strUrl);
}
break;
case DISPID_NEWWINDOW2:
if(pDispParams->cArgs==2)
{
BOOL bCancel=FALSE;
m_pEventSink->OnNewWindow2(pDispParams->rgvarg[1].ppdispVal,&bCancel);
*pDispParams->rgvarg[0].pboolVal=bCancel?VARIANT_TRUE:VARIANT_FALSE;
}
break;
case DISPID_NAVIGATEERROR:
if(pDispParams->cArgs==5)
{
BOOL bCancel=FALSE;
string strFrameName=pDispParams->rgvarg[2].pvarVal->bstrVal;
string strURL=pDispParams->rgvarg[3].pvarVal->bstrVal;
DWORD statusCode=pDispParams->rgvarg[1].pvarVal->intVal;
m_pEventSink->OnNavigateError(pDispParams->rgvarg[4].pdispVal,strURL,strFrameName,statusCode,&bCancel);
*pDispParams->rgvarg[0].pboolVal=bCancel?VARIANT_TRUE:VARIANT_FALSE;
}
break;
{
public:
virtual void OnBeforeNavigate2(IDispatch*pDisp,LPCTSTR lpszURL,DWORD nFlags,LPCTSTR lpszTargetFrameName,
VARIANT* PostedData, LPCTSTR lpszHeaders,BOOL* pbCancel )=0;
virtual void OnNavigateComplete2(IDispatch*pDisp,LPCTSTR strURL )=0;
virtual void OnNewWindow2(IDispatch**ppDisp,BOOL*pBCancel)=0;
virtual void OnDocumentComplete(IDispatch*pDisp,LPCTSTR lpszURL)=0;
virtual void OnNavigateError(IDispatch *pDisp,LPCTSTR lpszURL,LPCTSTR TargetFrameName,
DWORD StatusCode, BOOL *pBCancel)= 0 ;
};