Rover12421's Blog

The End.

用VisualStudio2005生成浏览器帮助对象

摘要:本文说明了如何使用 Microsoft Visual Studio 2005 创建一个简单的“浏览器帮助程序对象”(BHO),即实现 IObjectWithSite 接口并将其自身附加到 Internet Explorer 的一种“组件对象模型”(COM) 对象。本文逐步说明了如何创建入门级 BHO。首先,BHO 会在 Internet Explorer 加载文档时显示消息“大家好!”。然后,BHO 被扩展为从已加载页面删除图像。本文面向的是想要了解如何扩展浏览器功能以及如何为 Internet Explorer 创建 Web 开发人员工具的开发人员(本文还包含指向英文网页的链接)。

简介

本文凭借 Microsoft Visual Studio 2005 和“活动模板库”(ATL) 来开发使用 C++ 的 BHO。我们之所以决定使用 ATL,是因为它方便地实现了我们可以按需进行扩展的基本样板。还有其他方法可供用于创建 BHO(例如,使用“Microsoft 基础类”(MFC) 或 Win32 API 和 COM),但 ATL 是为我们自动处理许多细节的轻型库,包括建立含有 BHO 类标识符 (CLSID) 的注册表。

ATL 的另一个优势在于它的 COM 感知智能指针类(例如,CComPtr 和 CComBSTR),这些类可管理 COM 对象的生命周期。例如,CComPtr 在赋值时会调用 AddRef,而在对象被销毁或超出范围时会调用 Release。智能指针简化了代码并且有助于避免内存泄漏。当在单个方法范围内使用时,它们的稳定性和可靠性尤为有用。

本文的第一部分向您逐步介绍了如何实现简单的 BHO 并验证它是否由 Internet Explorer 加载。接下来的部分将说明如何将 BHO 连接到浏览器事件,最后一部分将介绍与更改网页外观的 DHTML 文档对象模型 (DOM) 的简单交互。

概述

到底什么是浏览器帮助程序对象 (BHO)?简言之,BHO 是将自定义功能添加到 Internet Explorer 的轻型 DLL 扩展。BHO 还可以将功能添加到 Windows 资源管理器外壳程序(尽管这并不常见,也不是本文重点)。

BHO 通常并不提供其自身的任何用户界面 (UI)。它们而是通过在后台响应浏览器事件和用户输入数据来发挥作用。例如,BHO 可以拦截弹出窗口、自动填充窗体或为鼠标手势添加支持。有一种常见误解认为工具栏扩展项需要 BHO;但如果将 BHO 与工具栏配合使用,则可以实现更丰富的用户体验。

注意 BHO 对于最终用户和开发人员同样都是便捷的工具;但由于 BHO 被赋予了对浏览器和 Web 内容的相当大的控制能力,并且它们通常都处于未检测的状态,因此用户应十分谨慎地从可靠来源获取和安装 BHO。

BHO 的生命周期与它所交互的浏览器实例的生命周期相等。在 Internet Explorer 6 和早期版本中,这意味着为每个新的顶层窗口都创建(和销毁)一个新 BHO。另一方面,Internet Explorer 7 会为每个选项卡都创建和销毁一个新 BHO。BHO 不是由承载 WebBrowser 控件的其他应用程序加载,也不是由 HTML 对话框之类的窗口加载。

BHO 的主要要求是实现 IObjectWithSite 接口。此接口提供了一个方法(即 SetSite),此方法方便了与 Internet Explorer 的初始通信,并会在其将要释放时通知 BHO。我们实现此接口,然后将 BHO 的 CLSID 添加到注册表中,由此创建一个简单的浏览器扩展。

让我们开始吧。

建立项目

通过 Microsoft Visual Studio 2005 创建 BHO 项目:

1.在“文件”菜单上,单击“新建项目…”。

随即出现“新建项目”对话框。此对话框将列出 Visual Studio 可以创建的应用程序类型。

2.在 Visual C++ 节点下,选中“ATL”(如果它未被选中),然后从 Visual C++ 项目类型中选择“ATL 项目”。将项目命名为“HelloWorld”并使用默认位置。单击“确定”。

