过河的卒子

我们都是过了河的卒子,不能回头!!!
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 这样的接口,以及在对象一端实现像 IOleClientSite 这样的接口,及在对象一端实现像 IOleObject 这样的接口。通过调用 IOleObject 上的方法,容器使对象知道他的宿主环境。

  当容器是 Internet Explorer (或支持 Web 的 视窗系统 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 还是 视窗系统 Explorer;

  2、获得处理 WebBrowser 对象的 IWebBrowser2 接口;

  3、捕捉 WebBrowser 的特定事件;

  4、访问正在查看的文件,确定他是 HTML 文件;

  5、管理显示 HTML 原始码的对话框窗口。

  第一步在 DllMain() 编码时完成。然而,我们是在 SetSite() 中获得指向 WebBrowser 对象的指针。下面我们来看一看这些步骤的周详内容。

 检测调用者
  像前面提到的,如果你运行的外壳版本不低于471,BHO 就既能被Internet Explorer又能被视窗系统 Explorer引用。在这个例子里,我们要是涉及一个专对 HTML 页面起作用的助手对象,所以他应该对 视窗系统 Explorer 不起所有作用。一个不想被特定的调用者加载的 DLL 能在他发现谁是调用者后,简单地在他的 DllMain() 函数中返回 False。当你将 NULL 作为 API 函数 GetModuleFileName() 的第一个参数调用他时,他返回调用 DLL 的模块名。这个参数是你想知道名字的模块的句柄。NULL 表示你想知道调用 DLL 的进程的名字。

  if (dwReason == DLL_PROCESS_ATTACH)

  {

   TCHAR pszLoader[MAX_PATH];

   GetModuleFileName(NULL, pszLoader, MAX_PATH);

   _tcslwr(pszLoader);

   if (_tcsstr(pszLoader, _T("explorer.exe")))

   return FALSE;

  }

  你知道了进程的名字,你就能在他是 视窗系统 Explorer 时退出加载过程。注意采取更严格的淘汰是非常危险的。实际上,其他进程可能视为一个正常的原因调用他但被拒绝。第一个受害者是 regsvr32.exe,这个程式用来自动注册对象。如果 你是用不同的测试条件,比如说,只允许 Internet Explorer 能执行:

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

  你就不能注册 DLL 了。实际上,当 regsvr32.exe 试图加载 DLL 以调用DllRegisterServer() 函数时,调用被拒绝。

同 WebBrowser 建立联系
  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 窗口并改动他的尺寸。那个挂钩,被用以提供一个热键,以方便用户显示或隐藏 HTML 代码窗口。

从浏览器中获得事件
  当你用 Internet Explorer 浏览 URL 时,浏览器需要首先完成两件事:下载引用的文件并为他准备宿主环境。用另一句话说,他必须为文件初始化对象模型并使之在外部可用。依赖于文件的类型,这意味着要么加载一个注册为处理此类文件的 Microsoft ActiveX 服务程式,要么初始化内部组件来分析文件的内容并填写处理他的对象模型的元素。这就是通过 DHTML 对象模型使 HTML 页面的内容可用时发生的过程。文件完全下载后,一个 DownloadComplete 事件就被引发。这并不一定表明能通过对象模型安全地访问文件内容了。DocumentComplete 事件才表明所有的工作都已完成并且文件已就绪。(注意 DocumentComplete 只是在你第一次访问 URL 时到达。随后,如果你按 F5 或 Refresh 按钮,你只会收到 DownloadComplete 事件。)

  为截获浏览器引发的事件,BHO 需要通过 IConnectionPoint 接口连接到浏览器,并将处理各种事件的函数表传递给 IDispatch。以前获得的指向 IConnectionPointContainer 的指针被用以调用 FindConnectionPoint 方法,他返回一个指向对外接口所需的连接点对象的指针:目前是 DIID_DWebBrowserEvents2。以下代码显示了连接是怎么发生的:

  HRESULT CViewSource::Connect(void)

  {

   HRESULT hr;

   CComPtr<IConnectionPoint> spCP;

   // 为 WebBrowser 事件查赵连接点

   hr = m_spCPC->FindConnectionPoint(DIID_DWebBrowserEvents2, &spCP);

   if (FAILED(hr))

   return hr;

   // 将我们的事件处理器传递给容器。事件发生时容器将调用我们实现的

   // IDispatch 接口函数

   hr = spCP->Advise( reinterpret_cast<IDispatch*>(this), &m_dwCookie);

   return hr;

  }

  通过调用 IConnectionPoint 的 Advise() 方法,BHO 使浏览器知道他希望收到和事件有关的通知。在 COM 事件处理机制下,这实际上就是 BHO 向浏览器提供一个指向他的 IDispatch 接口的指针。浏览器将会回调 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 事件时。


posted on 2008-11-09 22:04  过河的卒子  阅读(292)  评论(0)    收藏  举报