---- 1.引言
---- 如何實(shí)現對IE瀏覽器中對象的操作是一個(gè)很有實(shí)際意義問(wèn)題,通過(guò)和IE綁定的DLL我們可以記錄IE瀏覽過(guò)的網(wǎng)頁(yè)的順序,分析用戶(hù)的使用行為和模式。我們可以對網(wǎng)頁(yè)的內容進(jìn)行過(guò)濾和翻譯,可以自動(dòng)填寫(xiě)網(wǎng)頁(yè)中經(jīng)常需要用戶(hù)填寫(xiě)的Form內容等等,我們所有的例子代碼都是通過(guò)VC來(lái)表示的,采用的原理是通過(guò)和IE對象的接口的交互來(lái)實(shí)現對IE的訪(fǎng)問(wèn)。實(shí)際上是采用COM的技術(shù),我們知道COM是和語(yǔ)言無(wú)關(guān)的一種二進(jìn)制對象交互的模式,所以實(shí)際上我們下面所描述的內容都可以用其他的語(yǔ)言來(lái)實(shí)現,比如VB,DELPHI,C++ Builder等等。
---- 2.IE實(shí)例遍歷實(shí)現
---- 首先我們來(lái)看系統是如何知道當前有多少個(gè)IE的實(shí)例在運行。
---- 我們知道在Windows體系結構下,一個(gè)應用程序可以通過(guò)操作系統的運行對象表來(lái)和這些應用的實(shí)例進(jìn)行交互。但是IE當前的實(shí)現機制是不在運行對象表中進(jìn)行注冊,所以需要采用其他的方法。我們知道可以通過(guò)ShellWindows集合來(lái)代表屬于shell的當前打開(kāi)的窗口的集合,而IE就是屬于shell的一個(gè)應用程序。
---- 下面我們描述一下用VC實(shí)現對當前 IE實(shí)例的進(jìn)行遍歷的方法。IShellWindows是關(guān)于系統shell的一個(gè)接口,我們可以定義一個(gè)如下的接口變量:
SHDocVw::IShellWindowsPtr m_spSHWinds;
然后創(chuàng )建變量的實(shí)例:
m_spSHWinds.CreateInstance
(__uuidof(SHDocVw::ShellWindows));
通過(guò)IShellWindows接口的方法GetCount
可以得到當前實(shí)例的數目:
long nCount = m_spSHWinds- >GetCount();
通過(guò)IShellWindows接口的方法Item
可以得到每一個(gè)實(shí)例對象
IDispatchPtr spDisp;
_variant_t va(i, VT_I4);
spDisp = m_spSHWinds->Item(va);
然后我們可以判斷實(shí)例對象是不是
屬于IE瀏覽器對象,通過(guò)下面的語(yǔ)句實(shí)現:
SHDocVw::IWebBrowser2Ptr spBrowser(spDisp);
assert(spBrowser != NULL)
----在得到了IE瀏覽器對象以后,我們可以調用IWebBrowser2Ptr接口的方法來(lái)得到當前的文檔對象的指針: MSHTML::IHTMLDocument2Ptr spDoc(spBrowser->GetDocument());
---- 然后我們就可以通過(guò)這個(gè)接口對這個(gè)文檔對象進(jìn)行操作,比如通過(guò)Gettitle得到文檔的標題。
---- 我們在瀏覽網(wǎng)絡(luò )的時(shí)候,一般總會(huì )同時(shí)開(kāi)很多IE的實(shí)例,如果這些頁(yè)面都是很好的話(huà),我們可能想保存在硬盤(pán)上,這樣,我們需要對每一個(gè)實(shí)例進(jìn)行保存,而如果我們采用上面的原理,我們可以得到每一個(gè)IE的實(shí)例及其網(wǎng)頁(yè)對象的接口,這樣就可以通過(guò)一個(gè)簡(jiǎn)單的程序來(lái)批量的保存當前的所有打開(kāi)的網(wǎng)頁(yè)。采用上面介紹的方法實(shí)現了對當前IE實(shí)例的遍歷,但是我們希望得到每一個(gè)IE實(shí)例所產(chǎn)生的事件,這就需要通過(guò)DLL的機制來(lái)實(shí)現。
---- 3.和IE相綁定的DLL的實(shí)現
---- 我們介紹一下如何建立和IE進(jìn)行綁定的DLL的實(shí)現的過(guò)程。為了和IE的運行實(shí)例進(jìn)行綁定,我們需要建立一個(gè)能夠和每一個(gè)IE實(shí)例進(jìn)行綁定的DLL。IE的啟動(dòng)過(guò)程是這樣的,當每一個(gè)IE的實(shí)例啟動(dòng)的時(shí)候,它都會(huì )在注冊表中去尋找這個(gè)的一個(gè)CLSID,具體的注冊表的鍵位置為:
HKEY_LOCALL_MACHINE\SOFTWARE\Microsoft\Windows
\CurrentVersion\Explorer\Browser Helper Objects
---- 當在這個(gè)鍵位置下存在CLSIDs的時(shí)候,IE會(huì )通過(guò)使用CoCreateInstance()方法來(lái)創(chuàng )建列在該鍵位置下的每一個(gè)對象的實(shí)例。注意對象的CLSIDs必須用子鍵而非名字值的形式表現,比如{DD41D66E-CE4F-11D2-8DA9-00A0249EABF4} 就是一個(gè)有效的子鍵。我們使用DLL的形式而非EXE的形式的原因是因為DLL和IE實(shí)例運行在同一個(gè)進(jìn)程空間里面。每一個(gè)這種形式的DLL必須實(shí)現接口IObjectWithSite,其中方法SetSite必須被實(shí)現。通過(guò)這個(gè)方法,我們自己的DLL就可以得到一個(gè)指向IE COM對象的IUnknown的指針,實(shí)際上通過(guò)這個(gè)指針我們就可以通過(guò)COM對象中的方法QueryInterface來(lái)遍歷所有可以得到的接口,這是COM的基本的機制。當然我們需要的只是IWebBrowser2這個(gè)接口。
---- 實(shí)際上我們建立的是一個(gè)COM對象,DLL只不過(guò)是COM對象的一種表現形式。我們建立的COM對象需要建立和實(shí)現的方法有:
----1. IOleObjectWithSite接口的方法SetSite必須實(shí)現。實(shí)際上IE實(shí)例通過(guò)這個(gè)方法向我們的COM對象傳遞一個(gè)接口的指針。假設我們有一個(gè)接口指針的變量,不妨設為:
----CComQIPtr< IWebBrowser2, &IID_IWebBrowser2 > m_myWebBrowser2;
---- 我們就可以在方法SetSite中把這個(gè)傳進(jìn)來(lái)的接口指針賦給m_myWebBrowser2。 2. 在我們得到了指向IE COM對象的接口后,我們需要把自己的DLL和IE實(shí)例所發(fā)生的事件相關(guān)連,為了實(shí)現這個(gè)目的,需要介紹兩個(gè)接口:
----(1) IConnectionPointContainer。這里使用這個(gè)接口的目的是用來(lái)根據它得到的IID來(lái)建立和DLL的一個(gè)特定的連接。比如我們可以進(jìn)行如下的定義:
CComQIPtr< IConnectionPointContainer,
&IID_IConnectionPointContainer >
spCPContainer(m_myWebBrowser2);
----然后,我們需要把所有IE中發(fā)生的事件和我們的DLL進(jìn)行通訊,可以使用 IConnectPoint。
----(2) IConnectPoint。通過(guò)這個(gè)接口,客戶(hù)可以對連接的對象開(kāi)始或者是終止一個(gè)advisory循環(huán)。IConnectPoint有兩個(gè)主要的方法,一個(gè)為Advice,另一個(gè)為Unadvise。對于我們的應用來(lái)說(shuō),Advise是用來(lái)在每一個(gè)IE發(fā)生的事件和DLL之間建立一個(gè)通道。而Unadvise就是用來(lái)終止以前用Advise建立的通知關(guān)系。比如我們可以定義IConnectPoint接口如下: CComPtr< IConnectionPoint > spConnectionPoint;
---- 然后,我們要使所有在IE實(shí)例中發(fā)生的事件和我們的DLL相關(guān),可以使用 如下的方法:
hr = spCPContainer->FindConnectionPoint(
DIID_DWebBrowserEvents2, &spConnectionPoint);
----然后我們通過(guò)IConnectPoint接口的方法Advice使每當IE有一個(gè)新的事件發(fā)生的時(shí)候,都能夠讓我們的DLL知道??梢杂萌缦碌恼Z(yǔ)句實(shí)現:
hr = spConnectionPoint- >Advise(
(IDispatch*)this, &m_dwIDCode);
----在把IE實(shí)例中的事件和我們的DLL之間建立聯(lián)系以后,我們可以通過(guò)IDispatch接口的Invoke()方法來(lái)處理所有的IE的事件。
----3. IDispatch接口的Invoke()方法。IDispatch是從IUnknown中繼承的一個(gè)接口的類(lèi)型,通過(guò)COM接口提供的任何服務(wù)都可以通過(guò)IDispatch接口來(lái)實(shí)現。IDispatch::Invoke的工作方式同vtbl幕后的工作方式是類(lèi)似的,Invoke將實(shí)現一組按索引來(lái)訪(fǎng)問(wèn)的函數,我們可以對Invoke方法進(jìn)行動(dòng)態(tài)的定制以提供不同的服務(wù)。Invoke方法的表示如下:
STDMETHOD(Invoke)(DISPID dispidMember,REFIID
riid, LCID lcid, WORD wFlags,
DISPPARAMS * pdispparams, VARIANT * pvarResult,
EXCEPINFO * pexcepinfo, UINT * puArgErr);
----其中,DISPID是一個(gè)長(cháng)整數,它標識的是一個(gè)函數。對于IDispatch的某一個(gè)特定的實(shí)現,DISPID都是唯一的。IDispatch的每一個(gè)實(shí)現都有其自己的IID,這里dispidMemeber實(shí)際上是可以認為是和IE實(shí)例所發(fā)生的每一個(gè)事件相關(guān)的方法,比如:DISPID_BEFORENAVIGATE2,DISPID_NAVIGATECOMPLETE2等等。 這個(gè)方法中另外一個(gè)比較重要的參數是DISPPARAMS,它的結構如下:
typedef struct tagDISPPARAMS
{
VARIANTARG* rgvarg;
//VARIANTARG是同VARAIANT相同的,可以在
//OAIDL.IDL中找到。所以實(shí)際上rgvarg是一個(gè)參數數
//組
DISPID* rgdispidNameArgs; //命名參數的DISPID
unsigned int cArgs; //表示數組中元素的個(gè)數
unsigned int CnameArgs; //命名元素的個(gè)數
}DISPPARAMS
----要注意的是每一個(gè)參數的類(lèi)型都是VARIANTARG,所以在IE和我們DLL之間可以傳遞的參數類(lèi)型的數目是有限的。只有那些能夠被放到VARIANTARG結構中的類(lèi)型才可以通過(guò)調度接口進(jìn)行傳遞。 比如對于事件DISPID_NAVIGATECOMPLETE2來(lái)說(shuō):第一個(gè)參數表示IE在訪(fǎng)問(wèn)的URL的值,類(lèi)型是VT_BYREF|VT_VARIANT。注意DISPID_NAVIGATECOMPLETE2等DISPID已經(jīng)在VC中被定義,我們可以直接進(jìn)行使用。 如上說(shuō)述,我們在方法Invoke中可以得到所有IE實(shí)例所發(fā)生的事件,我們可以把這些數據放到文件中進(jìn)行事后的分析,也可以放到一個(gè)列表框中實(shí)時(shí)的顯示。
---- 4.微軟的HTML文檔對象模型和應用分析
---- 下面我們來(lái)看如何得到網(wǎng)頁(yè)文檔的接口:網(wǎng)頁(yè)文檔的接口為IHTMLDocument2,可以通過(guò)調用IE COM對象的get_Document方法來(lái)得到網(wǎng)頁(yè)的接口。使用如下的語(yǔ)句:
hr = m_spWebBrowser2- >get_Document(&spDisp);
CComQIPtr< IHTMLDocument2,
&IID_IHTMLDocument2 > spHTML;
spHTML = spDisp;
---- 這樣我們就得到了網(wǎng)頁(yè)對象的接口,然后我們就可以對網(wǎng)頁(yè)進(jìn)行分析,比如通過(guò)IHTMLDocument2提供的方法get_URL我們可以得到和該網(wǎng)頁(yè)相關(guān)的URL的地址值,通過(guò)get_forms方法可以該網(wǎng)頁(yè)中所有的Form對象的集合。實(shí)際上W3C組織已經(jīng)制定了一個(gè)DOM(Document Objec Model)標準,當然這個(gè)標準不僅僅是針對HTML,同時(shí)還是針對XML制定的。W3C組織只是定義了網(wǎng)頁(yè)對象的接口,不同的公司可以采用不同的語(yǔ)言和方法進(jìn)行具體的實(shí)現。按照W3C組織定義的網(wǎng)頁(yè)對象被認為是動(dòng)態(tài)的,即用戶(hù)可以動(dòng)態(tài)的對網(wǎng)頁(yè)對象里面所包含的每一個(gè)對象進(jìn)行操作。這里的對象可以是指一個(gè)輸入框,也可以是圖象和聲音等對象。同時(shí)按照W3C的正式文檔的說(shuō)明,網(wǎng)頁(yè)對象是可以動(dòng)態(tài)增加和刪除的。事實(shí)上,很少有廠(chǎng)商實(shí)現了DOM定義的所有功能。微軟對網(wǎng)頁(yè)對象的定義也基本上是按照這個(gè)標準實(shí)現的。但是當前的接口還不支持動(dòng)態(tài)的增加和刪除元素,但是可以對網(wǎng)頁(yè)中的基本元素進(jìn)行屬性的修改。比如IHTMLElementCollection表示網(wǎng)頁(yè)中一些基本的元素的集合,IHTMLElement表示網(wǎng)頁(yè)中的一個(gè)基本的元素。而象IHTMLOptionElement接口就表示一個(gè)特定的元素Option?;镜脑囟加衧etAttribute和geAttribute方法來(lái)動(dòng)態(tài)的設置和得到元素的名稱(chēng)和值。
---- 較為常見(jiàn)的一個(gè)應用是我們能夠分析網(wǎng)頁(yè)中是否有需要填寫(xiě)的Forms,如果這個(gè)網(wǎng)址的Forms以前已經(jīng)填寫(xiě)過(guò)而且數據我們已經(jīng)保存下來(lái)的話(huà),我們就可以把數據自動(dòng)放到和該URL下的Forms的相關(guān)的位置中去。另外,我們可以總結網(wǎng)頁(yè)上需要填寫(xiě)的Form的數據項,先對這些數據項進(jìn)行賦值,以后碰到有相同的數據項的時(shí)候就自動(dòng)把我們賦值的內容填寫(xiě)進(jìn)去。實(shí)際上Form是對象,Form中包含的元素,比如INPUT,OPTION,SELECT等類(lèi)型的輸入元素都是對象。
---- 另外一個(gè)可以想到的應用是自動(dòng)對網(wǎng)頁(yè)中的文本進(jìn)行翻譯,因為我們可以修改網(wǎng)頁(yè)中任何對象的屬性,所以我們可以把里面不屬于本國語(yǔ)言的部分自動(dòng)翻譯成本國語(yǔ)言,當然真正的實(shí)現還要靠自然語(yǔ)言理解方面技術(shù)的突破,但是IE瀏覽器的接口和對象的形式使我們能夠靈活的控制整個(gè)IE,無(wú)論是從事件對象還是到網(wǎng)頁(yè)對象。
---- 5.小結
---- 上面我們分析了如何得到所有IE的實(shí)例,同時(shí)介紹了和IE實(shí)例相捆綁的DLL的詳細的實(shí)現機制,同時(shí)對網(wǎng)頁(yè)的對象化進(jìn)行了分析。并且介紹了幾個(gè)相關(guān)的應用和實(shí)現的方法及存在的技術(shù)問(wèn)題。IE是一個(gè)組件化的以COM為基礎的瀏覽器,它具有強大的功能,同時(shí)為應用開(kāi)發(fā)者留下了廣闊的空間,當然它也存在體積比較大,速度相對比較慢的缺點(diǎn)。但是它的體系結構代表了微軟先進(jìn)的創(chuàng )新的技術(shù),因此具有強大的生命力。