3.在“ATL 项目向导”中,确保服务器类型为“动态链接库 (DLL)”,然后单击“完成”。

此时,Visual Studio 已为 DLL 创建了样板。现在,我们将添加实现 BHO 的 COM 对象。

4.在“解决方案资源管理器”面板上,右键单击该项目,然后从“添加”子菜单中选择“类…”。

5.选中“ATL 简单对象”,然后单击“添加”。

随即出现“ATL 简单对象向导”。

6.在“ATL 简单对象向导”的“名称”中,键入“HelloWorldBHO”以作为“短名称”。

余下的名称将自动填充。

7.在“ATL 简单对象向导”的“选项”中,选中“线程模型”下的“Apartment”、“聚合”下的“否”、“接口”下的“双重”以及“支持”下的“IobjectWithSite”。

image

8.单击“完成”。

以下文件将作为此项目的一部分创建。

•HelloWorldBHO.h – 此头文件包含 BHO 的类定义。

•HelloWorldBHO.cpp – 此源文件是项目的主文件并且包含 COM 对象。

•HelloWorld.cpp – 此源文件用于实现通过 DLL 提供 COM 对象的导出。

•HelloWorld.idl – 此源文件可用于定义自定义 COM 接口。对于本文,我们将不更改此文件。

•HelloWorld.rgs – 此资源文件包含注册和取消注册 DLL 时编写和删除的注册表项。

实现基本要素

“ATL 项目向导”提供了 SetSite 的默认实现。尽管 IObjectWithSite 的接口合约暗示了此方法可以在必要时被反复调用,但确切来说,Internet Explorer 只调用此方法两次;一次用于建立连接,另一次则是在浏览器退出时。特别要提的是,我们 BHO 中的 SetSite 实现将执行以下操作:

•存储对站点的引用。在初始化期间,浏览器将 IUnknown 指针传递给顶层 WebBrowser 控件,然后 BHO 将对它的引用存储在一个专用成员变量中。

•释放目前被占用的站点指针。Internet Explorer 传递 NULL 时,BHO 必须释放所有接口引用并且断开与浏览器的连接。

在处理 SetSite 的过程中,BHO 将根据需要执行其他初始化和非初始化。例如,您可以建立与浏览器的连接点以便接收浏览器事件。

HelloWorldBHO.h

在 Visual Studio 的“解决方案资源管理器”中双击打开 HelloWorldBHO.h。

首先,包含 shlguid.h。此文件定义了 IWebBrowser2 的接口标识符和稍后在项目中使用的事件。

include // IID_IWebBrowser2、DIID_DWebBrowserEvents2 等。

接下来,在 CHelloWorldBHO 类的公共部分声明 SetSite。

STDMETHOD(SetSite)(IUnknown *pUnkSite);

STDMETHOD 宏是一个将方法标记为虚拟方法并且确保其具有适用于公共 COM 接口的调用约定的 ATL 约定。它有助于区分 COM 接口和该类中可能存在的其他公共方法。实现成员方法时同样也会使用 STDMETHODIMP 宏。

最后,在类声明的专用部分中声明某成员变量以存储浏览器站点。

以下是引用片段:
private:
CComPtr m_spWebBrowser;
HelloWorldBHO.cpp
现在,切换到 HelloWorldBHO.cpp 并为 SetSite 插入以下代码。
STDMETHODIMP CHelloWorldBHO::SetSite(IUnknown* pUnkSite)
{
if (pUnkSite != NULL)
{
// 缓存指向 IWebBrowser2 的指针。
pUnkSite->QueryInterface(IID_IWebBrowser2, (void**)&m_spWebBrowser);
}
else
{
// 在此释放缓存的指针和其他资源。
m_spWebBrowser.Release();
}
// 返回基类实现
return IObjectWithSiteImpl::SetSite(pUnkSite);
}

初始化期间,浏览器将传递一个对其顶层 IWebBrowser2 接口(我们对其进行缓存处理)的引用。非初始化期间,浏览器将传递 NULL。为避免内存泄漏和循环引用计数,此时释放所有指针和资源非常重要。最后,我们调用基类实现以便它可以履行接口合约的其余部分。

