VC++开发BHO插件——定制你的浏览器.



正文
Windows操作系统上,我们最常见的浏览器有两种:文件浏览器(exploer.exe,应用于文件系统)和Internet浏览器(iexplore.exe,应用于互联网资源)。由于这两个浏览器功能强大,而且又与Windows操作系统捆绑销售,最终也就成为了浏览器的标准。但有时候,为了给浏览器加入一些新的特性,我们往往会重新设计一个自己的浏览器。新的浏览器模仿标准浏览器的大部分功能,同时加入新特性。这种做法最直观,但实际上也是相对于微软的重复劳动,且工作量比较大。其实,使用BHO插件,一切都变得很简单。

BHOBrowser Help Objects),是实现了特定接口的COM组件。开发好的BHO插件在注册表特定的位置注册好后,每当微软的浏览器启动,BHO实例就会被创建。在浏览器工作的工程中,BHO会接收到很多事件,比如浏览器浏览新的地址、前进或后退、生成新的窗口、浏览器退出等等;BHO可以在这些事件的响应中实现与浏览器的交互。

下面,我们首先来介绍一下BHO的工作原理。上面我们已经提到,BHOCOM组件,而且一定实现了IObjectWithSite接口。这些组件除了在注册表中注册为COM Server外,还必须将它们的CLSIDHKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\ CurrentVersion\Explorer\Browser Helper Objects下注册为子键。微软在设计浏览器的时候,已经给这些组件预留了空间。每当浏览器启动时,浏览器会首先在上述注册表位置查看是否有注册的BHO CLSID;如果有则分别创建一个实例,并对BHO实例进行初始化,建立交互连接。(注:BHO实例只有在创建它的浏览器窗口销毁时才被释放。)下图演示了BHO的创建过程:


成功创建的BHO,不仅可以得到各种标准的浏览器操作事件,并做出响应;还可以定制浏览器的菜单、工具条等界面元素;更或者可以安装钩子函数,监视浏览器的一举一动。值得注意的是,使用BHO插件,Internet浏览器要求在4.0以上版本;如果是文件浏览器,操作系统要求是Windows 95/98/2000Window NT 4.0以上版本,并且Shell的版本在4.71以上。下面是支持BHO特性的系统一览表:

Shell版本 操作系统版本 支持BHO
4.00 Windows 95 and Windows NT 4.0IE版本为 4.0) 仅IE4.0
4.71 Windows 95 and Windows NT 4.0IE版本为 4.0IE和文件浏览器
4.72 Windows 98 IE和文件浏览器
5.00  Windows 2000 IE和文件浏览器

