摘要
本文說明了如何使用 Microsoft Visual Studio
創建一個簡單的
浏覽器幫助程序對象
(BHO)
即實現 IObjectWithSite 接口並將其自身附加到 Internet Explorer 的一種
組件對象模型
(COM) 對象
本文逐步說明了如何創建入門級 BHO
首先
BHO 會在 Internet Explorer 加載文檔時顯示消息
大家好!
然後
BHO 被擴展為從已加載頁面刪除圖像
本文面向的是想要了解如何擴展浏覽器功能以及如何為 Internet Explorer 創建 Web 開發人員工具的開發人員(本文還包含指向英文網頁的鏈接)
簡介
本文憑借 Microsoft Visual Studio 和活動模板庫(ATL) 來開發使用 C++ 的 BHO我們之所以決定使用 ATL是因為它方便地實現了我們可以按需進行擴展的基本樣板還有其他方法可供用於創建 BHO(例如使用Microsoft 基礎類(MFC) 或 Win API 和 COM)但 ATL 是為我們自動處理許多細節的輕型庫包括建立含有 BHO 類標識符 (CLSID) 的注冊表
ATL 的另一個優勢在於它的 COM 感知智能指針類(例如CComPtr 和 CComBSTR)這些類可管理 COM 對象的生命周期例如CComPtr 在賦值時會調用 AddRef而在對象被銷毀或超出范圍時會調用 Release智能指針簡化了代碼並且有助於避免內存洩漏當在單個方法范圍內使用時它們的穩定性和可靠性尤為有用
本文的第一部分向您逐步介紹了如何實現簡單的 BHO 並驗證它是否由 Internet Explorer 加載接下來的部分將說明如何將 BHO 連接到浏覽器事件最後一部分將介紹與更改網頁外觀的 DHTML 文檔對象模型 (DOM) 的簡單交互
概述
到底什麼是浏覽器幫助程序對象 (BHO)?簡言之BHO 是將自定義功能添加到 Internet Explorer 的輕型 DLL 擴展BHO 還可以將功能添加到 Windows 資源管理器外殼程序(盡管這並不常見也不是本文重點)
BHO 通常並不提供其自身的任何用戶界面 (UI)它們而是通過在後台響應浏覽器事件和用戶輸入數據來發揮作用例如BHO 可以攔截彈出窗口自動填充窗體或為鼠標手勢添加支持有一種常見誤解認為工具欄擴展項需要 BHO;但如果將 BHO 與工具欄配合使用則可以實現更豐富的用戶體驗
注意 BHO 對於最終用戶和開發人員同樣都是便捷的工具;但由於 BHO 被賦予了對浏覽器和 Web 內容的相當大的控制能力並且它們通常都處於未檢測的狀態因此用戶應十分謹慎地從可靠來源獲取和安裝 BHO
BHO 的生命周期與它所交互的浏覽器實例的生命周期相等在 Internet Explorer 和早期版本中這意味著為每個新的頂層窗口都創建(和銷毀)一個新 BHO另一方面Internet Explorer 會為每個選項卡都創建和銷毀一個新 BHOBHO 不是由承載 WebBrowser 控件的其他應用程序加載也不是由 HTML 對話框之類的窗口加載
BHO 的主要要求是實現 IObjectWithSite 接口此接口提供了一個方法(即 SetSite)此方法方便了與 Internet Explorer 的初始通信並會在其將要釋放時通知 BHO我們實現此接口然後將 BHO 的 CLSID 添加到注冊表中由此創建一個簡單的浏覽器擴展
建立項目
通過 Microsoft Visual Studio 創建 BHO 項目
在文件菜單上單擊新建項目
隨即出現新建項目對話框此對話框將列出 Visual Studio 可以創建的應用程序類型
在 Visual C++ 節點下選中ATL(如果它未被選中)然後從 Visual C++ 項目類型中選擇ATL 項目將項目命名為HelloWorld並使用默認位置單擊確定
在ATL 項目向導中確保服務器類型為動態鏈接庫 (DLL)然後單擊完成
此時Visual Studio 已為 DLL 創建了樣板現在我們將添加實現 BHO 的 COM 對象
在解決方案資源管理器面板上右鍵單擊該項目然後從添加子菜單中選擇類
選中ATL 簡單對象然後單擊添加
隨即出現ATL 簡單對象向導
在ATL 簡單對象向導的名稱中鍵入HelloWorldBHO以作為短名稱
余下的名稱將自動填充
在ATL 簡單對象向導的選項中選中線程模型下的Apartment聚合下的否接口下的雙重以及支持下的IobjectWithSite
單擊完成
以下文件將作為此項目的一部分創建
;HelloWorldBHOh – 此頭文件包含 BHO 的類定義
;HelloWorldBHOcpp – 此源文件是項目的主文件並且包含 COM 對象
;HelloWorldcpp – 此源文件用於實現通過 DLL 提供 COM 對象的導出
;HelloWorldidl – 此源文件可用於定義自定義 COM 接口對於本文我們將不更改此文件
;HelloWorldrgs – 此資源文件包含注冊和取消注冊 DLL 時編寫和刪除的注冊表項
實現基本要素
ATL 項目向導提供了 SetSite 的默認實現盡管 IObjectWithSite 的接口合約暗示了此方法可以在必要時被反復調用但確切來說Internet Explorer 只調用此方法兩次;一次用於建立連接另一次則是在浏覽器退出時特別要提的是我們 BHO 中的 SetSite 實現將執行以下操作
;存儲對站點的引用在初始化期間浏覽器將 IUnknown 指針傳遞給頂層 WebBrowser 控件然後 BHO 將對它的引用存儲在一個專用成員變量中
;釋放目前被占用的站點指針Internet Explorer 傳遞 NULL 時BHO 必須釋放所有接口引用並且斷開與浏覽器的連接
在處理 SetSite 的過程中BHO 將根據需要執行其他初始化和非初始化例如您可以建立與浏覽器的連接點以便接收浏覽器事件
HelloWorldBHOh
在 Visual Studio 的解決方案資源管理器中雙擊打開 HelloWorldBHOh
首先包含 shlguidh此文件定義了 IWebBrowser 的接口標識符和稍後在項目中使用的事件
#include // IID_IWebBrowserDIID_DWebBrowserEvents 等
接下來在 CHelloWorldBHO 類的公共部分聲明 SetSite
STDMETHOD(SetSite)(IUnknown *pUnkSite);
STDMETHOD 宏是一個將方法標記為虛擬方法並且確保其具有適用於公共 COM 接口的調用約定的 ATL 約定它有助於區分 COM 接口和該類中可能存在的其他公共方法實現成員方法時同樣也會使用 STDMETHODIMP 宏
最後在類聲明的專用部分中聲明某成員變量以存儲浏覽器站點
以下是引用片段
private:
CComPtr m_spWebBrowser;
HelloWorldBHOcpp
現在切換到 HelloWorldBHOcpp 並為 SetSite 插入以下代碼
STDMETHODIMP CHelloWorldBHO::SetSite(IUnknown* pUnkSite)
{
if (pUnkSite != NULL)
{
// 緩存指向 IWebBrowser 的指針
pUnkSite>QueryInterface(IID_IWebBrowser (void**)&m_spWebBrowser);
}
else
{
// 在此釋放緩存的指針和其他資源
m_spWebBrowserRelease();
}
// 返回基類實現
return IObjectWithSiteImpl::SetSite(pUnkSite);
}
初始化期間浏覽器將傳遞一個對其頂層 IWebBrowser 接口(我們對其進行緩存處理)的引用非初始化期間浏覽器將傳遞 NULL為避免內存洩漏和循環引用計數此時釋放所有指針和資源非常重要最後我們調用基類實現以便它可以履行接口合約的其余部分
HelloWorldcpp
加載 DLL 後系統將通過 DLL_PROCESS_ATTACH 通知調用 DllMain 函數由於 Internet Explorer 大量使用多線程因此對 DllMain 的頻繁的 DLL_THREAD_ATTACH 和 DLL_THREAD_DETACH 通知會降低擴展和浏覽器進程的整體性能既然該 BHO 不需要線程級的跟蹤我們可以在 DLL_PROCESS_ATTACH 通知期間調用 DisableThreadLibraryCalls 以避免新線程通知的額外開銷
在 HelloWorldcpp 中如下編寫 DllMain 函數的代碼
以下是引用片段
extern C BOOL WINAPI DllMain(HINSTANCE hInstance DWORD dwReason LPVOID lpReserved)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
DisableThreadLibraryCalls(hInstance);
}
return _AtlModuleDllMain(dwReason lpReserved);
}
注冊 BHO
剩下要做的只是將 BHO 的 CLSID 添加到注冊表中此條目會將 DLL 標記為浏覽器幫助程序對象並使 Internet Explorer 在啟動時加載 BHOVisual Studio 可在生成項目時注冊 CLSID
注意 在 Windows Vista 上Visual Studio 需要提升的特權才能與注冊表進行交互請確保通過在開始菜單中右鍵單擊 Microsoft Visual Studio 並選擇以管理員身份運行來啟動開發環境
此 BHO 的 CLSID 可在 HelloWorldidl 中找到(位於如下所示的代碼塊中)
以下是引用片段
importlib(stdoletlb);
[
uuid(DFEECDCBCDADDD)
helpstring(HelloWorldBHO Class)
]
請注意此文件包含三個 GUID;我們需要的是用於類的 CLSID而不是用於庫的 CLSID 或接口 ID
創建自行注冊的 BHO
從 Visual Studio 中的解決方案資源管理器打開 HelloWorldrgs
將以下代碼添加到文件末尾
以下是引用片段
HKLM {
NoRemove SOFTWARE {
NoRemove Microsoft {
NoRemove Windows {
NoRemove CurrentVersion {
NoRemove Explorer {
NoRemove 浏覽器幫助程序對象 {
ForceRemove {DFEECDCBCDADDD} = s HelloWorldBHO {
val NoExplorer = d
}
}
}
}
}
}
}
}
將上述 ForceRemove 後面的 GUID 替換為 BHO 的 CLSID(可在 HelloWorldidl 中找到)
切勿替換大括號
保存文件然後重新生成解決方案(按 F)
Visual Studio 將自動注冊該對象
NoRemove 關鍵字表示取消注冊 BHO 時將不刪除該注冊表項除非您指定了此關鍵字否則將刪除空的注冊表項ForceRemove 關鍵字表示將刪除該注冊表項以及它所包含的任何值和子項ForceRemove 還將導致在注冊 BHO 後重新創建該注冊表項(如果它已存在)
既然此 BHO 專用於 Internet Explorer那麼我們指定 NoExplorer 值以防止 Windows Explorer 加載它值和類型是什麼都不重要只要 NoExplorer 條目存在Windows Explorer 就不會加載 BHO
現在您就可以從 Visual Studio 中的生成菜單生成解決方案
進行試用
為了進行快速測試請在 SetSite 中設置一個斷點然後按 F 啟動調試程序當出現調試會話的可執行文件對話框時選擇默認的 Web 浏覽器然後單擊確定如果 Internet Explorer 不是您的默認浏覽器則可以浏覽查找可執行文件
注意 在 Windows Vista 上Internet Explorer 的保護模式功能將啟動另一個進程然後退出這樣會給調試帶來一點難度您可以通過以下兩種方式輕松關閉當前會話的保護模式從管理進程(例如 Visual Studio)啟動浏覽器或者創建一個本地 HTML 文件並將其指定為 Internet Explorer 的命令行參數
浏覽器啟動時將加載 BHO 的 DLL命中斷點時請注意是否設置了 pUnkSite 參數再次按 F 以繼續加載主頁
關閉浏覽器以驗證是否通過 NULL 再次調用了 SetSite
對事件做出響應
既然已經確認了 Internet Explorer 可以加載和運行 BHO那就讓我們在所舉示例的基礎上再深入一些將 BHO 擴展到響應浏覽器事件在本部分中我們介紹如何使用 ATL 為 DocumentComplete(在頁面加載後顯示一個消息框)實現一個事件處理程序
為接到事件通知BHO 建立一個與浏覽器之間的連接點;為響應這些事件它將實現 IDispatch根據 DocumentComplete 的文檔該事件有兩個參數pDisp(IDispatch 的指針)和 pUrl這些參數將作為事件的一部分傳遞給 IDispatch::Invoke;但手動析取這些事件參數並非一項簡單的任務並且易於出錯幸好 ATL 提供了一個默認實現可以幫助簡化這個事件處理邏輯
HelloWorldBHOh
首先通過包含 exdispidh(為浏覽器事件定義調度 ID)處理 HelloWorldBHOh
#include // DISPID_DOCUMENTCOMPLETE 等
接下來從 IDispEventImpl 基類進行派生該基類為處理事件提供了除 Invoke 之外的另一個簡單安全的替代方法IDispEventImpl 與事件匯映射配合工作以將事件路由到相應的處理程序函數我們明確說明想要使用以下類定義(突出顯示)處理由 DWebBrowserEvents 接口定義的事件
以下是引用片段
class ATL_NO_VTABLE CHelloWorldBHO :
public CComObjectRootEx
public CComCoClass
public IObjectWithSiteImpl
public IDispatchImpl
public IDispEventImpl< CHelloWorldBHO &DIID_DWebBrowserEvents &LIBID_SHDocVw >
接下來添加將事件路由到新的 OnDocumentComplete 事件處理程序方法的 ATL 宏該事件處理程序方法采用的是 DocumentComplete 事件所定義的相同參數和順序將以下代碼放置到該類的公共部分
以下是引用片段
BEGIN_SINK_MAP(CHelloWorldBHO)
SINK_ENTRY_EX( DIID_DWebBrowserEvents DISPID_DOCUMENTCOMPLETE OnDocumentComplete)
END_SINK_MAP()
// DWebBrowserEvents
void STDMETHODCALLTYPE OnDocumentComplete(IDispatch *pDisp VARIANT *pvarURL);
提供給 SINK_ENTRY_EX 宏 () 的數字指的是 IDispEventImpl 類定義的第一個參數在必要時用於區分來自不同接口的事件另請注意不能從該事件處理程序返回值;這是因為 Internet Explorer 無論怎樣都會忽略從 Invoke 返回的值
最後添加一個專用成員變量以跟蹤各對象是否已建立了與浏覽器的連接
以下是引用片段
private:
BOOL m_fAdvised;
HelloWorldBHOcpp
要通過事件映射將事件處理程序連接到浏覽器可在處理 SetSite 期間調用 DispEventAdvise同樣使用 DispEventUnadvise 斷開連接
以下是 SetSite 的新實現
以下是引用片段
STDMETHODIMP CHelloWorldBHO::SetSite(IUnknown* pUnkSite)
{
if (pUnkSite != NULL)
{
// 緩存指向 IWebBrowser 的指針
HRESULT hr = pUnkSite>QueryInterface(IID_IWebBrowser (void **)&m_spWebBrowser);
if (SUCCEEDED(hr))
{
// 注冊以從 DWebBrowserEvents 中匯集事件
hr = DispEventAdvise(m_spWebBrowser);
if (SUCCEEDED(hr))
{
m_fAdvised = TRUE;
}
}
}
else
{
// 取消注冊事件匯
if (m_fAdvised)
{
DispEventUnadvise(m_spWebBrowser);
m_fAdvised = FALSE;
}
// 在此釋放緩存的指針和其他資源
m_spWebBrowserRelease();
}
// 調用基類實現
return IObjectWithSiteImpl::SetSite(pUnkSite);
}
最後添加一個簡單的 OnDocumentComplete 事件處理程序
以下是引用片段
void STDMETHODCALLTYPE CHelloWorldBHO::OnDocumentComplete(IDispatch *pDisp VARIANT *pvarURL)
{
// 從站點檢索頂級窗口
HWND hwnd;
HRESULT hr = m_spWebBrowser>get_HWND((LONG_PTR*)&hwnd);
if (SUCCEEDED(hr))
{
// 加載頁面時輸出消息框
MessageBox(hwnd L大家好! LBHO MB_OK);
}
}
請注意消息框會將站點的頂層窗口用作其父窗口而不僅僅是通過該參數傳遞 NULL在 Internet Explorer 中NULL 父窗口並不阻止應用程序也就是說在消息框等待用戶輸入時用戶可以繼續與浏覽器交互在某些情況下這會導致浏覽器掛起或崩潰在 BHO 需要顯示 UI 的這種少見情況下應始終通過指定指向父窗口的句柄來確保該對話框為應用程序模態
再一次試用
通過按 F 再次啟動 Internet Explorer文檔加載後BHO 將顯示其消息
繼續浏覽以觀察消息框出現的時間及頻率請注意不僅在加載頁面時會顯示 BHO 警告在通過單擊上一步按鈕重新加載該頁面時也會顯示 BHO 警告;但在單擊刷新按鈕時不會顯示該警告在 Internet Explorer 中對於每個新的選項卡都會顯示該消息框
該事件在頁面被下載和解析後激發但是在 windowonload 事件觸發之前激發在有多個框架的情況下該事件將激發多次結束時後面跟隨的是頂層框架在隨後的代碼中通過將事件的 pDisp 參數所傳遞的對象與在 SetSite 中進行緩存處理的頂層浏覽器進行比較來檢測出這一系列事件的最後事件
操作 DOM
以下 JavaScript 代碼演示了 DOM 的基本操作它通過將圖像的樣式對象的 display 屬性設置為none在網頁上隱藏圖像
以下是引用片段
function RemoveImages(doc)
{
var images = docimages;
if (images != null)
{
for (var i = ; i < imageslength; i++)
{
var img = em(i);
imgstyledisplay = none;
}
}
}
在最後這部分中我們將說明如何以 C++ 實現這個基本邏輯
HelloWorldBHOh
首先打開 HelloWorldBHOh 並將 mshtmlh 包含在內該頭文件定義了使用 DOM 時所需的接口
#include // DOM 接口
接下來定義專用成員方法以包含上述 JavaScript 的 C++ 實現
private:
void RemoveImages(IHTMLDocument *pDocument);
HelloWorldBHOcpp
現在OnDocumentComplete 事件處理程序要完成兩個新任務首先它將緩存處理後的 WebBrowser 指針與激發事件的對象進行比較;如果兩者相等則該事件用於頂層窗口並且文檔也完全加載其次它檢索一個指向 document 對象的指針並將其傳遞給 RemoveImages
以下是引用片段
void STDMETHODCALLTYPE CHelloWorldBHO::OnDocumentComplete(IDispatch *pDisp VARIANT *pvarURL)
{
HRESULT hr = S_OK;
// 查詢 IWebBrowser 接口
CComQIPtr spTempWebBrowser = pDisp;
// 此事件是否與頂級浏覽器相關聯?
if (spTempWebBrowser && m_spWebBrowser &&
m_spWebBrowserIsEqualObject(spTempWebBrowser))
{
// 從浏覽器中獲取當前文檔對象……
CComPtr spDispDoc;
hr = m_spWebBrowser>get_Document(&spDispDoc);
if (SUCCEEDED(hr))
{
// ……並查詢 HTML 文檔
CComQIPtr spHTMLDoc = spDispDoc;
if (spHTMLDoc != NULL)
{
// 最後刪除這些圖像
RemoveImages(spHTMLDoc);
}
}
}
}
pDisp 中的 IDispatch 指針包含了已在其中加載文檔的窗口或框架的 IWebBrowser 接口我們將該值存儲在 CComQIPtr 類變量中該變量將自動執行一個 QueryInterface接下來為確定該頁面是否已完全加載我們將該接口指針與頂層浏覽器在 SetSite 中進行緩存處理的接口指針進行比較本測試的結果是我們僅從頂層浏覽器框架的文檔中刪除了圖像;未加載到頂層框架中的文檔沒有通過本測試(有關詳細信息請參閱如何確定頁面何時在 WebBrowser 控件中完成加載和如何獲取 HTML 框架的 WebBrowser 對象模型)
檢索 HTML document 對象需要兩個步驟即使浏覽器已經承載了另一種類型的文檔對象(例如 Microsoft Word 文檔)get_Document 也要為活動文檔檢索一個指針因此必須查詢該活動文檔是否有 IHTMLDocument 接口以確定它是否確實是 HTML 頁面通過 IHTMLDocument 接口可以訪問 DHTML DOM 的內容
確認某 HTML 文檔已加載後將該值傳遞給 RemoveImages請注意該參數作為指針(而不是作為 CComPtr)傳遞給 IHTMLDocument
以下是引用片段
void CHelloWorldBHO::RemoveImages(IHTMLDocument* pDocument)
{
CComPtr spImages;
// 從 DOM 中獲取圖像集
HRESULT hr = pDocument>get_images(&spImages);
if (hr == S_OK && spImages != NULL)
{
// 獲取集合中的圖像數
long cImages = ;
hr = spImages>get_length(&cImages);
if (hr == S_OK && cImages > )
{
for (int i = ; i < cImages; i++)
{
CComVariant svarItemIndex(i);
CComVariant svarEmpty;
CComPtr spdispImage;
// 按索引從集合中獲取圖像
hr = spImages>item(svarItemIndex svarEmpty &spdispImage);
if (hr == S_OK && spdispImage != NULL)
{
// 首先查詢通用 HTML 元素接口……
CComQIPtr spElement = spdispImage;
if (spElement)
{
// ……然後請求樣式接口
CComPtr spStyle;
hr = spElement>get_style(&spStyle);
// 設置 display=none 以隱藏圖像
if (hr == S_OK && spStyle != NULL)
{
static const CComBSTR sbstrNone(Lnone);
spStyle>put_display(sbstrNone);
}
}
}
}
}
}
}
使用 C++ 與 DOM 交互要比使用 JavaScript 更繁瑣但代碼流在本質上相同
上述代碼將循環訪問圖像集合中的每個項在腳本中很明顯就可以看出是按序數還是按名稱訪問集合元素;但在 C++ 中則必須通過傳遞一個空變量來手動區分這些參數我們要再次依靠 ATL 幫助程序類(這次是 CComVariant)來將我們必須編寫的代碼量最小化
最後的注意事項
為便於編寫腳本DOM 中的所有對象都使用 IDispatch 來提供從多個接口派生的屬性和方法但在 C++ 中則必須要顯式查詢支持要使用的屬性或方法的接口例如圖像對象同時支持 IHTMLElement 接口和 IHTMLImgElement 接口因此要檢索圖像的 style 對象首先必須查詢 IHTMLElement 接口該接口可提供 get_style 方法
另請注意COM 規則不能保證發生故障時指針的有效性;因此在每次 COM 調用後都需要檢查 HRESULT此外對於許多 DOM 方法來說返回 NULL 值並不是錯誤;因此需要對返回值和指針值都進行仔細檢查為使該檢查更安全應始終預先將指針初始化為 NULL采用防御性的詳細容錯編碼樣式將有助於防止以後發生無法預測的程序錯誤
總結
雖然有各種類型的 BHO 用於多種用途但所有 BHO 都有一個共同特點與浏覽器連接由於 BHO 可以與 Internet Explorer 緊密集成因此受到需要擴展浏覽器功能的大量開發人員的重視本文說明了如何創建一個簡單 BHO 以用於在加載文檔中修改 IMG 元素的樣式屬性我們鼓勵您根據自己需要將本文中的入門級示例繼續延伸可通過訪問以下鏈接進一步探究這些可能性
From:http://tw.wingwit.com/Article/program/net/201311/13645.html