HelloWorld.cpp

加载 DLL 后,系统将通过 DLL_PROCESS_ATTACH 通知调用 DllMain 函数。由于 Internet Explorer 大量使用多线程,因此,对 DllMain 的频繁的 DLL_THREAD_ATTACH 和 DLL_THREAD_DETACH 通知会降低扩展和浏览器进程的整体性能。既然该 BHO 不需要线程级的跟踪,我们可以在 DLL_PROCESS_ATTACH 通知期间调用 DisableThreadLibraryCalls 以避免新线程通知的额外开销。

在 HelloWorld.cpp 中,如下编写 DllMain 函数的代码:

以下是引用片段:
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。Visual Studio 可在生成项目时注册 CLSID。

注意 在 Windows Vista 上,Visual Studio 需要提升的特权才能与注册表进行交互。请确保通过在“开始”菜单中右键单击 Microsoft Visual Studio 2005 并选择“以管理员身份运行”来启动开发环境。

此 BHO 的 CLSID 可在 HelloWorld.idl 中找到(位于如下所示的代码块中):

以下是引用片段:
importlib(“stdole2.tlb”);
[
uuid(D2F7E1E3-C9DC-4349-B72C-D5A708D6DD77),
helpstring(“HelloWorldBHO Class”)
]

请注意,此文件包含三个 GUID;我们需要的是用于类的 CLSID,而不是用于库的 CLSID 或接口 ID。

创建自行注册的 BHO:

1.从 Visual Studio 中的“解决方案资源管理器”打开 HelloWorld.rgs。

2.将以下代码添加到文件末尾:

以下是引用片段:
HKLM {
NoRemove SOFTWARE {
NoRemove Microsoft {
NoRemove Windows {
NoRemove CurrentVersion {
NoRemove Explorer {
NoRemove ‘浏览器帮助程序对象’ {
ForceRemove ‘{D2F7E1E3-C9DC-4349-B72C-D5A708D6DD77}’ = s ‘HelloWorldBHO’ {
val ‘NoExplorer’ = d ‘1’
}
}
}
}
}
}
}
}

3.将上述 ForceRemove 后面的 GUID 替换为 BHO 的 CLSID(可在 HelloWorld.idl 中找到)。

切勿替换大括号。

4.保存文件,然后重新生成解决方案。(按 F6。)

Visual Studio 将自动注册该对象。

NoRemove 关键字表示取消注册 BHO 时将不删除该注册表项。除非您指定了此关键字,否则将删除空的注册表项。ForceRemove 关键字表示将删除该注册表项以及它所包含的任何值和子项。ForceRemove 还将导致在注册 BHO 后重新创建该注册表项(如果它已存在)。

既然此 BHO 专用于 Internet Explorer,那么我们指定 NoExplorer 值以防止 Windows Explorer 加载它。值和类型是什么都不重要,只要 NoExplorer 条目存在,Windows Explorer 就不会加载 BHO。

现在,您就可以从 Visual Studio 2005 中的“生成”菜单生成解决方案。

进行试用

为了进行快速测试,请在 SetSite 中设置一个断点,然后按 F5 启动调试程序。当出现“调试会话的可执行文件”对话框时,选择“默认的 Web 浏览器”,然后单击“确定”。如果 Internet Explorer 不是您的默认浏览器,则可以浏览查找可执行文件。

注意 在 Windows Vista 上,Internet Explorer 的“保护模式”功能将启动另一个进程,然后退出,这样会给调试带来一点难度。您可以通过以下两种方式轻松关闭当前会话的“保护模式”:从管理进程(例如 Visual Studio)启动浏览器,或者创建一个本地 HTML 文件并将其指定为 Internet Explorer 的命令行参数。

浏览器启动时,将加载 BHO 的 DLL。命中断点时,请注意是否设置了 pUnkSite 参数。再次按 F5 以继续加载主页。

关闭浏览器以验证是否通过 NULL 再次调用了 SetSite。