接下去,笔者就来介绍一下如何开发BHO插件,开发环境为VC6.0(使用ATL),安装Platform SDK中的Internet Development SDK。首先,启动VCATL COM AppWizard,生成一个项目名为BhoPlugin,其余均采用默认设置。接着,我们就来分步详细阐述。
第一步,增加一个ATL Object到该项目中。VC菜单Insert->New ATL Object…,在弹出的对话框中选择“Internet Explorer Object”,输入COM类名(在Short Name后输入EyeOnIE,其它各项会自动生成)。完成后,我们可以看到CEyeOnIE类有一个基类IObjectWithSiteImpl,这个就是实现IObjectWithSite接口的模版类。
第二步,实现IObjectWithSite的接口方法。在这之前,我们要先定义几个成员变量:CComQIPtr mWebBrowser2,(需要加入#include "ExDisp.h"),用以保存浏览器组件的指针;DWORD mCookie,用以保存与浏览器的连接IDIObjectWithSite有两个接口方法:SetSiteGetSite。我们只需重载SetSite就行了。在EyeOnIE.h中增加函数声明STDMETHOD(SetSite)(IUnknown *pUnkSite),在EyeOnIE.cpp实现如下:

STDMETHODIMP CEyeOnIE::SetSite(IUnknown *pUnkSite)
{
USES_CONVERSION;

if (pUnkSite)
{
mWebBrowser2 = pUnkSite;
if (mWebBrowser2)
{
return RegisterEventHandler(TRUE);
}
}
return E_FAIL;
}

HRESULT CEyeOnIE::RegisterEventHandler(BOOL inAdvise)
{
CComPtr<IConnectionPoint> spCP;
// Receives the connection point for WebBrowser events
CComQIPtr<IConnectionPointContainer, &IID_IConnectionPointContainer> spCPC(mWebBrowser2);
HRESULT hr = spCPC->FindConnectionPoint(DIID_DWebBrowserEvents2, &spCP);
if (FAILED(hr))
return hr;

if (inAdvise)
{
// Pass the event handlers to the container
hr = spCP->Advise(reinterpret_cast<IDispatch*>(this), &mCookie);
}
else
{
spCP->Unadvise(mCookie);
}
return hr; 
}

我们可以看到,SetSite的参数实际上指向的是浏览器组件。在SetSite实现中,我们首先保存浏览器组件指针,然后将该BHO向浏览器注册为事件处理器。
第三步,实现IDispatch接口方法。事件处理也就在IDispatch::Invoke中实现(各个事件的IDExDispID.h中定义)。BHO可能会接收到很多事件,但我们只需要响应我们感兴趣的那一部分。首先在EyeOnIE.h中增加该函数的声明,在EyeOnIE.cpp的实现中,笔者试着响应浏览器浏览一个地址之前发出的事件DISPID_BEFORENAVIGATE2,以此来实现简单的网址过滤功能,代码参考如下:

STDMETHODIMP CEyeOnIE::Invoke(DISPID dispidMember,REFIID riid, LCID lcid, 
WORD wFlags, DISPPARAMS * pDispParams, 
VARIANT * pvarResult,EXCEPINFO * pexcepinfo, 
UINT * puArgErr)
{ 
USES_CONVERSION;

if (!pDispParams)
return E_INVALIDARG;

switch (dispidMember)
{
//
// The parameters for this DISPID are as follows:
// [0]: Cancel flag - VT_BYREF|VT_BOOL
// [1]: HTTP headers - VT_BYREF|VT_VARIANT
// [2]: Address of HTTP POST data - VT_BYREF|VT_VARIANT 
// [3]: Target frame name - VT_BYREF|VT_VARIANT 
// [4]: Option flags - VT_BYREF|VT_VARIANT
// [5]: URL to navigate to - VT_BYREF|VT_VARIANT
// [6]: An object that evaluates to the top-level or frame
// WebBrowser object corresponding to the event. 
//
case DISPID_BEFORENAVIGATE2:
{
LPOLESTR lpURL = NULL;
mWebBrowser2->get_LocationURL(&lpURL);
char * strurl;
if (pDispParams->cArgs >= 5 && pDispParams->rgvarg[5].vt == (VT_BYREF|VT_VARIANT))
{
CComVariant varURL(*pDispParams->rgvarg[5].pvarVal);
varURL.ChangeType(VT_BSTR);
strurl = OLE2A(varURL.bstrVal);
}
if (strstr(strurl, "girl.com"))
{
*pDispParams->rgvarg[0].pboolVal = TRUE;
::MessageBox(NULL, _T("该网页已被禁止!"),_T("Warning"),MB_ICONSTOP);
return S_OK;
}
break;
}

case DISPID_NAVIGATECOMPLETE2:
break;
case DISPID_DOCUMENTCOMPLETE:
break;
case DISPID_DOWNLOADBEGIN:
break;
case DISPID_DOWNLOADCOMPLETE:
break;
case DISPID_NEWWINDOW2:
break;
case DISPID_QUIT:
RegisterEventHandler(FALSE);
break;
default:
break;
}

return S_OK;
}

我们看到,当用户浏览的新地址包含"girl.com"字符的时候,浏览器就会弹出一个警告对话框,并且停止进一步的动作。另外值得注意的是,在DISPID_QUIT事件(浏览器将要退出)的响应中,我们将BHO事件处理器进行了注销。
第四步,因为BHO可能会被文件浏览器加载。如果我们不想这样,我们就要在DllMain中对加载者进行判断,参考如下:

extern "C"
BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID /*lpReserved*/)
{
if (dwReason == DLL_PROCESS_ATTACH)
{
// Check who''s loading us. 
// If it''s Explorer then "no thanks" and exit...
TCHAR pszLoader[MAX_PATH];
GetModuleFileName(NULL, pszLoader, MAX_PATH);
_tcslwr(pszLoader);
if (_tcsstr(pszLoader, _T("explorer.exe"))) 
return FALSE;

_Module.Init(ObjectMap, hInstance, &LIBID_BHOPLUGINLib);
DisableThreadLibraryCalls(hInstance);
}
else if (dwReason == DLL_PROCESS_DETACH)
_Module.Term();
return TRUE; // ok
}
最后,别忘了修改注册表文件,追加BHO的注册信息。在EyeOnIE.rgs文件的下面增加如下代码:
HKLM
{
SOFTWARE
{
Microsoft
{
Windows
{
CurrentVersion
{
Explorer
{
''Browser Helper Objects''
{
{6E28339B-7A2A-47B6-AEB2-46BA53782379}
}
}
}
}
}
}
}

注意,{6E28339B-7A2A-47B6-AEB2-46BA53782379}是笔者这个BHOCLSID,如果你自己开发BHO,这里应该正确填写你的CLSID

