前几天在《 一个基于MFC的自动化(Automation)实例》上说最近会发一个关于如何挂接浏览器事件的教程,现在如期兑现承诺啦。说实话,解决这个问题花了我近一年的时间,虽然期间不是每天都在想这个问题,但无论如何这听起来绝对是一段不短的时间!也许因为我是生物系的吧,不能像计算机系的朋友那样有那么多现成的资源可以利用,一切都靠自学,碰到不懂的问题就在浩瀚的网络世界中寻找答案,有时候的确感到很孤独!
人生最大的痛苦莫过于有了问题没有答案,有了答案又没有Money!毫不客气的说一句,中国在“开源”方面做的的确不咱的~一点点小的成就就希望把它转变成大把大把的金钱,结果呢,正如大家看到的,中国菜鸟的死亡率要明显高于美国(因为中国的菜鸟在成长为肉鸟的过程中需要付出太多的¥,嘿嘿~)。
当然,我并不崇洋媚外,相反,我觉得自己更像是个“愤青”。我说中国的“开源”做的不好那是事实,没什么好争论的。而我之所以没有沦落为“崇洋派”是因为我注意到了其中的潜台词:“在当前国情下,…..”。现在的中国人虽然早已不像六七十年代时那么贫苦,但也不见得就衣食无忧。既如此,那么想挣点钱养家糊口的想法也就很正常拉。这就是中国开源做的不好的原因。而如果我本身就有1000万,我又怎么会在乎那么个小程序所带来的利润?这就是美国的开源为什么做的好的原因。有些朋友可能会反驳,美国人最近也不好过啊,金融危机弄的人心慌慌的~哈,对啊,这就是“沃尔玛”踩踏事件发生的原因。前些天还在看网友对“重庆家乐福踩踏事件”的评论呢,哎呀,说什么中国人劣根性啊,中国人爱贪小便宜的啊,那叫一个多~当然,中国人民一向勤劳、善良,而我做为一个自豪于祖国五千年灿烂文明的普通中国人自然是不会去说美国人怎么劣根性啦,我只是想揭露这样一个事实:人与人,种族与种族,国家与国家之间是没有什么本质上的区别的,全民素质的不同是基于不同的基本国情的。当把所有人都置入一个相同的环境中,其基本表现是相同的。正如这次金融危机,不就是近似的把中国人和美国人置入了一个同等的环境?那么,他们的表现有不同吗?
时刻记住这句潜台词,那么你对国家的现状将会有一个理性的解读。就拿“开源”这件事来说,人家肯免费给你那自然是人家的深明大意,而人家要收费呢那很正常,没什么可以值得批判的。如果你真的对此看不过去,那么你就应该踏踏实实的多为国家做点事,帮助国家快速发展。等国家富裕了,开源自然也就做好了。正如我上面所说,基本国情将决定全名素质。而我决定免费公开我的文章,那并不是说我有多高尚,而是因为我不在乎这么点钱(哈,是虚拟币啦),如果有一天,我混到流落街头了,大家说我还会这么伟大吗?必竟连马克思都说人的基本需求之一是生理需求啊(说白了就是民以食为天嘛~嘿嘿)。
扯哪去都不知道了 哈哈…也不知道今天哪来这么多感慨,回归正题吧~当你决定看这篇文章的时候我已假设你具备了以下知识:①掌握了COM的一些基本知识,如连接点,接收器等;②具有一定的MFC编程经验,了解MFC接收器(Sink)的内部实现;③了解HTML的基础知识;④对IE内部接口有一定的了解(如IWebBrowser2, IHTMLDocument2等)
本文通过一个MFC对话框程序实现的接收器达到挂接IE事件的目的。在Visual stdio2008,IE 8.0下测试通过。用VC6.0的朋友需将Microsoft SDK更新成6.0。
另外,推荐大家看一篇MSDN上的文章(Handling HTML Element Events),虽然是英文,看起来会有点吃力,但大家一定要学会看MSDN,有什么人会比生产商更了解产品的内部实现呢?看完之后,你就会底气十足的说”I CAN DO IT!”。别犹豫拉,我四级都考N次拉(不知道这次有没有过,a meng~~~),不照样看~
首先给大家介绍一下程序实现的流程:利用MFC完成接收器(CSink)的编写->获得IWebBrowser2接口->…->获得IHTMLElement接口指针->调用AfxConnectionAdvise实现接收器与连接点的连接->响应事件。
接下来一步步实现上述步骤:
第一步:建立一个MFC对话框工程,工程名为HandleEvent,注意选上自动化支持->添加新类CSink使其继承自CCmdTarget(此基类实现了IDispatch),并选上自动化支持(Automation)。完成这些之后这个类其实就是一个Sink啦,当然,到目前为止它不具有任何的功能。
下面为这个接收器添加事件处理函数,分两步:
1. 在sink.cpp中的BEGIN_DISPATCH_MAP(CSink, CCmdTarget)与END_DISPATCH_MAP()之间间加入 DISP_FUNCTION_ID(CSink,"onclick",DISPID_HTMLELEMENTEVENTS2_ONCLICK,OnClick,VT_VARIANT,VTS_DISPATCH)。
2. 在sink.h中加入OnClick的实现:
BOOL CSink::OnClick(IHTMLEventObj *pEvtObj)
{
::AfxMessageBox(L"Button clicked!");
return TRUE;
}
注意:请在sink.h中包含:mshtml.h和mshtmdid.h
至此我们已经完成了一个接收器该做的所有事,下面说说这两步的内含:前面已经说过,CCmdTarget类已经实现了IDispatch接口,当通过AfxConnectionAdvise()将连接点和接收器连接之后,当有事件发生时,连接点将调用CCmdTarget(CSink)的Invoke()函数,这是在外部。而在内部,CCmdTarget将Invoke的调用映射到DISPATCH_MAP上,在其上查找有无与当前事件的DISPID对应的处理函数。比如当发生onclick事件时,连接点将调用Invoke(..,DISPID_HTMLELEMENTEVENTS2_ONCLICK,…),而在CCmdTarget内部,它会在DISPATCH_MAP上寻找有没有DISPID= DISPID_HTMLELEMENTEVENTS2_ONCLICK的处理函数。如果有则再将事件映射到OnClick上。
MSDN中有这么一段供参考:
Handling Events using MFC
The MFC CCmdTarget class implements the IDispatch interface, which means that any class derived from this class can implement an event sink. The IDispatch feature requires a call to the CCmdTarget::EnableAutomation method.
The MFC AfxConnectionAdvise and AfxConnectionUnadvise functions find a specified connection point and then advise or unadvise appropriately.
The DECLARE_DISPATCH_MAP, BEGIN_DISPATCH_MAP, DISP_FUNCTION, DISP_FUNCTION_ID and END_DISPATCH_MAP macros are used to map each event method to your event handler function.
When handling events from a Microsoft ActiveX control you are hosting in your MFC applications, use the DECLARE_EVENTSINK_MAP, BEGIN_EVENTSINK_MAP, ON_EVENT and END_EVENTSINK_MAP macros.
If you are hosting the WebBrowser Control on a dialog box, you can use the Microsoft Visual C++ ClassWizard to map events to event handlers.
The MFC CHtmlView class hosts the WebBrowser Control and provides overridable methods for the WebBrowser Control events.
第二步:获得IWebBrowser2接口。能看这篇文章的朋友不应该不熟悉这个接口吧?它的获取有多种方式,如果真的不知道,那就直接用基于CHtmlView的单文档程序,嘿嘿,够直接了吧~在本例中,通过一个对话框程序获得IE浏览器中的IWebBrowser2接口的,由于实现较复杂,用到了”oleacc.dll”,如果讨论太多会偏离主题,因此在第三步中只将代码贴出,有兴趣的朋友可以研究一下,如有不懂,我或许可以再出一篇教程。
第三步:通过函数GetIHTMLElement(IWebBrowser2 *pwb2)获得IHTMLElement接口指针,由于这些都是平时IE编程的基础这里就不多说拉:
本例中使用到的html文件的代码如下:
<button>CLICK ME!</button>
哈哈,别怀疑撒,就是这么一句话~当然这是为了测试的方便,否则我就太对不起我PHP程序员的称号啦~~
以下是GetIHTMLElement的实现代码,注意” 获取Internet Explorer_Server句柄”这部分是针对IE8.0的,如果是另外浏览器当然也可以,不过就请你自己获取吧,过程差不多啦!另外请在HandleEventDlg.h中包含:atlbase.h,oleacc.h。
IHTMLElement* CHandleEventDlg::GetIHTMLElementPoint()
{
//获取Internet Explorer_Server句柄
HWND hIEFrame=::FindWindow("IEFrame",NULL);
HWND hFrameTab=::FindWindowEx(hIEFrame,NULL,"Frame Tab",NULL);
HWND hTabWindowClass=::FindWindowEx(hFrameTab,NULL,"TabWindowClass",NULL);
HWND hShellDocObjectView=::FindWindowEx(hTabWindowClass,NULL,"Shell DocObject View",NULL);
HWND hExplorer_Server=::FindWindowEx(hShellDocObjectView,NULL,"Internet Explorer_Server",NULL);
//CString str;
//str.Format("%d",hExplorer_Server);
//::AfxMessageBox(str);
//获取IHTMLDocument2接口
CComQIPtr<IHTMLDocument2>hd2;
UINT uMsg;
uMsg=::RegisterWindowMessage(_T("WM_HTML_GETOBJECT"));
LRESULT lRes;
::SendMessageTimeout(hExplorer_Server,uMsg,0L,0L,SMTO_ABORTIFHUNG,1000,(DWORD*)&lRes);
HINSTANCE hDll=::LoadLibrary(_T("OLEACC.dll"));
LPFNOBJECTFROMLRESULT pfObjectFromLresult=(LPFNOBJECTFROMLRESULT)::GetProcAddress(hDll,"ObjectFromLresult");
pfObjectFromLresult(lRes,IID_IHTMLDocument2,0,(LPVOID*)&hd2);
CComVariant color("black");
hd2->put_bgColor(color);
::FreeLibrary(hDll);
///
CComQIPtr<IHTMLElementCollection>hec;
LRESULT hr=NULL;
CComQIPtr<IHTMLElementCollection>pElemColl;
hd2->get_all(&pElemColl);
IDispatch* pElemDisp = NULL;
IHTMLElement* pElem = NULL;
VARIANT varID,varIdx;
varID.vt=VT_I4;
varID.lVal=0;
varIdx.vt=VT_I4;
varIdx.lVal=0;
hr = pElemColl->item(varID, varIdx, &pElemDisp);
hr = pElemDisp->QueryInterface(IID_IHTMLElement, (void**)&pElem);
return pElem;
}
第四步:调用AfxConnectionAdvise实现接收器与连接点的连接。在对话框中加入一个Button,添加LBUTTONDOWN事件,我们将在这里实现两者的连接,请在HandleEventDlg.h中加入CSink *m_pSink,DWORD m_dwCookie代码如下:
void CHandleEventDlg::OnConnect()
{
m_pElem=GetIHTMLElementPoint();
m_pSink=new CSink();
LPUNKNOWN pUnkSink=m_pSink->GetIDispatch(FALSE);
if(!::AfxConnectionAdvise(m_pElem,DIID_HTMLElementEvents2,pUnkSink,FALSE,&m_dwCookie))
{
::AfxMessageBox("连接失败!");
}
else
{
::AfxMessageBox("连接成功!请点击按钮测试!");
}
}
第五步:在OnDestroy()进行垃圾回收:
void CHandleEventFromIEDlg::OnDestroy()
{
CDialog::OnDestroy();
// TODO: 在此处添加消息处理程序代码
LPUNKNOWN pUnkSink=m_pSink->GetIDispatch(FALSE);
if(::AfxConnectionUnadvise(pElem,DIID_HTMLElementEvents2,pUnkSink,FALSE,m_dwCookie))
{
if(m_pSink!=NULL)
{
delete m_pSink;
m_pSink=NULL;
m_dwCookie=NULL;
}
}
}
这里唠叨一下,在AfxConnectionAdvise内部会调用QueryInterface(IID_IConnectionPointContainer,..)及FindConnectionPoint(),因此只需要把m_pSink直接传进去即可,要是在外部调用这两个函数再传进去就明显会出错拉!
哈,结束拉,现在点击测试吧~注意先把网页打开哦,为了方便没有做异常处理,如果先运行程序的话会使程序崩溃的呀~
最后,我会在近期把工程文件上传,如果你是在别处看到此文章的,那么请回到我的博客,这里将会有下载的链接:http://blog.csdn.net/xiaodao1986/archive/2008/12/31/3672062.aspx,也欢迎在我的博客留言,或也可以给我发邮件zhangwenbo_1986@163.com。想转载的朋友当然也欢迎拉,本来就是想为大家做点事,不过请保留原文出处,这是对作者基本的礼貌吧?嘿嘿