对事件做出响应

既然已经确认了 Internet Explorer 可以加载和运行 BHO,那就让我们在所举示例的基础上再深入一些,将 BHO 扩展到响应浏览器事件。在本部分中,我们介绍如何使用 ATL 为 DocumentComplete(在页面加载后显示一个消息框)实现一个事件处理程序。

为接到事件通知,BHO 建立一个与浏览器之间的连接点;为响应这些事件,它将实现 IDispatch。根据 DocumentComplete 的文档,该事件有两个参数:pDisp(IDispatch 的指针)和 pUrl。这些参数将作为事件的一部分传递给 IDispatch::Invoke;但手动析取这些事件参数并非一项简单的任务,并且易于出错。幸好 ATL 提供了一个默认实现,可以帮助简化这个事件处理逻辑。

HelloWorldBHO.h

首先通过包含 exdispid.h(为浏览器事件定义调度 ID)处理 HelloWorldBHO.h。

include // DISPID_DOCUMENTCOMPLETE 等。

接下来,从 IDispEventImpl 基类进行派生,该基类为处理事件提供了除 Invoke 之外的另一个简单安全的替代方法。IDispEventImpl 与事件汇映射配合工作,以将事件路由到相应的处理程序函数。我们明确说明,想要使用以下类定义(突出显示)处理由 DWebBrowserEvents2 接口定义的事件。

以下是引用片段:
class ATL_NO_VTABLE CHelloWorldBHO :
public CComObjectRootEx,
public CComCoClass,
public IObjectWithSiteImpl,
public IDispatchImpl,
public IDispEventImpl<1, CHelloWorldBHO, &DIID_DWebBrowserEvents2, &LIBID_SHDocVw, 1, 1>

接下来,添加将事件路由到新的 OnDocumentComplete 事件处理程序方法的 ATL 宏,该事件处理程序方法采用的是 DocumentComplete 事件所定义的相同参数和顺序。将以下代码放置到该类的公共部分。

以下是引用片段:
BEGIN_SINK_MAP(CHelloWorldBHO)
SINK_ENTRY_EX(1, DIID_DWebBrowserEvents2, DISPID_DOCUMENTCOMPLETE, OnDocumentComplete)
END_SINK_MAP()
// DWebBrowserEvents2
void STDMETHODCALLTYPE OnDocumentComplete(IDispatch *pDisp, VARIANT *pvarURL);

提供给 SINK_ENTRY_EX 宏 (1) 的数字指的是 IDispEventImpl 类定义的第一个参数,在必要时用于区分来自不同接口的事件。另请注意,不能从该事件处理程序返回值;这是因为 Internet Explorer 无论怎样都会忽略从 Invoke 返回的值。

最后,添加一个专用成员变量,以跟踪各对象是否已建立了与浏览器的连接。

以下是引用片段:
private:
BOOL m_fAdvised;
HelloWorldBHO.cpp

要通过事件映射将事件处理程序连接到浏览器,可在处理 SetSite 期间调用 DispEventAdvise。同样,使用 DispEventUnadvise 断开连接。

以下是 SetSite 的新实现:

以下是引用片段:
STDMETHODIMP CHelloWorldBHO::SetSite(IUnknown* pUnkSite)
{
if (pUnkSite != NULL)
{
// 缓存指向 IWebBrowser2 的指针。
HRESULT hr = pUnkSite->QueryInterface(IID_IWebBrowser2, (void **)&m_spWebBrowser);
if (SUCCEEDED(hr))
{
// 注册以从 DWebBrowserEvents2 中汇集事件。
hr = DispEventAdvise(m_spWebBrowser);
if (SUCCEEDED(hr))
{
m_fAdvised = TRUE;
}
}
}
else
{
// 取消注册事件汇。
if (m_fAdvised)
{
DispEventUnadvise(m_spWebBrowser);
m_fAdvised = FALSE;
}
// 在此释放缓存的指针和其他资源。
m_spWebBrowser.Release();
}
// 调用基类实现。
return IObjectWithSiteImpl::SetSite(pUnkSite);
}