好了,一个简单的BHO开发完成了。(可以到本人的个人主页 http://hqtech.nease.net 下载实例源代码。)BHO插件可以实现的功能还有很多,比如网页内容分析、IE界面定制等等。作为总结,笔者还要提醒读者一点的是,如果不想让BHO起作用了,可以注销该插件,如下格式:regsvr32 /u yourpath\yourbho.dll,或者直接在注册表中将“Browser Helper Objects”目录下注册的CLSID删掉。
正文完

如何使用BHO定制你的Internet Explorer浏览器

原文:微软公司 Dino Esposito
编译:朱先中

原文出处:Browser Helper Objects: The Browser the Way You Want It

一、简介
  有时,你可能需要一个定制版本的浏览器。在这种情况下,你可以自由地把一些新颖但又不标准的特征增加到一个浏览器上。结果,你最终有的只是一个新但不标准的浏览器。Web浏览器控件只是浏览器的分析引擎。这意味着仍然存在若干的与用户接口相关的工作等待你做――增加一个地址栏,工具栏,历史记录,状态栏,频道栏和收藏夹等。如此,要产生一个定制的浏览器,你可以进行两种类型的编程――一种象微软把Web浏览器控件转变成一个功能齐全的浏览器如 Internet Explorer;一种是在现有的基础上加一些新的功能。如果有一个直接的方法定制现有的Internet Explorer该多好?BHOBrowser Helper Objects,我译为"浏览器帮助者对象",以下皆简称BHO)正是用来实现此目的的。

二、关于软件定制
  以前,定制一个软件的行为主要是通过子类化方法实现的。 通过这种办法,你可以改变一个窗口的外表与行为。子类化虽然被认为是一种有点暴力方式――受害者根本不知道发生的事情――但它还是长时间以来的唯一的选择。
  随着微软Win32 API的到来,进程间子类化不再被鼓励使用并愈发变得困难起来。当然,如果你是勇敢的--指针从未吓倒你,而最重要的是,如果你已经游刃于系统钩子之间,你可能觉得这一问题太简单了。但是情形并不总是这样。暂放下这点不管,问题在于每一个进程运行在自己的地址空间中,而且打破进程边界略微有些不正确性。 另一方面,你可能需要对定制进行更好的管理。更经常情况下,定制可能是程序本身强烈要求实现的。
  在后者情况下,已安装的软件只需在既定的磁盘位置查询另外的组件模块,然后装载、设定初值,最后让它们自由地按照既定的设计工作。这正是Internet Explorer浏览器和它的BHO所要实现的。

三、什么是BHO?
  从某种观点看,Internet Explorer同普通的Win32程序没有什么两样。借助于BHO,你可以写一个进程内COM对象,这个对象在每次启动时都要加载。这样的对象会在与浏览器相同的上下文中运行,并能对可用的窗口和模块执行任何行动。例如,一个BHO能够探测到典型的事件,如GoBackGoForwardDocumentComplete等;另外BHO能够存取浏览器的菜单与工具栏并能做出修改,还能够产生新窗口来显示当前网页的一些额外信息,还能够安装钩子以监控一些消息和动作。简而言之, BHO的工作如我们打入浏览器领地的一位间谍(注意这是微软允许的合法工作)。
  在进一步了解BHO细节之前,有几点我需要进一步阐述。首先,BHO对象依托于浏览器主窗口。实际上,这意味着一旦一个浏览器窗口产生,一个新的BHO对象实例就要生成。任何 BHO对象与浏览器实例的生命周期是一致的。其次, BHO仅存在于Internet Explorer 4.0及以后版本中。
如果你在使用 Microsoft Windows? 98Windows 2000Windows 95or Windows NT版本4.0 操作系统的话,也就一块运行了活动桌面外壳4.71BHO也被 Windows资源管理器所支持。 BHO是一个COM进程内服务,注册于注册表中某一键下。在启动时,Internet Explorer查询那个键并把该键下的所有对象预以加载。
  Internet Explorer浏览器初始化这一对象并要求某一接口功能。如果发现这一接口, Internet Explorer使用其提供的方法传递 IUnknown 指针到BHO对象。见图一:


图一 ie浏览器如何装入和初始化BHO对象,BHO场所(site)是用于实现通信的COM接口

  浏览器可能在注册表中发现一系列的CLSID,并由此为每个CLSID建立一个进程中实例。结果是,这些对象被装载至浏览器上下文中并运行起来,好象它们是本地组件一样。但是,由于Internet ExplorerCOM特性,即使被装入到它的进程空间中于事(你的野心实现)也不一定会有多大帮助。用另一说法, BHO的确能够做许多潜在的有用的事情,如子类化组成窗口或者安装线程局部钩子,但是它确实远离浏览器的核心活动。为了钩住浏览器的事件或者自动化浏览器,BHO需要建立一个私有的基于COM的通讯通道。为此,该BHO应该实现一个称为IObjectWithSite的接口。事实上,通过接口 IobjectWithSiteInternet Explorer 可以传递它的IUnknown 接口。BHO反过来能够存储该接口并进一步查询更专门的接口,如IWebBrowser2IDispatchIConnectionPointContainer
  另外一种分析BHO对象的途径与Internet Explorer外壳扩展有关。我们知道,一个WINDOWS外壳扩展即是一个进程内的COM服务器,它在Windows资源管理器执行某种动作时装入内存――如显示上下文菜单。通过建立一个实现几个COM接口的COM模块,你就给上下文菜单加上一些项并能预以正确处理。一个外壳扩展必须以Windows 资源管理器能够发现的方法注册。一个BHO对象遵循同样的模式――唯一的改变在于要实现的接口。然而,尽管实现方式有所不同,外壳扩展与 BHO 仍有许多共同的特点。如下表一:

表一 外壳扩展与 BHO相近特性比较

特性

外壳扩展

BHO对象

加载者

Windows资源管理器

Internet Explorer(和外壳4.17及以上版本的Windows资源管理器)

击活动作

在某类文档上的用户动作(即单击右键)

打开浏览器窗口

何时卸载

参考计数达到0的几秒之后

导致它加载的窗口关闭时

实现形式

COM进程中DLL

COM 进程中 DLL

注册需求

常常是为一个COM服务器设置的入口处,另加的入口依赖于外壳类型及它要应用至的文档类型

常常是为一个COM服务器设置的入口处,另加一个把它申请为BHO的注册入口

接口需求

依赖于外壳扩展的类型

IObjectWithSite

如果你对SHELL扩展编程有兴趣的话,可以参考MSDN有关资料。

四、BHO的生存周期
  前面已经说过,BHO不仅仅为Internet Explorer所支持。如果你在使用外壳 4.71或者更高版本,你的BHO对象也会被Windows资源管理器所加载。下表二展示了我们可以使用的不同版本的外壳产品情况,Windows外壳版本号存于库文件shell32.dll中。

表二 不同版本的Windows外壳对于BHO的支持情况

外壳版本

安装的产品

BHO的支持情况

4.00

Windows 95Windows  NT 4.0 带或不带 Internet Explorer 4.0 或更老版本。 注意没有安装外壳更新

Internet Explorer 4.0

4.71

Windows 95Windows NT 4.0 Internet Explorer 4.0 和活动桌面外壳更新

Internet Explorer Windows 资源管理器

4.72

Windows 98

Internet ExplorerWindows 资源管理器

5.00

Windows 2000

Internet ExplorerWindows 资源管理器

  BHO对象随着浏览器主窗口的显示而装入,随着浏览器主窗口的销毁而缷载。如果你打开多个浏览器窗口,多个BHO实例也一同产生。
  无论浏览器以什么样的命令行启动,BHO对象都被加载。举例来说,即使你只是想要见到特定的 HTML 页或一个给定的文件夹,BHO对象也被加载。一般地,当 explorer.exe iexplore.exe 运行的时候,BHO都要被考虑在内。如果你设置了"Open each folder in its own window"(对每一个文件夹以一个独立窗口打开)文件夹选项,那么你每次打开一个文件夹,BHO对象都要被加载。见图二。


图二 经过这样设置,你每次打开一个文件夹时,执行一个独立的explorer.exe实例,并装入已注册的BHO对象。

  但是注意,这种情形仅适于当你从桌面上的"我的电脑"图标中打开文件夹的情况。在这种情况下,每次你移到另外一个文件夹时外壳都要调用 explorer.exe。这种情况在你同时用两个窗格进行浏览时是不会发生的。事实上,当你改变文件夹时,外壳是不会启动浏览器的新的实例的而仅是简单创建嵌入视图对象的另外一个实例。奇怪的是,如果你在地址栏中输入一个新的名字来改变文件夹时,在同一个窗口中同样可以达到浏览之目的,无论 Windows资源管理器视图是单个的还是双视图形式。
  对于Internet Explorer的情形,事情要更简单一些。只有你显式地多次运行iexplore.exe浏览器时,你才有多个Internet Explorer的拷贝。当你从Internet Explorer中打开新的窗口时,每一个窗口在一个新的线程中被复制而不是创建一个新的进程,因此也就不需要重新载入BHO对象。
  首先, BHO最有趣的地方是,它是极度动态的。每次Windows资源管理器或者Internet Explorer打开,装载器从注册表中读取已安装的BHO对象的CLSID然后处理它们。如果你在打开的浏览器多个实例中间编辑注册表的话,你可以随着多个浏览器拷贝的载入而装入多个不同的BHO。 这就是说,如果你选择从头创建一个新的属于自己的浏览器,那么你可以把它内嵌在一个Visual Basic或者MFC框架窗口中。同时你有相当的机会来灵活安排浏览程序。如果它们能满足你的需要的话,你可以依赖于Internet Explorer的强大的功能并且加上你想要的尽可能多的插件。

五、关于IObjectWithSite接口
  从一个高起点来看,BHO即是一个DLL,它能够依附于Internet Explorer浏览器的一个新建的实例,在某些情况下也适用于Windows资源管理器。
  一般地,一个场所(site)是一个中间对象,它位于容器对象与被包容对象之间。通过它,容器对象管理被包容对象的内容,也因此使得对象的内部功能可用。为此,容器方要实现接口IoleClientSite,被包容对象要实现接口IOleObject 。通过调用IOleObject提供的方法,容器对象使得被包容对象清楚地了解其HOST的环境。
  一旦容器对象成为Internet Explorer(或是具有WEB能力的Windows资源管理器),被包容对象只需实现一个轻型的IObjectWithSite接口。该接口提供了以下方法:

表三 IObjectWithSite定义

方法

描述

HRESULT SetSite(IUnknown* pUnkSite)

接收ie浏览器的IUnknown指针。典型实现是保存该指针以备将来使用。.

HRESULT GetSite(REFIID riidvoid** ppvSite)

从通过SetSite()方法设置的场所中接收并返回指定的接口,典型实现是查询前面保存的接口指针以进一步取得指定的接口。

  对BHO 的唯一严格的要求正在于必须实现这一个接口。 注意你应该避免在调用以上任何一个函数时返回E_NOTIMPL 。 要么你不实现这一接口,要么应保证在调用这些方法时进行正确地编码。

六、构造自己的BHO对象
  一个BHO对象就是一个进程中服务器DLL,选用ATL创建它是再恰当不过的了。我们选择ATL的另外一个原因是因为它已经提供了缺省的而且提供了 IObjectWithSite接口的足够好的实现。另外,在ATL COM 向导本地支持的已定义好的对象类型当中,有一个,就是Internet Explorer对象,这正是一个BHO应该具有的类型。一个 ATL Internet Explorer 对象,事实上是一个简单对象――也就是说,是一个支持IUnknown和自注册,还有接口IObjectWithSiteCOM 服务器。如果你在ATL工程中添加一个这样的对象,并调用相应的类CViewSource,你将从向导中得到下列代码:

class ATL_NO_VTABLE CViewSource :
   public CComObjectRootEx<CComSingleThreadModel>,
   public CComCoClass<CViewSource, &CLSID_ViewSource>,
   public IObjectWithSiteImpl<CViewSource>,
   public IDispatchImpl<IViewSource, &IID_IViewSource,
                        &LIBID_HTMLEDITLib>      

正如你所见,向导已经使类从接口IObjectWithSiteImpl继承,这是一个ATL模板类,它提供了接口IObjectWithSite的基本实现。一般情况下,没有必要重载成员函数GetSite()。取而代之的是, SetSite() 实现代码经常需要加以定制。ATL实际上仅仅把一个IUnknown接口指针存储在成员变量m_spUnkSite中。
  在文章的剩余部分,我将讨论一个 BHO 的相当复杂而丰富的例子。该BHO对象将依附于Internet Explorer,并显示一个文本框来显示当前正浏览的网页源码。该代码窗口将 随着你改变网页而自动更新,如果浏览器显示的不是一个HTML网页时,它将变灰。你对于原始HTML代码的任何改动立即反映在浏览器中。 HTML (DHTML)使得这一看似魔术般的实现成为可能。该代码窗口可被隐藏和通过按动热键重现。 在可见情况下,它与Internet Explorer共享整个桌面空间,见图三。


图三 BHO对象在使用中。它依附于Internet Explorer,并显示一个窗口来显示当前正浏览的网页源码。还允许你源码进行修改。

  本例子的关键点在于存取Internet Explorer的浏览机制,其实它只不过是WebBrowser控件的一个实例而已。这个例子可以分解为以下五步来实现:

1. 探测谁在装入这个对象,是Internet Explorer还是Windows资源管理器;

2. 获取接口IWebBrowser2以实现Web浏览器对象;

3. 捕捉Web浏览器的特定事件;

4. 存取当前文档对象,确定它是一份HTML类型的文件;

5. 管理对话框窗口以实现HTML源码的显示;

  第一个步骤是在DllMain()中完成的。SetSite()是取得指向WebBrowser对象指针的适当位置。请详细分析以下步骤。

七、探测谁在调用这个对象
  如前所述,一个BHO对象会被Internet Explorer或者Windows资源管理器(前提:外壳版本4.71或者更高)所加载。所以我专门设计了一个BHO来处理HTML网页,因此这个 BHO与资源管理器毫无关系。如果一个Dll不想被调用者一起加载,只需在DllMain()中实现了探明谁在调用该对象后返回FALSE即可。参看下面代码:

if (dwReason == DLL_PROCESS_ATTACH)
{
TCHAR pszLoader[MAX_PATH];

//返回调用者模块的名称,第一个参数应为NULL,详见msdn。
GetModuleFileName(NULL, pszLoader, MAX_PATH);
_tcslwr(pszLoader);
if (_tcsstr(pszLoader, _T("explorer.exe")))
   return FALSE;
}

一旦知道了当前进程是Windows资源管理器,可立即退出。
  注意,再多加一些条件语句是危险的!事实上,另外一些进程试图装入该DLL时将被放弃。如果你做另外一个试验,比方说针对Internet Explorer的执行文件iexplorer.exe,这时第一个受害者就是regsvr32.exe(该程序用于自动注册对象)。

if (!_tcsstr(pszLoader, _T("iexplore.exe")))

你不能够再次注册该DLL库了。 事实上,当 regsvr32.exe 试图装入DLL以激活函数DllRegisterServer()时,该调用将被放弃。

