Tuesday, January 11, 2005

 

IE and Browser Helper Objects (BHOs)

1.
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,

   public CComCoClass,

   public IObjectWithSiteImpl,

   public IDispatchImpl
              &LIBID_HTMLEDITLib>

像你看到的那样,向导已经使这个类继承 IObjectWithSiteImpl,它是提供 IObjectWithSite 的基本实现的一个 ATL 模板类。(参见 Microsoft Visual Studio 98 中 ATLINCLUDE 目录下的 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 对象的指针。下面我们来看一看这些步骤的详细内容。

检测调用者
  像前面提到的,如果你运行的外壳版本不低于471,BHO 就既能被Internet Explorer又能被Windows Explorer引用。在这个例子里,我们要是涉及一个专对 HTML 页面起作用的助手对象,所以它应该对 Windows 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;

  }

  你知道了进程的名字,你就能在它是 Windows 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 m_spWebBrowser2;

  CComQIPtr
     &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 spCP;

   // 为 WebBrowser 事件查赵连接点

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

   if (FAILED(hr))

   return hr;

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

   // IDispatch 接口函数

   hr = spCP->Advise( reinterpret_cast(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 事件时。

访问文档对象
  现在 BHO 引用了 Internet Explorer 的 WebBrowser 控件并已经连接到浏览器 以接收它产生的事件。在 Web 页面被完全下载并被正确地初始化之后,现在终于可以通过 DHTML 文档对象模型访问它了。WebBrowser 的 Document 属性返回一个指向文档对象的 IDispatch 接口的指针:

  CComPtr pDisp;

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

  get_Document() 方法提供的只是一个指向接口的指针。我们需要确定在 IDispatch 指针后面确实是一个 HTML 文档对象。如果使用 Visual Basic,以下是等价的代码:

  Dim doc As Object

  Set doc = WebBrowser1.Document

  If TypeName(doc)="HTMLDocument" Then

   ' Get the document content and display

  Else

   ' Disable the display dialog

  End If

  现在我们需要判断 get_Document() 返回的 IDispatch 指针的实质。Internet Explorer 不仅是一个 HTML 浏览器,还能处理任何 ActiveX 文档 ;即任何有作为 ActiveX 文档服务程序的应用程序支持的文档。这样一来,就不能保证查看的文档的确是一个 HTML 页面。

  有一个解决办法就是查看 URL 并检查 URL 的扩展名。但该如何处理 Active Server Pages (ASP) 或一个暗含指向 HTML 页面的 URL?如果你使用了像 about 或 res 这样的定制协议又该如何?

  我们决定采取另一种方式,它和上面的 Visual Basic 代码性质相同。这种想法就是,如果 IDispatch 指针确实指向一个 HTML 文档,对 IHTMLDocument2 接口的访问就能成功地返回。IHTMLDocument2 是综合了 DHTML 对象模型为 HTML 页面实现的所用功能的接口。以下代码片断说明如何进行这样的判断:

  CComPtr pDisp;

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

  CComQIPtr spHTML;

  spHTML = pDisp;

  if (spHTML) {

   // 取得文档的内容并显示它

  }

  else {

   // 禁止代码窗口控件

  }

  如果访问 IHTMLDocument2 接口失败,spHTML 指针为 NULL。否则,我们就可以正常访问 DHTML 对象模型的方法和属性了。

  现在的问题是如何获得已显示的页面的源代码。幸好,基本的 DHTML 知识就足以做到这一点。由于 HTML 页面将它所有的内容包含在 BODY> 标记中,DHTML 对象模型要求你首先获得指向 Body 对象的指针:

  CComPtr m_pBody;

  hr = spHTML->get_body(&m_pBody);

  奇特的是,DHTML 对象模型不让你知道在 BODY> 之前的标记,例如 HEAD> 的原始内容。这些内容已经被处理并被保存到一系列属性中了,但你依然不能得到一个最初的 HTML 文件的原始内容。然而,现在 body 能告诉我们的就足够了。我们需要将 outerHTML 属性的内容读取到一个 BSTR 变量里以获得包含在 BODY> 和 /BODY> 之间的 HTML 代码。

  BSTR bstrHTMLText;

  hr = m_pBody->get_outerHTML(&bstrHTMLText);

  现在,在代码窗口中显示文本的工作就是创建窗口、将字符串从 Unicode 转换为 ANSI,并如图 3 中所示设置编辑框。以下是完成这些工作的全部代码:

  HRESULT CViewSource::GetDocumentContent()

  {

   USES_CONVERSION;

  

   // 获得 WebBrowser 文档对象

   CComPtr pDisp;

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

   if (FAILED(hr))

   return hr;

   // 验证我们得到了一个指向 IHTMLDocument2 接口的指针

   // 我们查询 IHTMLDocument2 接口 (通过灵巧指针)

   CComQIPtr 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 键或浏览器的 Refresh 按钮刷新视图后,所有的修改会立即丢失。通过对 DownloadComplete 事件进行处理你能同时刷新代码窗口。(注意 DownloadComplete 事件比 DocumentComplete 事件先到达) 这时,你应该忽略第一次下载页面时产生的 DownloadComplete 而只考虑刷新时产生的事件。一个简单的布尔成员例如 m_bDocumentCompleted 可以用来区分这两种情况。

管理代码窗口
  用以显示当前页面的 HTML 源代码的代码窗口是 ATL 的另一个基本元素,一个可以在 ATL 对象向导的 Miscellaneous 页里找到的对话框窗口。我们重置这个窗口的尺寸以响应 WM_INITDIALOG 消息,并使此窗口占据桌面工作区,即屏幕的可用部分减掉任务栏可能占据位置最下面的部分。

  浏览器启动时此窗口可能出现也可能不出现。默认情况下它是出现的,但可以通过清除复选框 Show window at startup 禁止。如果你愿意也可以关掉它。随后,可以在任何时候按 F12 键将其召回。F12 由我们在 SetSite() 中安装的键盘挂钩捕获。

  启动设置完全按照 Microsoft 指示保存在注册表里。读写注册表时我们没有使用 Win32 函数,而是使用了新的 Shell Lightweight API (shlwapi.dll),这样 可以避免打开和关闭相应的注册表项的麻烦:

  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 和 Active Desktop 中引入的,从 Windows 98 开始成为标准的系统组件。这些函数比相应的 Win32 函数更直接,适合只进行一次读写时使用。

注册助手对象
  BHO 是 COM 服务程序,应该同时以 COM 服务程序和 BHO 注册。ATL 模板为你提供了完成第一项注册的注册脚本代码 (RGS) 。以下是完成 BHO 注册的 RGS 代码。(CLSID 是从例程序中得到的。)

  HKLM {

   SOFTWARE {

   Microsoft {

   Windows {

   CurrentVersion {

     Explorer {

      'Browser Helper Objects' {

      ForceRemove {1E1B2879-88FF-11D2-8D96-D7ACAC95951F}

  }}}}}}}

  注意 ForceRemove 子句使键在对象取消注册时被删除。

  在 Browser Helper Objects 键下是所有安装的助手对象。浏览器从不将这些放入缓存,所以安装并测试 BHO 是一个很快的过程。

BHO小结
  本文中,我们介绍了浏览器助手对象,一种相对来说比较新的、在浏览器的地址空间内直接引入你的代码的有效方式。你所要做的就是编写一个 COM 服务程序以支持 IObjectWithSite 接口。这里,你的模块从所有预定目的来看都是浏览器机构中的一个组件。本文中我们建立的例程序还涉及到如 COM 事件、动态 HTML 对象模型以及 WebBrowser 编程接口等内容。我们认为它演示了 BHO 的功能,同时提供了一个创建你自己对象的实用平台。如果你需要知道浏览器正在显示什么,你一定需要熟悉事件并进一步了解 WebBrowser。现在你知道:预先警告是为了早做准备。作为结语,我们提醒你 BHO 对 Windows Explorer 非常有用,而且,通过 WebBrowser,它能由你的代码驱动。

2.
用C#制作IE浏览器插件和远程控制IE

作者:Steven M. Cohn
原文:http://weblogs.asp.net/stevencohn/articles/60948.aspx
http://weblogs.asp.net/stevencohn/archive/2004/01/23/62117.aspx

(按:高人就是多呀,什么技术都有人玩)

I'm currently writing a new IE utility that requires a very tight relationship with the inner workings of Internet Explorer. Ostensibly, I needed to hook into the event model of the browser as it surfs the Web, moving from URL to URL. When I recognize the current URL as contained within a predefined collection, I want to take some action. While researching, I came across IE Browser Helper Objects and discovered how to implement them using .NET. After stumbling through a few ATL examples (which is nothing short of hell after looking at C# for a couple of years) I was pleasantly surprised at how easy it was to do in .NET.

A Browser Helper Object, BHO, is nothing more than a COM object loaded for each IE window. As a window is created, it creates its own copy of the BHO; and, when that window is closed, it destroys its copy of the BHO.

interface IObjectWithSite

To begin, A BHO must implement IObjectWithSite - a stupid name for a very tidy interface. The GUID must be exactly as shown here, otherwise it won't work.

using System;
using System.Runtime.InteropServices;
[
ComVisible(true),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown),
Guid("FC4801A3-2BA9-11CF-A229-00AA003D7352")
]
public interface IObjectWithSite
{
[PreserveSig]
int SetSite ([MarshalAs(UnmanagedType.IUnknown)]object site);
[PreserveSig]
int GetSite (ref Guid guid, out IntPtr ppvSite);
}

In general, you would never need to override the GetSite method. However, the SetSite method is invoked when the BHO is instantiated and when it is destroyed. In the former case, the siteIWebBrowser2. In the latter case, the site argument is null.

Here's an example of how you might implement SetSite:

if (site == null)
{
browser.DocumentComplete -=
new DWebBrowserEvents2_DocumentCompleteEventHandler(
this.OnDocumentComplete);
browser = null;
}
else
{
browser = (WebBrowser)site;
browser.DocumentComplete +=
new DWebBrowserEvents2_DocumentCompleteEventHandler(
this.OnDocumentComplete);
}

The code above shows the two states of the site argument. If null, then unregister the event handler and dereference the browser instance. Otherwise, reference the browser instance and register an event handler. Obviously, you can register handlers for any events throws by the browser (and there are a ton!)

OnDocumentComplete is a private method with this signature:

private void OnDocumentComplete (object frame, ref object urlObj);

While I haven't investigated the first argument, the second argument is an object from which you can extract the current URL using urlObj.ToString().

Implementing IObjectWithSite

Remember that a BHO is a COM object, so we need to add attributes to the class definition. One attribute is a GUID that you will need to generate. Once you generate this GUID, you can use that value forever for this one class. Visual Studio .NET 2003 has a built-in utility found on the Tools menu to create a new GUID.

Here an example of implementing the BHO.

[
ComVisible(true),
ClassInterface(ClassInterfaceType.None),
Guid("D5F20021-2084-4564-9449-BF195C577FDC")
]
public class SiteWatcherBHO : IObjectWithSite
{
}

Auto-Register the COM object

To register this COM object, you can use the .NET regasm tool. This tool will interogate the attribute you applied to the class and register the DLL as a COM component in the System Registry.

But this doesn't bind the DLL as a BHO. You first need to define a couple of special methods used only during COM registration and unregistration.

When you register the DLL using the command regasm /codebase, the regasm tool searches for a method with the ComRegisterFunction attribute and, if found, will execute it. Here is where you need to add custom code to set up the system registry.

When you register the DLL using the command regasm /unregister, the regasm tool searches for a method with the ComUnregisterFunction attribute and, if found, will execute it. Here is where you need to add custom code to delete the registry keys you created in the ComRegisterFunction method.

[ComRegisterFunction]
public static void RegisterBHO (Type t)
{
}

[ComUnregisterFunction]
public static void UnregisterBHO (Type t)
{
}

All the first method needs to do is add the registry key:

Software\Microsoft\Windows\CurrentVersion ...
\Explorer\Browser Helper Objects

Then define a key below that, naming it the value of your GUID, for example:

Software\Microsoft\Windows\CurrentVersion ...
\Explorer\Browser Helper Objects ...
\{D5F20021-2084-4564-9449-BF195C577FDC}

The second method simply deletes this GUID key below the Browser Helper Objects key. You should not delete the Browser Helper Objects key because, most likely, there will be other third-party BHOs already defined there.

Debugging

Finally, you can debug the BHO code. But there is a catch. Since it's managed code and obviously IE is not managed code, it's rather difficult to jump in right at the first instantiation of the first BHO. But that's not a big deal.

I typically have my IE home page set to about:blank. That way I can start up the browser as fast as possible and go where I need to. So, start up the first IE window. The from VS.NET, use the Attach to Process item in the Debug menu to attach to iexplore.exe. Set breakpoints in your BHO. To break within the constructor, just open a second IE window.

(按:C#的玩法和我们以前C++差不多,不过总觉得C#要方便些,咳咳,该死的感冒病毒)

Here's a very simple way of automating a running instance of IE from a remote process:

Add a reference in your project to SHDocVw.dll. This is a COM component named Microsoft Internet Controls. It contains the definitions of the InternetExplorer and ShellWindows classes.
Add a using clause in your source file to reference SHDocVw.
Use the following code fragment to find the first occurance of a running instance of Internet Explorer:

SHDocVw.InternetExplorer browser = null;
string filnam;

SHDocVw.ShellWindows shellWindows =
new SHDocVw.ShellWindowsClass();

foreach (SHDocVw.InternetExplorer ie
in shellWindows)
{
filnam = Path.GetFileNameWithoutExtension(
ie.FullName).ToLower();

if (filnam.Equals("iexplore"))
{
browser = ie;
break; // i hate 'break' but it's easy here
}
}
Notice that we're testing for the executing file name. Otherwise, we might grab an instance of Windows Explorer (explore.exe) which is just another instance of Internet Explorer.

Finally, use can use the InternetExplorer.Navigate method to force the window to redirect to your chosen URL.
Possible enhancements might include a way of determining which IE window - of many - is stacked ontop of the others or which one may have started the remote application in the first place. Also, if IE is not currently running, then you'll need to invoke a new instance.



<< Home

This page is powered by Blogger. Isn't yours?