用Visual Studio 2008開發IE BHO (瀏覽器幫助物件)之一

FirstHope發表於2020-04-05
 
這篇文章是應同學們的要求寫的,以前都是用VC++ 6.0+Platform SDK完成的. 遷移到 VS2008之後,原來Visual Studio 6.0裡的BHO嚮導不復存在,因此特此不厭其煩,詳細說明,本文也適用於VS2005.

首先談BHO的開發工具,我偏向使用VC++(unmanaged C++) 作為開發工具,因為Java JVM或.Net CLR的虛擬機器是個很笨重的東西,也是記憶體殺手, 並不具備寫plugin的快捷輕巧的特點.個人並不喜歡將其作為Plug-in的開發平臺,不過我會有另文說明用C#開發BHO的全過程, 作為那些偏重開發效率的同學的參考.


其次是類庫的選擇,我傾向利用“活動模板庫”(ATL) 來開發使用 C++ 的 BHO。之所以使用 ATL,是因為它方便地實現了我們可以按需進行擴充套件的基本樣板。儘管使用“Microsoft 基礎類”(MFC) 或 Win32 API 和 COM)也可以建立BHO,但 ATL 是為我們提供了自動處理許多細節的輕型庫,包括建立含有 BHO 類識別符號 (CLSID) 的登錄檔。

ATL 的另一個優勢在於它的 COM智慧感知指標類(例如,CComPtr 和 CComBSTR),這些類可管理 COM 物件的生命週期。例如,CComPtr 在賦值時會呼叫 AddRef,而在物件被銷燬或超出範圍時會呼叫 Release。智慧指標簡化了程式碼並且有助於避免記憶體洩漏。當在單個方法範圍內使用時,它們的穩定性和可靠性尤為有用。

介紹完ATL, 我們也簡單介紹一下BHO. BHO是將自定義功能新增到 Internet Explorer 的輕型 DLL 擴充套件,除了IE, BHO 還可以將功能新增到 Windows 資源管理器外殼程式.

BHO 通常並不提供其自身的任何使用者介面 (UI)。它們而是通過在後臺響應瀏覽器事件和使用者輸入資料來發揮作用。例如,BHO 可以攔截彈出視窗、自動填充窗體或為滑鼠手勢新增支援。

有一種常見誤解認為工具欄擴充套件項需要 BHO.但如果將 BHO 與工具欄配合使用,則可以實現更豐富的使用者體驗。(關於IE工具欄的程式設計,在另一篇文章中說明).

BHO 的生命週期與它所互動的瀏覽器例項的生命週期相等。在 IE 6 和早期版本中,這意味著要為每個新的頂層視窗建立(和銷燬)一個新 BHO。在IE 7中則是為每個選項卡都建立和銷燬一個新 BHO。


BHO 必須實現 IObjectWithSite 介面, 該介面提供了兩個方法GetSite和SetSite。根據MSDN的說明:
Java程式碼 複製程式碼 收藏程式碼
  1. GetSite: Gets the last site set with IObjectWithSite::SetSite. If there is no known site, the object returns a failure code.
  2. SetSite: Provides the site's IUnknown pointer to the object.
GetSite:  Gets the last site set with IObjectWithSite::SetSite. If there is no known site, the object returns a failure code.

SetSite:  Provides the site's IUnknown pointer to the object.


我們主要是對後者進行呼叫,此方法方便了與 Internet Explorer 的初始通訊,並會在其將要釋放時通知 BHO。我們實現此介面,然後將 BHO 的 CLSID 新增到登錄檔中,就可以建立一個簡單的瀏覽器擴充套件。過程如下:

1. 在Visual Studio中,選擇VC++中的ATL專案, 建立一個新的專案MySolutionPlugin, 在隨後的嚮導中,確認Server Type是Dll, Visual Studio會為我們建立程式的模板.

2. 為該專案新增我們的程式主體, (不熟悉visual studio的同學在資源瀏覽器裡的右鍵選單裡選 add-->class, 可別選到New Item), 型別選ATL Simple Object , short name命名為RayBHO,各項屬性如下:
a) “執行緒模型” ---“Apartment”
b) “聚合”---“否”
c) “介面”---“雙重”
d) “支援”---勾上“IobjectWithSite”。

具體的含義請參考MSDN.


一般來說,Internet Explorer 至少呼叫SetSite方法兩次: 一次用於建立連線,另一次則是在瀏覽器退出時。我們 BHO 中的 SetSite 實現將執行以下操作:

  •儲存對站點的引用。在初始化期間,瀏覽器將 IUnknown 指標傳遞給頂層 WebBrowser 控制元件,然後 BHO 將對它的引用儲存在一個專用成員變數中。

  •釋放目前被佔用的站點指標。Internet Explorer 傳遞 NULL 時,BHO 必須釋放所有介面引用並且斷開與瀏覽器的連線。


要實現SetSite,我們需手工在新增一個public的方法:
Java程式碼 複製程式碼 收藏程式碼
  1. STDMETHOD(SetSite)(IUnknown * pUnkSite);
	STDMETHOD(SetSite)(IUnknown * pUnkSite);


STDMETHOD 巨集是將方法標記為虛方法並且確保其具有適用於公共 COM 介面的呼叫約定的一個ATL 約定, 它有助於區分 COM 介面和該類中可能存在的其他公共方法。其實現成員方法時應相應使用 STDMETHODIMP 巨集。同時我們需要宣告一個私有變數來儲存Browser的指標"
Java程式碼 複製程式碼 收藏程式碼
  1. CComPtr<IWebBrowser2> m_spWebBrowser;//儲存Browser指標的私有變數
         CComPtr<IWebBrowser2> m_spWebBrowser;//儲存Browser指標的私有變數