八、与Web浏览器取得联系
  SetSite()方法正是BHO对象被初始化的地方,此外,在这个方法中你可以执行所有的仅仅允许发生一次的任务。当你用Internet Explorer打开一个URL时,你应该等待一系列的事件以确保要求的文档已完全下载并被初始化。唯有在此时,你才可以通过对象模型暴露的接口(如果存在的话)存取文档内容。这就是说你要取得一系列的指针。第一个就是指向IWebBrowser2(该接口用来生成WebBrowser对象)的指针。第二个指针与事件有关。该模块必须作为一个浏览器的事件侦听器来实现,目的是为接收下载以及与文档相关的事件。下面用ATL灵敏指针加以封装:

CComQIPtr< IWebBrowser2, &IID_IWebBrowser2> m_spWebBrowser2;
CComQIPtr<IConnectionPointContainer,
      &IID_IConnectionPointContainer> m_spCPC;

源代码部分如下所示:

HRESULT CViewSource::SetSite(IUnknown *pUnkSite)
{
  // 检索并存储 IWebBrowser2 指针
  m_spWebBrowser2 = pUnkSite;
  if (m_spWebBrowser2 == NULL)
   return E_INVALIDARG;
  //检索并存储 IConnectionPointerContainer指针
  m_spCPC = m_spWebBrowser2;
  if (m_spCPC == NULL)
   return E_POINTER;
  //检索并存储浏览器的句柄HWND. 并且安装一个键盘钩子备后用
  RetrieveBrowserWindow();
  // 为接受事件通知连接到容器
  return Connect();
}

为了取得IWebBrowser2接口指针,你可以进行查询。当然也可以在事件刚刚发生时查询IConnectionPointContainer。这里,SetSite()检索了浏览器的句柄HWND,并且在当前线程中安装了一个键盘钩子。HWND用于后面Internet Explorer窗口的移动或尺寸调整。这里的钩子用来实现热键功能,用户可以按动热键来显示/隐藏代码窗口。

九、从Internet Explorer浏览器取得事件
  当你导向一个新的URL时,浏览器最需要完成的是两种事件:下载文档并为之准备HOST环境。也就是说,它必须初始化某对象并使该对象从外部可以利用。针对不同的文档类型,或者装入一个已注册的Microsoft ActiveX? 服务器来处理该文档(如Word对于.doc文件的处理)或者初始化一些内部组件来分析文档内容并生成和显示该文档。对于HTML网页就是这样,其内容由于DHTML对象作用而变得可用。当文档全部下载结束,DownloadComplete事件被激活。这并不是说,这样利用对象模型就可以安全地管理文档的内容了。事实上,DocumentComplete 事件仅指明一切已经结束,文档已准备好了 (注意DocumentComplete事件仅在你第一次存取URL时到达,如果你执行了刷新动作,你仅仅收到一个DocumentComplete事件)
  为了截获浏览器发出的事件, BHO需要通过IConnectionPoint 接口连接到浏览器上并且实现传递接口IDispatch指针以处理各种事件。现在利用前面取得的IConnectionPointContainer指针来调用 FindConnectionPoint方法――它返回一个指针指向连接点对象(正是通过这个连接点对象来取得要求的外向接口,此时是 DIID_DWebBrowserEvent2)。 下列代码显示了连接点的发生情况:

HRESULT CViewSource::Connect(void)
{
  HRESULT hr;
  CComPtr<IConnectionPoint> spCP;
  //为Web浏览器事件而接收(receive)连接点
  hr = m_spCPC->FindConnectionPoint(DIID_DWebBrowserEvent2, &spCP);
  if (FAILED(hr))
   return hr;
  // 把事件处理器传递到容器。每次事件发生容器都将激活我们实现的IDispatch接口上的相应的函数。
  hr = spCP->Advise( reinterpret_cast<IDispatch*>(this), &m_dwCookie);
  return hr;
}

通过调用接口IConnectionPointAdvise() 方法, BHO告诉浏览器它对它产生的事件很感兴趣。由于COM事件处理机制,所有这些意味着BHOIDispatch接口指针提供给浏览器。浏览器将回调IDispatch接口的Invoke() 方法,以事件的ID值作为第一参数:

HRESULT CViewSource::Invoke(DISPID dispidMember, REFIID riid,
   LCID lcid, WORD wFlags, DISPPARAMS* pDispParams,
   VARIANT* pvarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr)
{
  if (dispidMember == DISPID_DOCUMENTCOMPLETE) {
      OnDocumentComplete();
      m_bDocumentCompleted = true;
  }
  :
}

切记,当事件不再需要时,应该使之与浏览器分离。如果你忘记了做这件事情,BHO对象将被锁定,即使在你关闭浏览器窗口之后。很明显,实现分离的最佳时机是收到事件OnQuit时。

