IE里的探索之定制浏览器好助手(上)
青苹果工作室编译
有些情况下你需要特制的、或多或少有些改变的浏览器。这种情况下,你有时候会基于 WebBrowser 控件开发一个完全定制的模块,实现按钮、标题以及用户界面需要的其它东西。这时,你可以自由地在这个浏览器中添加任何新的、非标准的功能。WebBrowser 控件只是浏览器的语法分析引擎。这就是说还有很多用户界面相关的任务必须由你完成:添加地址栏、工具条、历史、状态栏、频道和收藏夹等等。所以,要创建定制的浏览器,你必须编写两种代码:将 WebBrowser 控件变成类似于 Microsoft Internet Explorer 的全功能浏览器的代码和支持你的新功能的代码。如果有一种定制 Internet Explorer 的直接方式不是很好吗?浏览器助手对象 (BHO) 就是做这件事用的。
程序定制
历史上,定制程序行为的第一种方法是子类。通过这种方法,你能改变程序中给定的窗口处理消息的方式以获得不同的行为。这是一种原始的实现方式,然而因为受害者很少意识到,在很长一段时间内这是唯一的选择。
Microsoft Win32 API 出现时,不鼓励使用进程间的子类,而且它们的代码比较难写。然而,如果你有一颗勇敢的心,指针从来就吓不倒你;毕竟,你生活在系统挂钩的环境里,你也许会发现它其实很简单。但不总是这种情况。不管是多么聪明的编程,有一个问题就是每一个 Win32 进程运行在它自己的地址空间内,而有时打破这种进程的边界是不正确的。另一方面,这要求你倾尽全力完成这种编程。更为常见的是,定制可能是指程序本身在设计时就确定的指定功能。
后来,程序在众所周知的、预先指定的磁盘空间寻找附加模块,加载、初始化它们,然后让它们完成预先设计的工作。这就是 Internet Explorer 和它的助手对象的实际工作方式。
浏览器助手对象(BHO)是什么
从这个角度来看,Internet Explorer 就和任何其它使用自己内存空间的 Win32 程序一样。你能使用浏览器助手对象编写组件——进程内的组件对象模型 (COM) 组件——Internet Explorer 在每次启动时加载这些组件。这些组件和浏览器运行在相同的内存上下文里并且能在可用的窗口和模块里完成任何操作。例如,一个 BHO 能检测到浏览器的典型事件,如 GoBack、GoForward 和 DocumentComplete;访问浏览器的菜单和工具条并改变它们;创建窗口以显示当前可视页面上的附加信息;安装挂钩以监视消息和操作。简单地说,BHO 就像我们派出的潜入浏览器的间谍一样工作。
在我们深入到 BHO 核心细节之前,有些情况我需要说明。首先,BHO 连接在浏览器的主窗口上。实际上,这意味着每创建一个浏览器窗口,就创建了该对象的一个新实例。任何 BHO 实例同浏览器实例同时产生、同时消亡。其次,BHO 只存在于 Internet Explorer 4.0 以上版本。
如你运行带有 Active Desktop Shell Update (shell 版本 4.71) 的 Microsoft Windows 98、Windows 2000、Windows 95 或者 Windows NT 4.0 版操作系统,Windows Explorer 也支持 BHO。以后在讨论性能问题和实现压缩的 BHO 时我们会谈到相关内容。
最简单的情况下,BHO 是一个在特定注册表项下注册的进程内 COM 服务器。启动时,Internet Explorer 查找注册表并加载所有将其 CLSID 保存在此处的对象。浏览器初始化对象并要求它提供特定接口。如果发现了这样的接口,Internet Explorer 使用所提供的方法将它的 IUnknown 指针传递给助手对象。图1说明了这一过程。
图 1:Internet Explorer 如何加载并初始化浏览器助手对象。BHO site 是建立通讯所用的 COM 接口。
浏览器可能在注册表里发现一系列 CLSID,并为每一个 CLSID 创建一个进程内的实例。结果,这些对象被加载到浏览器的上下文,并且可以向内置部件一样使用。然而,由于浏览器本质上是基于 COM 的,加载到进程内部并不很重要。从另外一方面看,BHO 确实能实现一系列潜在的功能,比如说实现窗口的子类或安装线程局域挂钩,但 BHO 的主要目的是脱离浏览器核心操作。为了连接浏览器事件,或者说,将事件自动化,助手对象需要建立一个有权限的并且是基于 COM 的通讯通道。所以,BHO 应实现名为 IObjectWithSite 的接口。实际上, Internet Explorer 通过 IObjectWithSite 传递一个指向它自己的 IUnknown 接口指针。随后,BHO 就将这个指针保存起来,并通过它获得其它所需的接口,如 IWebBrowser2、IDispatch 和 IConnectionPointContainer。
可以从另一个方面,即 Internet Explorer 外壳扩展程序的角度来看待 BHO。像你知道的那样,Windows 外壳扩展程序是一个运行中的com,Windows Explorer装载后对文档进行特定操作。例如,显示它的上下文相关菜单时,加载的进程 内的 COM 服务程序。通过编写实现几个 COM 接口的 COM 模块,你就能在上下文 相关菜单中添加菜单项并适当地处理它们。外壳扩展程序必须以 Windows Explorer 能够找到的方式进行注册。浏览器助手对象遵从同样的模式 ;唯一的改变是要实现的接口。导致 BHO 被加载的触发条件是一个小差别。然而,除了实现的不同之外,像下表所说的那样,外壳扩展和 BHO 在本质上是一样的。
表 1. 外壳扩展程序和浏览器助手对象如何实现一般功能
如果你对外壳扩展程序感兴趣,请先参阅 MSDN 在线文档或 CD 文档。
助手对象的生命周期
像我们前面提到的那样,只有 Internet Explorer 支持 BHO。如果你运行了不低于版本 4.71 的外壳,你的 BHO 也可以被 Windows Explorer 载入。这样可以通过一个单一的浏览器并基于同样的用户经验同时浏览 Web 和本地磁盘。下表提供对当前可用的各种外壳版本的一个面向产品的概览。外壳的版本号取决于保存在 shell32.dll 中的版本信息。
表 2. 不同外壳版本对浏览器助手对象的支持
浏览器助手对象在浏览器的主窗口将要显示出来时加载,在窗口消失时卸载。你打开的浏览器窗口越多,创建的 BHO 实例也就越多。即使以命令行方式启动浏览器它也被加载。一般情况下,BHO 实例的数目和运行的 explorer.exe 或 iexplorer.exe 的数目一样多。如果你在文件夹选项里设置了“在不同窗口打开不同文件夹”,每次你打开一个新的文件夹时都会加载 BHO。
图 2. 使用这一设置,每打开一个文件夹就运行 explorer.exe 的一个单独实例 并加载注册了的 BHO。
然而,需要注意的是,这种情况仅仅发生在你从桌面上“我的电脑”图标开始打开文件夹的时候。在这种情况下,每次你转移到另外的文件夹时外壳都调用 explorer.exe。你在两栏视图中开始浏览时不会发生这种情况。实际上,你改变文件夹时外壳并不是启动浏览器的一个新实例,而是简单的创建嵌入视图对象的一个实例。特别是你在地址栏里输入一个新名字以改变文件夹时,无论 Windows Explorer 的视图是一栏还是两栏,浏览都在同一窗口内进行。
对 Internet Explorer 来说情况就简单多了。只有你多次显式地运行 iexplorer.exe 才会产生多个拷贝。当你从 Internet Explorer 中打开新窗口时,每个窗口在一个新的线程中复制,而不是创建一个新的进程,这样就不会重新加载 BHO。
尤其,BHO 最令人感兴趣的特征就是它们是动态的。每次打开 Window Explorer 或 Internet Explorer 的窗口时,它们从注册表里读取已安装的助手对象的 CLSID,然后进行处理。如果你编辑打开浏览器的不同实例的注册表项,就能使浏览器的不同拷贝加载不同的 BHO。这意味着你有了一个非常好的选择以取代编写新的浏览器。你可以在 Microsoft Visual Basic 或 Microsoft Foundation Classes (MFC) 的 frame window 中嵌入 WebBrowser。同时,你有很好的机会布置扩展性很强的浏览应用程序。你可以依赖Internet Explorer的全部功能并尽可能地添加想要的附加功能以满足你的需要。
IObjectWithSite 接口
从这个高层来看待浏览器助手对象,一个概念就清晰的显现出来了:BHO 是一个动态连接库 (DLL),它能附着在 Internet Explorer 的一个新实例上,在某些情况下,也能附着在 Windows Explorer 的实例上。这样的模块能通过容器的现场与浏览器建立联系。
通常,现场是指处于容器和每个被包含的对象之间的中介对象。容器通过它管理包含的对象,并随后使对象的内置功能可用。这种容器和对象之间的基于现场的关系涉及到在容器一端实现像 IOleClientSite 这样的接口,以及在对象一端实现像 IOleObject 这样的接口。通过调用 IOleObject 上的方法,容器使对象知道它的宿主环境。
当容器是 Internet Explorer (或支持 Web 的 Windows Explorer),从性能的角度考虑,需用将这种通讯模式降低到必要的程度。现在对象需要实现更简单更小的叫作 IObjectWithSite 的接口。它只需提供两个方法。
表 3. IObjectWithSite 接口定义
对 BHO 的唯一严格要求就是实现这个接口。注意你要避免从前面所说的函数中返回 E_NOTIMPL。你要么不去实现这个接口,要么正确地编写它的方法。
编写浏览器助手对象
浏览器助手对象是进程内的 COM 服务程序,那么还有什么比 Active Template Library (ATL)更适合用来编写它呢?选择 ATL 的另一个原因是它已经默认提供了一个很好的 IObjectWithSite 接口。还有,在 ATL COM 向导内置支持的预定义对象类型中,有一个 Internet Explorer 对象,正好是 BHO 的对象类型。实际上,ATL Internet Explorer 对象是一个简单的对象。就是说,一个 COM 服务程序,支持 IUnknown 和自我注册加上 IObjectWithSite。如果你在 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,它是提供 IObjectWithSite 的基本实现的一个 ATL 模板类。(参见 Microsoft Visual Studio 98 中 ATL\INCLUDE 目录下的 atlcom.h。) 通常不需要重载 GetSite() 成员函数。相反,GetSite() 的已有代码常常(即使并不总是)需要按用户要求来重写。实际上,ATL 只是简单地将 IUnknown 指针保存到一个叫 m_spUnkSite 的成员变量里。
在文章的其余部分我们将讨论一个相当复杂的 BHO 例子。这个对象只附加到 Internet Explorer 上,并显示一个带有所查看的页面源代码的文本框。当你改变页面时,此代码窗口自动更新,并且,在 Internet Explorer 显示的不是一个 HTML 页面时变成灰色。你对 HTML 代码的任何修改会立即在浏览器中反映出来。动态 HTML (DHTML) 使这种魔术成为可能。这样的代码窗口可以隐藏,并在以后通过热键召回。可见时,它同 Internet Explorer 分享整个桌面工作区,并能像图 3 所示的那样适当地改变尺寸。
图 3. 工作中的浏览器助手对象。它附着在 Internet Explorer 上并显示所查看页面的源代码。它同时允许你修改代码 (但不能保存)。
这个例子的关键点是访问 Internet Explorer 的浏览机制,而它不过是 WebBrowser 控件的一个实例。这个例子可以分为以下五个主要步骤:
1、检测谁加载了对象,是 Internet Explorer 还是 Windows Explorer;
2、获得处理 WebBrowser 对象的 IWebBrowser2 接口;
3、捕获 WebBrowser 的特定事件;
4、访问正在查看的文档,确定它是 HTML 文档;
5、管理显示 HTML 源代码的对话框窗口。
第一步在 DllMain() 编码时完成。然而,我们是在 SetSite() 中获得指向 WebBrowser 对象的指针。下面我们来看一看这些步骤的详细内容。