前幾天在《 一個基于 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 。想轉載的朋友當然也歡迎拉,本來就是想為大家做點事,不過請保留原文出處,這是對作者基本的禮貌吧?嘿嘿
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