十、存取文档对象
  此时,该BHO已经有一个参照指向Internet ExplorerWeb浏览器控件并被连接到浏览器控件以接收所有它产生的事件。当网页被全部下载并正确初始化后,我们就可以通过DHTML文档模型存取它。Web浏览器的文档属性返回一个指向文档对象的IDispatch接口的指针:

CComPtr<IDispatch> pDisp;
HRESULT hr = m_spWebBrowser2->get_Document(&pDisp);

get_Document() 方法取得的仅仅是一个接口指针。我们要进一步确定在IDispatch 指针背后存在一个HTML文档对象。用VB实现的话,可以用下面代码:

Dim doc As Object
Set doc = WebBrowser1.Document
If TypeName(doc)="HTMLDocument" Then
   \'\' 获取文档内容并予以显示
Else
   \'\' Disable the display dialog
End If

现在要了解一下get_Document()返回的IDispatch指针 。Internet Explorer不仅仅是一个HTML浏览器,而且还是一个ActiveX文档容器。这样一来,难以保证当前浏览对象就是一个HTML文档。不过办法还是有的――你想,如果IDispatch指针真正指向一个HTML文档,查询 IHTMLDocument2 接口一定成功。
IHTMLDocument2接口包装了DHTML对象模型用来展现HTML页面的所有功能。下面代码实现这些功能:

CComPtr<IDispatch> pDisp;
HRESULT hr = m_spWebBrowser2->get_Document(&pDisp);
CComQIPtr<IHTMLDocument2, &IID_IHTMLDocument2> spHTML;
spHTML = pDisp;
if (spHTML) {
   // 获取文档内容并予以显示
}
else {
   // disable the Code Window controls
}

如果IHTMLDocument2接口查询失败,spHTML指针将是NULL
  现在考虑如何获得当前显示窗口的源代码。正如一个HTML页把它所有的内容封装在标签<BODY>中,DHTML对象模型要求你取得一个指向Body对象的指针:

CComPtr<IHTMLElement> m_pBody;
hr = spHTML->get_body(&m_pBody);

奇怪的是,DHTML对象模型不让你取得标签<BODY>之前的原始内容,如<HEAD>。其内容被处理并存于一些属性中,但你还是不能从HTML原始文件中提取这部分的RAW文本。这过,仅从BODY部分取得的内容足够了。为了取得包含在<BODY>…< /BODY>间的HTML代码部分,可以把outerHTML属性内容读取到一个BSTR变量中:

BSTR bstrHTMLText;
hr = m_pBody->get_outerHTML(&bstrHTMLText);

在此基础上,在代码窗口中显示源码就是一种简单的事情了:生成一个窗口,进行字符的UNICODEANSI转化和设置编辑框控件的问题。下面代码实现这些功能:

HRESULT CViewSource::GetDocumentContent()
{
  USES_CONVERSION;
  
  // 获取 WebBrowser的文档对象
  CComPtr<IDispatch> pDisp;
  HRESULT hr = m_spWebBrowser2->get_Document(&pDisp);
  if (FAILED(hr))
   return hr;

  // 确保我们取得的是一个IHTMLDocument2接口指针
  //让我们查询一下 IHTMLDocument2 接口 (使用灵敏指针)
  CComQIPtr<IHTMLDocument2, &IID_IHTMLDocument2> spHTML;
  spHTML = pDisp;

  // 抽取文档源代码
  if (spHTML)
  {
    // 取得BODY 对象
    hr = spHTML->get_body(&m_pBody); 
    if (FAILED(hr))
        return hr;
    // 取得HTML 文本
    BSTR bstrHTMLText;
    hr = m_pBody->get_outerHTML(&bstrHTMLText); 
    if (FAILED(hr))
     return hr;
    // 进行文本的Unicode到 ANSI的转换
    LPTSTR psz = new TCHAR[SysStringLen(bstrHTMLText)];
    lstrcpy(psz, OLE2T(bstrHTMLText));
     // 文本进行相应的调整
    HWND hwnd = m_dlgCode.GetDlgItem(IDC_TEXT);
    EnableWindow(hwnd, true);
    hwnd = m_dlgCode.GetDlgItem(IDC_APPLY);
    EnableWindow(hwnd, true);

    // 设置代码窗口中的文本
    m_dlgCode.SetDlgItemText(IDC_TEXT, psz); 
    delete [] psz;
  }
  else   // 文档不是一个 HTML 页
  {
    m_dlgCode.SetDlgItemText(IDC_TEXT, ""); 
    HWND hwnd = m_dlgCode.GetDlgItem(IDC_TEXT);
    EnableWindow(hwnd, false);
    hwnd = m_dlgCode.GetDlgItem(IDC_APPLY);
    EnableWindow(hwnd, false);
  }

  return S_OK;  
}

因为我要运行这段代码来响应DocumentComplete事件通知,每个新的页自动地而且敏捷地被处理。DHTML对象模型使你能够随意修改网页的结构,但这一变化在按F5刷新后全部复原。你还要处理一下DownloadComplete事件以刷新代码窗口 (注意, DownloadComplete 事件发生在 DocumentComplete事件之前)。你应该忽略网页的首次DownloadComplete事件,而是在执行刷新动作时才关注这一事件。布尔成员变量m_bDocumentCompleted正是用来区别这两种情形的。