然後是SetSite的實現
Java程式碼 複製程式碼 收藏程式碼
  1. STDMETHODIMP CRayBHO::SetSite(IUnknown*pUnkSite)
  2. {
  3. if(pUnkSite!=NULL)
  4. {
  5. //快取指向IWebBrowser2的指標。
  6. pUnkSite->QueryInterface(IID_IWebBrowser2,(void**)&m_spWebBrowser);
  7. }
  8. else
  9. {
  10. //在此釋放快取的指標和其他資源。
  11. m_spWebBrowser.Release();
  12. }
  13. //返回基類實現
  14. return IObjectWithSiteImpl::SetSite(pUnkSite);
  15. }
   STDMETHODIMP CRayBHO::SetSite(IUnknown*pUnkSite)
   {
	if(pUnkSite!=NULL)
	{
	//快取指向IWebBrowser2的指標。
       pUnkSite->QueryInterface(IID_IWebBrowser2,(void**)&m_spWebBrowser);
	}
	else
	{
		//在此釋放快取的指標和其他資源。
		m_spWebBrowser.Release();
	}
	//返回基類實現
	return IObjectWithSiteImpl::SetSite(pUnkSite);
}



從上面的介紹我們知道, 初始化期間,瀏覽器將傳遞一個對其頂層 IWebBrowser2 介面(我們對其進行快取處理)的引用。瀏覽器關閉時將傳遞 NULL,為避免記憶體洩漏和迴圈引用計數,此時釋放所有指標和資源非常重要。最後,我們呼叫基類實現以便繼續執行介面合約的其餘部分。

載入DLL 後,系統將通過 DLL_PROCESS_ATTACH 通知呼叫 DllMain 函式。由於 Internet Explorer 大量使用多執行緒,因此,對 DllMain 的頻繁的 DLL_THREAD_ATTACH 和 DLL_THREAD_DETACH 通知會降低擴充套件和瀏覽器程式的整體效能。

如果BHO 不需要執行緒級的跟蹤,我們可以在 DLL_PROCESS_ATTACH 通知期間呼叫 DisableThreadLibraryCalls 以避免新執行緒通知的額外開銷。修改DllMain.cpp 中的DllMain函式:
Java程式碼 複製程式碼 收藏程式碼
  1. extern "C" BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
  2. {
  3. if(dwReason==DLL_PROCESS_ATTACH)
  4. {
  5. DisableThreadLibraryCalls(hInstance);
  6. }
  7. return _AtlModule.DllMain(dwReason,lpReserved);
  8. }
extern "C" BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
	if(dwReason==DLL_PROCESS_ATTACH)
	{
		DisableThreadLibraryCalls(hInstance);
	}
	return _AtlModule.DllMain(dwReason,lpReserved);
}


要使BHO工作,我們還需要把BHO 的 CLSID 新增到登錄檔中。此條目會將 此DLL 標記為瀏覽器幫助程式物件,並使 Internet Explorer 在啟動時載入 BHO。我們可以在MySolutionPlugin.idl中找到該BHO的CLSID.幸運的,Visual Studio會幫助我們實現這些, 你看到:
Java程式碼 複製程式碼 收藏程式碼
  1. importlib("stdole2.tlb");
  2. [
  3. uuid(057F3E68-6C2E-40A5-A641-E8CF9D6766F3),
  4. helpstring("RayBHO Class")
  5. ]
	importlib("stdole2.tlb");
	[
		uuid(057F3E68-6C2E-40A5-A641-E8CF9D6766F3),
		helpstring("RayBHO Class")
	]


您的機器的CLSID可能有所不同, 接著開啟RayBHO.rgs檔案,新增入:

Java程式碼 複製程式碼 收藏程式碼
  1. HKLM
  2. {
  3. NoRemove SOFTWARE
  4. {
  5. NoRemove Microsoft
  6. {
  7. NoRemove Windows
  8. {
  9. NoRemove CurrentVersion
  10. {
  11. NoRemove Explorer
  12. {
  13. NoRemove 'Browser Helper Objects'
  14. {
  15. ForceRemove {057F3E68-6C2E-40A5-A641-E8CF9D6766F3} = s 'RayBHO Class'
  16. {
  17. val NoExplorer = d '1'
  18. }
  19. }
  20. }
  21. }
  22. }
  23. }
  24. }
  25. }
HKLM
{
	NoRemove SOFTWARE
	{
		NoRemove Microsoft
		{
			NoRemove Windows
			{
				NoRemove CurrentVersion
				{
					NoRemove Explorer
					{
						NoRemove 'Browser Helper Objects'
						{
							ForceRemove {057F3E68-6C2E-40A5-A641-E8CF9D6766F3} = s 'RayBHO Class'
							{
								val NoExplorer = d '1'
							}
						}
					}		
				}
			}
		}
	}
}


這一段是為了在登錄檔裡新增一個雙位元組的NoExplorer=1的鍵,不讓Windows Explorer載入該BHO,因此該BHO只能在ie中執行.

你可以編譯這個BHO. 如果一切正常, 你可以在IE的管理載入項裡看到這個BHO.

如果不幸報錯: 該BHO無法被註冊,根據我的經驗,原因大概有2類,可以依次檢查

1. 你是否有管理員許可權以修改登錄檔,如不是管理員身份,可以在選單上右擊Microsoft Visual studio 2008,從右鍵選單中選擇"執行方式"...
2. 你的登錄檔條目語法是否正確,或者含有非法字元.

相關文章