最后,添加一个简单的 OnDocumentComplete 事件处理程序。

以下是引用片段:
void STDMETHODCALLTYPE CHelloWorldBHO::OnDocumentComplete(IDispatch *pDisp, VARIANT *pvarURL)
{
// 从站点检索顶级窗口。
HWND hwnd;
HRESULT hr = m_spWebBrowser->get_HWND((LONG_PTR*)&hwnd);
if (SUCCEEDED(hr))
{
// 加载页面时输出消息框。
MessageBox(hwnd, L"大家好!“, L"BHO”, MB_OK);
}
}

请注意,消息框会将站点的顶层窗口用作其父窗口,而不仅仅是通过该参数传递 NULL。在 Internet Explorer 6 中,NULL 父窗口并不阻止应用程序,也就是说,在消息框等待用户输入时用户可以继续与浏览器交互。在某些情况下,这会导致浏览器挂起或崩溃。在 BHO 需要显示 UI 的这种少见情况下,应始终通过指定指向父窗口的句柄来确保该对话框为应用程序模态。

再一次试用

通过按 F5 再次启动 Internet Explorer。文档加载后,BHO 将显示其消息。

image

继续浏览以观察消息框出现的时间及频率。请注意,不仅在加载页面时会显示 BHO 警告,在通过单击“上一步”按钮重新加载该页面时也会显示 BHO 警告;但在单击“刷新”按钮时不会显示该警告。在 Internet Explorer 7 中,对于每个新的选项卡都会显示该消息框。

该事件在页面被下载和解析后激发,但是在 window.onload 事件触发之前激发。在有多个框架的情况下,该事件将激发多次,结束时后面跟随的是顶层框架。在随后的代码中,通过将事件的 pDisp 参数所传递的对象与在 SetSite 中进行缓存处理的顶层浏览器进行比较来检测出这一系列事件的最后事件。

操作 DOM

以下 JavaScript 代码演示了 DOM 的基本操作。它通过将图像的样式对象的 display 属性设置为“none”在网页上隐藏图像。

以下是引用片段:
function RemoveImages(doc)
{
var images = doc.images;
if (images != null)
{
for (var i = 0; i < images.length; i++)
{
var img = images.item(i);
img.style.display = “none”;
}
}
}

在最后这部分中,我们将说明如何以 C++ 实现这个基本逻辑。

HelloWorldBHO.h

首先打开 HelloWorldBHO.h 并将 mshtml.h 包含在内。该头文件定义了使用 DOM 时所需的接口。

include // DOM 接口

接下来,定义专用成员方法以包含上述 JavaScript 的 C++ 实现。

private:

void RemoveImages(IHTMLDocument2 *pDocument);

HelloWorldBHO.cpp

现在,OnDocumentComplete 事件处理程序要完成两个新任务。首先,它将缓存处理后的 WebBrowser 指针与激发事件的对象进行比较;如果两者相等,则该事件用于顶层窗口,并且文档也完全加载。其次,它检索一个指向 document 对象的指针并将其传递给 RemoveImages。

以下是引用片段:
void STDMETHODCALLTYPE CHelloWorldBHO::OnDocumentComplete(IDispatch *pDisp, VARIANT *pvarURL)
{
HRESULT hr = S_OK;
// 查询 IWebBrowser2 接口。
CComQIPtr spTempWebBrowser = pDisp;
// 此事件是否与顶级浏览器相关联?
if (spTempWebBrowser && m_spWebBrowser &&
m_spWebBrowser.IsEqualObject(spTempWebBrowser))
{
// 从浏览器中获取当前文档对象……
CComPtr spDispDoc;
hr = m_spWebBrowser->get_Document(&spDispDoc);
if (SUCCEEDED(hr))
{
// ……并查询 HTML 文档。
CComQIPtr spHTMLDoc = spDispDoc;
if (spHTMLDoc != NULL)
{
// 最后,删除这些图像。
RemoveImages(spHTMLDoc);
}
}
}
}