十一、管理代码窗口
  用来显示当前HTML页原始码的代码窗口涉及另外一个ATL 基本编程问题-对话框窗口,它位于ATL对象向导的"Miscellaneous"选项卡下。
  我调整了代码窗口的大小来响应WM_INITDIALOG消息,使它占居桌面空间的下部区域,正好是在任务栏的上面。在浏览器启动时你可以选择显示或不显示这个窗口。缺省情况下是显示的,但这可以通过清除"Show window at startup"复选框项来实现。当然喜欢的话,你可以随时关闭。按键F12即可重新显示代码窗口。F12是通过在SetSite()中安装的键盘钩子实现的。启动环境存于WINDOWS注册表中,我选择外壳库文件shlwapi.dll中函数SHGetValue来实现注册表的读写操作。这同使用Reg 开头的Win32函数操作相比,简单极了。请看:

DWORD dwType, dwVal;
DWORD dwSize = sizeof(DWORD);
SHGetValue(HKEY_CURRENT_USER, _T("Software\\\\MSDN\\\\BHO"), _T("ShowWindowAtStartup"), &dwType, &dwVal, &dwSize);

这个DLL文件是同Internet Explorer 4.0 和活动桌面的诞生一起产生的,是WIN98及以后版本的标准组成,你可以放心使用。

十二、注册BHO对象
  因为BHO 是一个COM 服务器,所以既应该作为COM 服务器注册又应该作为BHO对象注册。ATL向导自动生成.rgs文件,第一种情况的注册就免除了。下面的文件代码段是用来实现作为BHO对象注册的(CLSID为例中生成)

HKLM {
 SOFTWARE {
  Microsoft {  
   Windows {
    CurrentVersion {
     Explorer {
      \'\'BHO\'\' {
       ForceRemove {1E1B2879-88FF-11D2-8D96-D7ACAC95951F}        
}}}}}}}

注意ForceRemove一词能够实现在卸载对象时删除这一行相应的键值。BHO键下聚集了所有的BHO对象。对于这么多的一串家伙是从来不作缓冲调用的。这样以来,安装与测试BHO就是不费时的事情了。

十三、总结
  本文描述了BHO对象,通过它你可以把自己的代码注入浏览器的地址空间中。你必须做的事情是写一个支持IObjectWithSite 接口的COM 服务器。在这一点上,你的BHO对象可以实现浏览器机制范围内的各种合法目的。本文所及示例涉及了COM事件,DHTML对象模型以及WEB浏览器编程接口。虽然内容稍宽一些,但它正显示了现实世界中的BHO对象的应用。如,你想知道浏览器在显示什么,那么您就需要了解接收事件并要熟悉WEB浏览器才行。
  另外:Windows资源管理器也是与BHO对象交互的,这一点在编程时要特别注意。本文所附源程序为MSDN所带,在Windows2000/VC6下调试通过(编译通过后,重新启动IE即得到结果)。 

 

BHO就是Browser Helper Object(浏览器辅助对象)

BHO关联原理 (BHO关联的是SHDOCVW,也就是说不只关联IE,下面全部用IE来说明)
1.IE的窗口打开时,先寻找HKLM下的SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Browser Helper Objects\ 里的CLSID,这些CLSID,都对应着相应的BHO插件,然后根据这个CLSIDHKCR下的CLSIDs里找到此插件的信息,包括文件位置等。
2.IE根据找到的CLSID信息创建 BHO 对象,并且查找 IObjectWithSite 接口. (这个接口非常简单,只有SetSiteGetSite两个方法)
3.IEIWebBrowser2(浏览器插件)传到 BHO SetSite 方法,用户在此方法中可挂载自己的事件处理方法。
4.窗口关闭时,IEnull 传到 BHO SetSite 方法,此方法用来去掉挂载的事件处理方法。

编写BHO流程
1.创建IObjectWithSite显式接口,创建 COM 类型,实现继承IObjectWithSite接口
2.实现此接口并在SetSite方法里加上所要挂载的事件
3.处理事件
4.注册此BHO到注册表中HKLM下的Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Browser Helper Objects;(HKCR下的CLSIDs是根据上面的路径自动注册的)
5.net 下须设置此BHO项目的 配置属性_>生成 中为Interop注册为True,这样才能将.net 类库文件注册到COM

删除BHO 
打开注册表项到:HKLM下的Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Browser Helper Objects 可以看到下面有一些CLSID,这些值对应相关的插件,点击可以在默认值后看到相关插件的名称!可以复制相关CLSID到注册表中搜索相关CLSID,找到后打开InprocServer32可以看到相关文件的路径,至于DLL文件等可以用UEDIT32.exe工具打开查看具体信息,当然也可以用修改程序类的eXeScope.exe研究一下!
请根据具体情况删除相关键值和相关文件!

 


评论