c#

位置:IT落伍者 >> c# >> 浏览文章

用VisualStudio2005生成浏览器帮助对象


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

简介

本文凭借 Microsoft Visual Studio 活动模板库(ATL) 来开发使用 C++ 的 BHO我们之所以决定使用 ATL是因为它方便地实现了我们可以按需进行扩展的基本样板还有其他方法可供用于创建 BHO(例如使用Microsoft 基础类(MFC) 或 Win 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 和早期版本中这意味着为每个新的顶层窗口都创建(和销毁)一个新 BHO另一方面Internet Explorer 会为每个选项卡都创建和销毁一个新 BHOBHO 不是由承载 WebBrowser 控件的其他应用程序加载也不是由 HTML 对话框之类的窗口加载

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

建立项目

通过 Microsoft Visual Studio 创建 BHO 项目

文件菜单上单击新建项目

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

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

ATL 项目向导确保服务器类型为动态链接库 (DLL)然后单击完成

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

解决方案资源管理器面板上右键单击该项目然后从添加子菜单中选择

选中ATL 简单对象然后单击添加

随即出现ATL 简单对象向导

ATL 简单对象向导名称键入HelloWorldBHO以作为短名称

余下的名称将自动填充

ATL 简单对象向导选项选中线程模型下的Apartment聚合下的接口下的双重以及支持下的IobjectWithSite

单击完成

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

&#;HelloWorldBHOh – 此头文件包含 BHO 的类定义

&#;HelloWorldBHOcpp – 此源文件是项目的主文件并且包含 COM 对象

&#;HelloWorldcpp – 此源文件用于实现通过 DLL 提供 COM 对象的导出

&#;HelloWorldidl – 此源文件可用于定义自定义 COM 接口对于本文我们将不更改此文件

&#;HelloWorldrgs – 此资源文件包含注册和取消注册 DLL 时编写和删除的注册表项

实现基本要素

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

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

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

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

HelloWorldBHOh

在 Visual Studio 的解决方案资源管理器中双击打开 HelloWorldBHOh

首先包含 shlguidh此文件定义了 IWebBrowser 的接口标识符和稍后在项目中使用的事件

#include // IID_IWebBrowserDIID_DWebBrowserEvents

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

STDMETHOD(SetSite)(IUnknown *pUnkSite);

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

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

          以下是引用片段

private:

CComPtrm_spWebBrowser;

HelloWorldBHOcpp

现在切换到HelloWorldBHOcpp并为SetSite插入以下代码

STDMETHODIMPCHelloWorldBHO::SetSite(IUnknown*pUnkSite)

{

if(pUnkSite!=NULL)

{

//缓存指向IWebBrowser的指针

pUnkSite>QueryInterface(IID_IWebBrowser(void**)&m_spWebBrowser);

}

else

{

//在此释放缓存的指针和其他资源

m_spWebBrowserRelease();

}

//返回基类实现

returnIObjectWithSiteImpl::SetSite(pUnkSite);

}      

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

HelloWorldcpp

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

在 HelloWorldcpp 中如下编写 DllMain 函数的代码


          以下是引用片段

externCBOOLWINAPIDllMain(HINSTANCEhInstanceDWORDdwReasonLPVOIDlpReserved)

{

if(dwReason==DLL_PROCESS_ATTACH)

{

DisableThreadLibraryCalls(hInstance);

}

return_AtlModuleDllMain(dwReasonlpReserved);

}      

注册 BHO

剩下要做的只是将 BHO 的 CLSID 添加到注册表中此条目会将 DLL 标记为浏览器帮助程序对象并使 Internet Explorer 在启动时加载 BHOVisual Studio 可在生成项目时注册 CLSID

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

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

          以下是引用片段

importlib(stdoletlb);

[

uuid(DFEECDCBCDADDD)

helpstring(HelloWorldBHOClass)

]      

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

创建自行注册的 BHO

从 Visual Studio 中的解决方案资源管理器打开 HelloWorldrgs

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

          以下是引用片段

HKLM{

NoRemoveSOFTWARE{

NoRemoveMicrosoft{

NoRemoveWindows{

NoRemoveCurrentVersion{

NoRemoveExplorer{

NoRemove浏览器帮助程序对象{

ForceRemove{DFEECDCBCDADDD}=sHelloWorldBHO{

valNoExplorer=d

}

}

}

}

}

}

}

}      

将上述 ForceRemove 后面的 GUID 替换为 BHO 的 CLSID(可在 HelloWorldidl 中找到)

切勿替换大括号

保存文件然后重新生成解决方案(按 F)

Visual Studio 将自动注册该对象

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

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

现在您就可以从 Visual Studio 中的生成菜单生成解决方案

进行试用

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

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

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

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

对事件做出响应

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

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

HelloWorldBHOh

首先通过包含 exdispidh(为浏览器事件定义调度 ID)处理 HelloWorldBHOh

#include // DISPID_DOCUMENTCOMPLETE 等

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

          以下是引用片段

classATL_NO_VTABLECHelloWorldBHO:

publicCComObjectRootEx

publicCComCoClass

publicIObjectWithSiteImpl

publicIDispatchImpl

publicIDispEventImpl<CHelloWorldBHO&DIID_DWebBrowserEvents&LIBID_SHDocVw>      

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


          以下是引用片段

BEGIN_SINK_MAP(CHelloWorldBHO)

SINK_ENTRY_EX(DIID_DWebBrowserEventsDISPID_DOCUMENTCOMPLETEOnDocumentComplete)

END_SINK_MAP()

//DWebBrowserEvents

voidSTDMETHODCALLTYPEOnDocumentComplete(IDispatch*pDispVARIANT*pvarURL);      

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

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

          以下是引用片段

private:

BOOLm_fAdvised;

HelloWorldBHOcpp      

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

以下是 SetSite 的新实现

          以下是引用片段

STDMETHODIMPCHelloWorldBHO::SetSite(IUnknown*pUnkSite)

{

if(pUnkSite!=NULL)

{

//缓存指向IWebBrowser的指针

HRESULThr=pUnkSite>QueryInterface(IID_IWebBrowser(void**)&m_spWebBrowser);

if(SUCCEEDED(hr))

{

//注册以从DWebBrowserEvents中汇集事件

hr=DispEventAdvise(m_spWebBrowser);

if(SUCCEEDED(hr))

{

m_fAdvised=TRUE;

}

}

}

else

{

//取消注册事件汇

if(m_fAdvised)

{

DispEventUnadvise(m_spWebBrowser);

m_fAdvised=FALSE;

}

//在此释放缓存的指针和其他资源

m_spWebBrowserRelease();

}

//调用基类实现

returnIObjectWithSiteImpl::SetSite(pUnkSite);

}      

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

          以下是引用片段

voidSTDMETHODCALLTYPECHelloWorldBHO::OnDocumentComplete(IDispatch*pDispVARIANT*pvarURL)

{

//从站点检索顶级窗口

HWNDhwnd;

HRESULThr=m_spWebBrowser>get_HWND((LONG_PTR*)&hwnd);

if(SUCCEEDED(hr))

{

//加载页面时输出消息框

MessageBox(hwndL大家好!LBHOMB_OK);

}

}      

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

再一次试用

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


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

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

操作 DOM

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

          以下是引用片段

functionRemoveImages(doc)

{

varimages=docimages;

if(images!=null)

{

for(vari=;i<imageslength;i++)

{

varimg=em(i);

imgstyledisplay=none;

}

}

}      

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

HelloWorldBHOh

首先打开 HelloWorldBHOh 并将 mshtmlh 包含在内该头文件定义了使用 DOM 时所需的接口

#include // DOM 接口

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

private:

void RemoveImages(IHTMLDocument *pDocument);

HelloWorldBHOcpp

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


          以下是引用片段

voidSTDMETHODCALLTYPECHelloWorldBHO::OnDocumentComplete(IDispatch*pDispVARIANT*pvarURL)

{

HRESULThr=S_OK;

//查询IWebBrowser接口

CComQIPtrspTempWebBrowser=pDisp;

//此事件是否与顶级浏览器相关联?

if(spTempWebBrowser&&m_spWebBrowser&&

m_spWebBrowserIsEqualObject(spTempWebBrowser))

{

//从浏览器中获取当前文档对象……

CComPtrspDispDoc;

hr=m_spWebBrowser>get_Document(&spDispDoc);

if(SUCCEEDED(hr))

{

//……并查询HTML文档

CComQIPtrspHTMLDoc=spDispDoc;

if(spHTMLDoc!=NULL)

{

//最后删除这些图像

RemoveImages(spHTMLDoc);

}

}

}

}      

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

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

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

          以下是引用片段

voidCHelloWorldBHO::RemoveImages(IHTMLDocument*pDocument)

{

CComPtrspImages;

//从DOM中获取图像集

HRESULThr=pDocument>get_images(&spImages);

if(hr==S_OK&&spImages!=NULL)

{

//获取集合中的图像数

longcImages=;

hr=spImages>get_length(&cImages);

if(hr==S_OK&&cImages>)

{

for(inti=;i<cImages;i++)

{

CComVariantsvarItemIndex(i);

CComVariantsvarEmpty;

CComPtrspdispImage;

//按索引从集合中获取图像

hr=spImages>item(svarItemIndexsvarEmpty&spdispImage);

if(hr==S_OK&&spdispImage!=NULL)

{

//首先查询通用HTML元素接口……

CComQIPtrspElement=spdispImage;

if(spElement)

{

//……然后请求样式接口

CComPtrspStyle;

hr=spElement>get_style(&spStyle);

//设置display=none以隐藏图像

if(hr==S_OK&&spStyle!=NULL)

{

staticconstCComBSTRsbstrNone(Lnone);

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 元素的样式属性我们鼓励您根据自己需要将本文中的入门级示例继续延伸可通过访问以下链接进一步探究这些可能性

               

上一篇:C#编程中的 New 关键词的几种用法

下一篇:C#用Activex实现Web客户端读取RFID功能