pDisp 中的 IDispatch 指针包含了已在其中加载文档的窗口或框架的 IWebBrowser2 接口。我们将该值存储在 CComQIPtr 类变量中,该变量将自动执行一个 QueryInterface。接下来,为确定该页面是否已完全加载,我们将该接口指针与顶层浏览器在 SetSite 中进行缓存处理的接口指针进行比较。本测试的结果是,我们仅从顶层浏览器框架的文档中删除了图像;未加载到顶层框架中的文档没有通过本测试。(有关详细信息,请参阅如何确定页面何时在 WebBrowser 控件中完成加载和如何获取 HTML 框架的 WebBrowser 对象模型。)

检索 HTML document 对象需要两个步骤。即使浏览器已经承载了另一种类型的文档对象(例如 Microsoft Word 文档),get_Document 也要为活动文档检索一个指针,因此,必须查询该活动文档是否有 IHTMLDocument2 接口,以确定它是否确实是 HTML 页面。通过 IHTMLDocument2 接口可以访问 DHTML DOM 的内容。

确认某 HTML 文档已加载后,将该值传递给 RemoveImages。请注意,该参数作为指针(而不是作为 CComPtr)传递给 IHTMLDocument2。

以下是引用片段:
void CHelloWorldBHO::RemoveImages(IHTMLDocument2* pDocument)
{
CComPtr spImages;
// 从 DOM 中获取图像集。
HRESULT hr = pDocument->get_images(&spImages);
if (hr == S_OK && spImages != NULL)
{
// 获取集合中的图像数。
long cImages = 0;
hr = spImages->get_length(&cImages);
if (hr == S_OK && cImages > 0)
{
for (int i = 0; i < cImages; i++)
{
CComVariant svarItemIndex(i);
CComVariant svarEmpty;
CComPtr spdispImage;
// 按索引从集合中获取图像。
hr = spImages->item(svarItemIndex, svarEmpty, &spdispImage);
if (hr == S_OK && spdispImage != NULL)
{
// 首先,查询通用 HTML 元素接口……
CComQIPtr spElement = spdispImage;
if (spElement)
{
// ……然后请求样式接口。
CComPtr spStyle;
hr = spElement->get_style(&spStyle);
// 设置 display=“none” 以隐藏图像。
if (hr == S_OK && spStyle != NULL)
{
static const CComBSTR sbstrNone(L"none");
spStyle->put_display(sbstrNone);
}
}
}
}
}
}
}

使用 C++ 与 DOM 交互要比使用 JavaScript 更繁琐,但代码流在本质上相同。

上述代码将循环访问图像集合中的每个项。在脚本中,很明显就可以看出是按序数还是按名称访问集合元素;但在 C++ 中,则必须通过传递一个空变量来手动区分这些参数。我们要再次依靠 ATL 帮助程序类(这次是 CComVariant)来将我们必须编写的代码量最小化。

最后的注意事项

为便于编写脚本,DOM 中的所有对象都使用 IDispatch 来提供从多个接口派生的属性和方法。但在 C++ 中,则必须要显式查询支持要使用的属性或方法的接口。例如,图像对象同时支持 IHTMLElement 接口和 IHTMLImgElement 接口。因此,要检索图像的 style 对象,首先必须查询 IHTMLElement 接口,该接口可提供 get_style 方法。

另请注意,COM 规则不能保证发生故障时指针的有效性;因此在每次 COM 调用后都需要检查 HRESULT。此外,对于许多 DOM 方法来说,返回 NULL 值并不是错误;因此需要对返回值和指针值都进行仔细检查。为使该检查更安全,应始终预先将指针初始化为 NULL。采用防御性的详细容错编码样式将有助于防止以后发生无法预测的程序错误。

总结

虽然有各种类型的 BHO 用于多种用途,但所有 BHO 都有一个共同特点:与浏览器连接。由于 BHO 可以与 Internet Explorer 紧密集成,因此受到需要扩展浏览器功能的大量开发人员的重视。本文说明了如何创建一个简单 BHO 以用于在加载文档中修改 IMG 元素的样式属性。我们鼓励您根据自己需要将本文中的入门级示例继续延伸。可通过访问以下链接进一步探究这些可能性。

Comments