在Windows操作系統上,我們最常見的瀏覽器有兩種:文件瀏覽器(exploer.exe,應用于文件系統)和Internet瀏覽器(iexplore.exe,應用于互聯網資源)。由于這兩個瀏覽器功能強大,而且又與Windows操作系統捆綁銷售,最終也就成為了瀏覽器的標準。但有時候,為了給瀏覽器加入一些新的特性,我們往往會重新設計一個自己的瀏覽器。新的瀏覽器模仿標準瀏覽器的大部分功能,同時加入新特性。這種做法最直觀,但實際上也是相對于微軟的重復勞動,且工作量比較大。其實,使用BHO插件,一切都變得很簡單。
BHO(Browser Help Objects),是實現了特定接口的COM組件。開發好的BHO插件在注冊表特定的位置注冊好后,每當微軟的瀏覽器啟動,BHO實例就會被創建。在瀏覽器工作的工程中,BHO會接收到很多事件,比如瀏覽器瀏覽新的地址、前進或后退、生成新的窗口、瀏覽器退出等等;BHO可以在這些事件的響應中實現與瀏覽器的交互。
下面,我們首先來介紹一下BHO的工作原理。上面我們已經提到,BHO是COM組件,而且一定實現了IObjectWithSite接口。這些組件除了在注冊表中注冊為COM Server外,還必須將它們的CLSID在HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\ CurrentVersion\Explorer\Browser Helper Objects下注冊為子鍵。微軟在設計瀏覽器的時候,已經給這些組件預留了空間。每當瀏覽器啟動時,瀏覽器會首先在上述注冊表位置查看是否有注冊的BHO CLSID;如果有則分別創建一個實例,并對BHO實例進行初始化,建立交互連接。(注:BHO實例只有在創建它的瀏覽器窗口銷毀時才被釋放。)下圖演示了BHO的創建過程:

成功創建的BHO,不僅可以得到各種標準的瀏覽器操作事件,并做出響應;還可以定制瀏覽器的菜單、工具條等界面元素;更或者可以安裝鉤子函數,監視瀏覽器的一舉一動。值得注意的是,使用BHO插件,Internet瀏覽器要求在4.0以上版本;如果是文件瀏覽器,操作系統要求是Windows 95/98/2000或Window NT 4.0以上版本,并且Shell的版本在4.71以上。下面是支持BHO特性的系統一覽表:
Shell版本 |
操作系統版本 |
支持BHO |
4.00 |
Windows 95 and Windows NT 4.0(IE版本為 4.0) |
僅IE4.0 |
4.71 |
Windows 95 and Windows NT 4.0(IE版本為 4.0) |
IE和文件瀏覽器 |
4.72 |
Windows 98 |
IE和文件瀏覽器 |
5.00 |
Windows 2000 |
IE和文件瀏覽器 |
接下去,筆者就來介紹一下如何開發BHO插件,開發環境為VC6.0(使用ATL),安裝Platform SDK中的Internet Development SDK。首先,啟動VC的ATL COM AppWizard,生成一個項目名為BhoPlugin,其余均采用默認設置。接著,我們就來分步詳細闡述。
第一步,增加一個ATL Object到該項目中。VC菜單Insert->New ATL Object…,在彈出的對話框中選擇“Internet Explorer Object”,輸入COM類名(在Short Name后輸入EyeOnIE,其它各項會自動生成)。完成后,我們可以看到CEyeOnIE類有一個基類IObjectWithSiteImpl,這個就是實現IObjectWithSite接口的模版類。
第二步,實現IObjectWithSite的接口方法。在這之前,我們要先定義幾個成員變量:CComQIPtr<IWebBrowser2, &IID_IWebBrowser2> mWebBrowser2,(需要加入#include "ExDisp.h"),用以保存瀏覽器組件的指針;DWORD mCookie,用以保存與瀏覽器的連接ID。IObjectWithSite有兩個接口方法:SetSite和GetSite。我們只需重載SetSite就行了。在EyeOnIE.h中增加函數聲明STDMETHOD(SetSite)(IUnknown *pUnkSite),在EyeOnIE.cpp實現如下:
?1
STDMETHODIMP?CEyeOnIE::SetSite(IUnknown?*pUnkSite)
?2

{
?3
USES_CONVERSION;
?4
?5
if?(pUnkSite)
?6
{
?7
mWebBrowser2?=?pUnkSite;
?8
if?(mWebBrowser2)
?9
{
10
return?RegisterEventHandler(TRUE);
11
}
12
}
13
return?E_FAIL;
14
}
15
16
HRESULT?CEyeOnIE::RegisterEventHandler(BOOL?inAdvise)
17

{
18
CComPtr<IConnectionPoint>?spCP;
19
//?Receives?the?connection?point?for?WebBrowser?events
20
CComQIPtr<IConnectionPointContainer,?&IID_IConnectionPointContainer>?spCPC(mWebBrowser2);
21
HRESULT?hr?=?spCPC->FindConnectionPoint(DIID_DWebBrowserEvents2,?&spCP);
22
if?(FAILED(hr))
23
return?hr;
24
25
if?(inAdvise)
26
{
27
//?Pass?the?event?handlers?to?the?container
28
hr?=?spCP->Advise(reinterpret_cast<IDispatch*>(this),?&mCookie);
29
}
30
else
31
{
32
spCP->Unadvise(mCookie);
33
}
34
return?hr;?
35
} 我們可以看到,SetSite的參數實際上指向的是瀏覽器組件。在SetSite實現中,我們首先保存瀏覽器組件指針,然后將該BHO向瀏覽器注冊為事件處理器。
第三步,實現IDispatch接口方法。事件處理也就在IDispatch::Invoke中實現(各個事件的ID在ExDispID.h中定義)。BHO可能會接收到很多事件,但我們只需要響應我們感興趣的那一部分。首先在EyeOnIE.h中增加該函數的聲明,在EyeOnIE.cpp的實現中,筆者試著響應瀏覽器瀏覽一個地址之前發出的事件DISPID_BEFORENAVIGATE2,以此來實現簡單的網址過濾功能,代碼參考如下:
?1
STDMETHODIMP?CEyeOnIE::Invoke(DISPID?dispidMember,REFIID?riid,?LCID?lcid,?
?2
WORD?wFlags,?DISPPARAMS?*?pDispParams,?
?3
VARIANT?*?pvarResult,EXCEPINFO?*?pexcepinfo,?
?4
UINT?*?puArgErr)
?5

{?
?6
USES_CONVERSION;
?7
?8
if?(!pDispParams)
?9
return?E_INVALIDARG;
10
11
switch?(dispidMember)
12
{
13
//
14
//?The?parameters?for?this?DISPID?are?as?follows:
15
//?[0]:?Cancel?flag?-?VT_BYREF|VT_BOOL
16
//?[1]:?HTTP?headers?-?VT_BYREF|VT_VARIANT
17
//?[2]:?Address?of?HTTP?POST?data?-?VT_BYREF|VT_VARIANT?
18
//?[3]:?Target?frame?name?-?VT_BYREF|VT_VARIANT?
19
//?[4]:?Option?flags?-?VT_BYREF|VT_VARIANT
20
//?[5]:?URL?to?navigate?to?-?VT_BYREF|VT_VARIANT
21
//?[6]:?An?object?that?evaluates?to?the?top-level?or?frame
22
//?WebBrowser?object?corresponding?to?the?event.?
23
//
24
case?DISPID_BEFORENAVIGATE2:
25
{
26
LPOLESTR?lpURL?=?NULL;
27
mWebBrowser2->get_LocationURL(&lpURL);
28
char?*?strurl;
29
if?(pDispParams->cArgs?>=?5?&&?pDispParams->rgvarg[5].vt?==?(VT_BYREF|VT_VARIANT))
30
{
31
CComVariant?varURL(*pDispParams->rgvarg[5].pvarVal);
32
varURL.ChangeType(VT_BSTR);
33
strurl?=?OLE2A(varURL.bstrVal);
34
}
35
if?(strstr(strurl,?"girl.com"))
36
{
37
*pDispParams->rgvarg[0].pboolVal?=?TRUE;
38
::MessageBox(NULL,?_T("該網頁已被禁止!"),_T("Warning"),MB_ICONSTOP);
39
return?S_OK;
40
}
41
break;
42
}
43
44
case?DISPID_NAVIGATECOMPLETE2:
45
break;
46
case?DISPID_DOCUMENTCOMPLETE:
47
break;
48
case?DISPID_DOWNLOADBEGIN:
49
break;
50
case?DISPID_DOWNLOADCOMPLETE:
51
break;
52
case?DISPID_NEWWINDOW2:
53
break;
54
case?DISPID_QUIT:
55
RegisterEventHandler(FALSE);
56
break;
57
default:
58
break;
59
}
60
61
return?S_OK;
62
}
我們看到,當用戶瀏覽的新地址包含"girl.com"字符的時候,瀏覽器就會彈出一個警告對話框,并且停止進一步的動作。另外值得注意的是,在DISPID_QUIT事件(瀏覽器將要退出)的響應中,我們將BHO事件處理器進行了注銷。
第四步,因為BHO可能會被文件瀏覽器加載。如果我們不想這樣,我們就要在DllMain中對加載者進行判斷,參考如下:
?1
extern?"C"
?2
BOOL?WINAPI?DllMain(HINSTANCE?hInstance,?DWORD?dwReason,?LPVOID?/**//*lpReserved*/)
?3

{
?4
if?(dwReason?==?DLL_PROCESS_ATTACH)
?5
{
?6
//?Check?who's?loading?us.?
?7
//?If?it's?Explorer?then?"no?thanks"?and?exit
?8
TCHAR?pszLoader[MAX_PATH];
?9
GetModuleFileName(NULL,?pszLoader,?MAX_PATH);
10
_tcslwr(pszLoader);
11
if?(_tcsstr(pszLoader,?_T("explorer.exe")))?
12
return?FALSE;
13
14
_Module.Init(ObjectMap,?hInstance,?&LIBID_BHOPLUGINLib);
15
DisableThreadLibraryCalls(hInstance);
16
}
17
else?if?(dwReason?==?DLL_PROCESS_DETACH)
18
_Module.Term();
19
return?TRUE;?//?ok
20
} 最后,別忘了修改注冊表文件,追加BHO的注冊信息。在EyeOnIE.rgs文件的下面增加如下代碼:
?1
HKLM
?2

{
?3
SOFTWARE
?4
{
?5
Microsoft
?6
{
?7
Windows
?8
{
?9
CurrentVersion
10
{
11
Explorer
12
{
13
'Browser?Helper?Objects'
14
{
15
{6E28339B-7A2A-47B6-AEB2-46BA53782379}
16
}
17
}
18
}
19
}
20
}
21
}
22
}
注意,{6E28339B-7A2A-47B6-AEB2-46BA53782379}是筆者這個BHO的CLSID,如果你自己開發BHO,這里應該正確填寫你的CLSID。
好了,一個簡單的BHO開發完成了。(可以到本人的個人主頁 http://hqtech.nease.net 下載實例源代碼。)BHO插件可以實現的功能還有很多,比如網頁內容分析、IE界面定制等等。作為總結,筆者還要提醒讀者一點的是,如果不想讓BHO起作用了,可以注銷該插件,如下格式:regsvr32 /u yourpath\yourbho.dll,或者直接在注冊表中將“Browser Helper Objects”目錄下注冊的CLSID刪掉。