原文 http://blog.sina.com.cn/s/blog_4a34240c0100d35o.html
前言:
本文档是为了演示如何使用V2005开发一个简单的BHO组件,BHO组件是实现了IObjectWithSite接口的
COM对象,该COM对象与IE进行绑定。
本文档将逐步演示如何建立BHO组件的过程,该组件实现的功能:首先当装载WEB页的时候
打印出"Hello world"信息,然后把该WEB页的所有图片资源删除。
内容提纲:
简单介绍
概要介绍
建立项目
实现基本应用
对事件进行响应
处理DOM
总结
相关参考资料
简单介绍
上述功能的实现主要使用V2005和ATL技术,我们之所以使用ATL的愿意在于它提供了一个方便扩展的模板文件。其实还有其他方法可以建立BHO,例如MFC,WIN32 API和COM等技术,但是ATL是一个轻量级的库它提供了很多内置的功能为我们处理底层的诸多细节实现,例如将为我们自动注册BHO的CLSID。
ATL的另一个优点是支持智能指针的类(例如CComPtr 和CComBSTR),这些智能指针类将起到管理COM
生命周期的作用。例如,CComPtr通过调用AddRef方法来进行赋值操作,而通过调用Release来对对象进行
销毁操作。智能指针简化了代码,并且提供了检测内存错误的功能,它们提供的安全性和稳定性在方法内非常有用。
文章的第一部分将演示如何实现一个简单的BHO组件,并且确认在IE启动的时候该组件被执行。
文章的第二部分则演示如何把BHO组件与浏览器事件进行关联,文档的最后一部分则是一个把该
组件与 DOM关联来修改WEB页面的功能。
概要介绍
什么是BHO?BHO是一个组件,是个轻量级的DLL扩展,提供了向IE增加新功能的途径。尽管BHO使用的
比较少,并且不是本文档的重点内容,但是BHOs还可以向WINDOWS EXplorer添加新的功能扩展。
BHOs不会提供任何界面,它们常常运行在后台,捕获IE事件与用户的输入。例如,BHOs可以阻塞弹出
菜单,可以自动填充表单,可以对光标进行捕获等。认为BHOs仅仅是为了扩展工具条的功能是个普遍的错误认识,然而,BHOs与工具条的联合使用可以提供富客户端的用户体验。
提示:BHOs是一个很方便的工具对于用户和开发者来说;但是因为BHOs被赋予的权限超过了浏览器
本身的权限要求,而且它们的运行常常不容易被探测到,所以我们要确保BHOs的来源是可信任、
的。BHOs的生命周期将与其关联的IE浏览器实例一样长,在IE6之前的版本,这意味着BHO的生成
是在顶层窗口之上的,而在IE7之后的版本则是一个浏览标签TAB页。BHO不能被其他的应用及
HTML的对话框所装载。
BHO最关键的是实现了IObjectWithSite接口,这个接口为我们提供了诸如SetSite等方法,这些
方法提供了与IE交互的途径。我们将生成一个简单的BHO并且在注册表里为其注册个CLSID。
让我们开始吧。
建立一个新项目
1, File->New Project...
对话框里将列表显示出可以用VS生成的项目类型。
2,在{visual c++}节点下选择"ATL",生成一个ATL类型的项目,我们为这个项目起名
为"Hello World"
3,在ATL Project Wizard的项目生成向导中需要确定服务类型为动态连接库
(Dynamic-link library)
在本步VS为我们生成了项目模板,我们需要添加COM对象来实现BHO组件。
1,在Solution Exploerer控制面板里,右键单击,选择class...从Add菜单里。
2,选择"ATL Simple Object"并且点{Add}按纽,则ATL Simple Object Wizard向
导出现。
3,在名字文本框里,写入"HelloWorldBHO.
4,在<Options>的面板上,需要进行确认的选择项如下:
选择"Apartment "对Threading Model.
选择"No"对Aggregation
选择"Dual"对Interface
选择"IObjectWithSite"对Support.
5,点击 {完成按纽}.
出现的文件列表:
HelloWorld.idl--这是个定义COM接口的文件,我们不在这个项目里进行任何修改。
HelloWorld.rgs--这是个包含注册键码的文件,当该DLL被注册或是被解除注册
的时候进行定义。
基础功能
ATL Project Wizard提供了SetSite的默认实现,尽管该方法可以被不限定次数的调
用,但是IE调用该方法仅两次,一次是建立连接的时候,一次是当退出IE的时候。
明确的讲,SetSite主要
完成如下功能:
1,保存对站点的引用。在IE初始化的时候,IE传送一个IUnknown指针到顶层的
top-level的
WebBrowser Control,而BHO则保存对其的引用,该引用保存在一个私有变量里。
2,释放保存的站点指针。当IE传送NULL的时候,BHO必须释放对所有接口的引用,并
且断开与该浏览器对象的连接。
作为SetSite的处理的一部分,BHO仍然可以进行其他的初始操作与释放操作,例如
你希望建立一个与特定浏览器对象的连接,然后检测该浏览器的事件。
HelloWorldBHO.h
首先添加对shlguid.h头文件的包含命令,该文件定义了IWebBrowser2的接口,和
事件。
然后在CHelloWorldBho类的公共变量列表中添加
STDMETHOD(SetSite)(IUnknown *pUnkSite);
STDMETHOD宏是ATL的应用范例,它标志该方法是虚方法,并且确保它被正确调用
最后在私有变量部分声明一个保存浏览器站点的引用变量。
private:
CComPtr<IWebBrowser2>m_spWebBrowser;
HelloWorldBHO.cpp
STDMETHODIMP CHelloWorldBHO::SetSite(IUnknown* pUnkSite)
{
if (pUnkSite != NULL)
{
// Cache the pointer to IWebBrowser2.
pUnkSite->QueryInterface(IID_IWebBrowser2,
(void**)&m_spWebBrowser);
}
else
{
// Release cached pointers and other resources
//here.
m_spWebBrowser.Release();
}
// Return the base class implementation
return IObjectWithSiteImpl<CHelloWorldBHO>::SetSite(pUnkSite);
}
HelloWorld.cpp
当DLL被装载的时候,系统调用DllMain方法,当DLL_PROCESS_ATTACH通知发送的时候。
因为BHO不需要在线程的级别上进行跟踪,所以我们可以调用
DisableThreadLibraryCalls.可以阻止新的线程通知消息。
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到注册表。该CLSID标志该DLL是BHO
并且激活BHO在IE浏览器启动的时候。VS可以注册该CLSID当编译该项目的时候。
BHO的CLSID在HelloWorld.idl文件中,这段代码类似这样:
importlib("stdole2.tlb");
[
uuid(D2F7E1E3-C9DC-4349-B72C-D5A708D6DD77),
helpstring("HelloWorldBHO Class")
]
注意该文件里包括三个GUID,我们仅仅需要CLASS的CLSID,其他的不做注意。
生成一个自注册的BHO
1,打开HelloWorld.rgs.
2, 在文件的结尾处,添加如下代码:
HKLM {
NoRemove SOFTWARE {
NoRemove Microsoft {
NoRemove Windows {
NoRemove CurrentVersion {
NoRemove Explorer {
NoRemove 'Browser Helper Objects' {
ForceRemove '{D2F7E1E3-C9DC-4349-B72C-D5A708D6DD77}' = s 'HelloWorldBHO' {
val 'NoExplorer' = d '1'
}
}
}
}
}
}
}
}
启动测试过程
为了进行快速测试,可以在SetSite方法设置个断点,并且启动调试,通过按F5。
当出现Executable for Debug Session对话框的时候,选择"Default Web Browser"
然后点{OK}按纽。
对浏览器事件进行处理
在本部分,我们将演示如何使用ATL来实现一个事件处理器,在这里我们以DocumentComplete
为例。
该事件包括两个参数:pDisp(指向IDispatch对象的指针)和pUrl.这两个参数都会传送到
IDispatch::Invoke处理方法,幸运的是ATL提供了一个默认实现,来帮助我们来简化该事件
处理器的底层开发。
HelloWorldBHO.h
添加#include <exdispid.h>该文件定义了浏览器事件的ID。
添加对IDispEventImpl基类的继承,该类提供了一个简单,安全的方法来调用事件的处理
方法。
我们确定需要处理在DWebBrowserEvent2接口中定义的事件类型。
class ATL_NO_VTABLE CHelloWorldBHO :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CHelloWorldBHO, &CLSID_HelloWorldBHO>,
public IObjectWithSiteImpl<CHelloWorldBHO>,
public IDispatchImpl<IHelloWorldBHO, &IID_IHelloWorldBHO, &LIBID_HelloWorldLib, 1, 0>,
public IDispEventImpl<1, CHelloWorldBHO, &DIID_DWebBrowserEvents2, &LIBID_SHDocVw, 1, 1>
接下来我们添加一个ATL宏来路由该事件到一个新的事件处理器OnDocumentComplete的事件处理
器方法,该方法具备一样的参数和顺序,参照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);
最后添加一个私有变量来跟踪该对象是否与浏览器建立了连接。
private:
BOOL m_fAdvised;
HelloWorldBHO.cpp
建立事件处理器与浏览器的连接,是通过事件影射来完成的。我们通过在SetSite里对
DispEventAdvise的调用。
而类似的对DispEventUnadvise的调用即可断开连接。
这里是新实现的SetSite方法:
STDMETHODIMP CHelloWorldBHO::SetSite(IUnknown* pUnkSite)
{
if (pUnkSite != NULL)
{
// Cache the pointer to IWebBrowser2.
HRESULT hr = pUnkSite->QueryInterface(IID_IWebBrowser2, (void **)&m_spWebBrowser);
if (SUCCEEDED(hr))
{
// Register to sink events from DWebBrowserEvents2.
hr = DispEventAdvise(m_spWebBrowser);
if (SUCCEEDED(hr))
{
m_fAdvised = TRUE;
}
}
}
else
{
// Unregister event sink.
if (m_fAdvised)
{
DispEventUnadvise(m_spWebBrowser);
m_fAdvised = FALSE;
}
// Release cached pointers and other resources here.
m_spWebBrowser.Release();
}
// Call base class implementation.
return IObjectWithSiteImpl<CHelloWorldBHO>::SetSite(pUnkSite);
}
最后添加一个简单的OnDocumentComplete事件处理方法
void STDMETHODCALLTYPE CHelloWorldBHO::OnDocumentComplete(IDispatch *pDisp, VARIANT *pvarURL)
{
// Retrieve the top-level window from the site.
HWND hwnd;
HRESULT hr = m_spWebBrowser->get_HWND((LONG_PTR*)&hwnd);
if (SUCCEEDED(hr))
{
// Output a message box when page is loaded.
MessageBox(hwnd, L"Hello World!", L"BHO", MB_OK);
}
}
处理DOM树
下面的JS代码演示了基本的对DOM树的处理过程,它隐藏了所有的图片资源。
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文件添加如下语句#include <mshtml.h>
接下来添加一个私有方法来使用C++来实现上面的对DOM处理的简单业务逻辑。
private:
void RemoveImages(IHTMLDocument2 *pDocument);
HelloWorldBHO.cpp
OnDocumentComplete 事件处理器,现在做两件新的事情。第一件事情,它会比较
缓存的WebBrowser指针与事件被触发的指针对象;如果他们相等,则该事件针对顶
层窗口与文档的全部装载进行触发。
第二件事情,它获取document对象的指针,并且把该指针传送到RemoveImages方法。
void STDMETHODCALLTYPE CHelloWorldBHO::OnDocumentComplete(IDispatch *pDisp, VARIANT *pvarURL)
{
HRESULT hr = S_OK;
// Query for the IWebBrowser2 interface.
CComQIPtr<IWebBrowser2> spTempWebBrowser = pDisp;
// Is this event associated with the top-level browser?
if (spTempWebBrowser && m_spWebBrowser &&
m_spWebBrowser.IsEqualObject(spTempWebBrowser))
{
// Get the current document object from browser...
CComPtr<IDispatch> spDispDoc;
hr = m_spWebBrowser->get_Document(&spDispDoc);
if (SUCCEEDED(hr))
{
// ...and query for an HTML document.
CComQIPtr<IHTMLDocument2> spHTMLDoc = spDispDoc;
if (spHTMLDoc != NULL)
{
// Finally, remove the images.
RemoveImages(spHTMLDoc);
}
}
}
}
该IDispatch指针包含着IWebBrowser2接口,该接口指向包含文档被装载的窗口。
我们保存该变量到CComQIPtr类变量中,该类自动执行QueryInterface接口。
接下来判断该页面是否装载完全,我们通过比较我们缓存到SetSits的指针。
作为测试的结果,我们仅仅是移动顶层窗体文档的所有图片元素,没有在顶层窗体
装载的文档不被传送到该测试功能中。
这里需要两个步骤来获取HTML文档的对象,因为get_Document将会获取一个当前
活跃的文档指针,甚至该文档并不是在IE程序里被装载的,我们必须进步查询该文档
采用IHTMLDocument2来确定该文档是否确实是HTML页面文档。IHTMLDocument2
接口提供了访问DHTML DOM树的指针。
在确定了一个HTML文档被装载后,我们把该值传送到RemoveImages方法。注意传入的
该值是一个指向IHTMLDocument接口的指针,而不是CComPtr.
void CHelloWorldBHO::RemoveImages(IHTMLDocument2* pDocument)
{
CComPtr<IHTMLElementCollection> spImages;
// Get the collection of images from the DOM.
HRESULT hr = pDocument->get_images(&spImages;);
if (hr == S_OK && spImages != NULL)
{
// Get the number of images in the collection.
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<IDispatch> spdispImage;
// Get the image out of the collection by index.
hr = spImages->item(svarItemIndex, svarEmpty, &spdispImage);
if (hr == S_OK && spdispImage != NULL)
{
// First, query for the generic HTML element interface...
CComQIPtr<IHTMLElement> spElement = spdispImage;
if (spElement)
{
// ...then ask for the style interface.
CComPtr<IHTMLStyle> spStyle;
hr = spElement->get_style(&spStyle;);
// Set display="none" to hide the image.
if (hr == S_OK && spStyle != NULL)
{
static const CComBSTR sbstrNone(L"none");
spStyle->put_display(sbstrNone);
}
}
}
}
}
}
}
比较来看,使用VC与DOM交互与用JS技术来比较,代码比较复杂,但是逻辑流程是相同的。
该处理代码需要遍历每个images集合中的项目,但是在脚本中,则一切都比较简单,我们可以
通过名字或是索引来访问,我们可以依靠一个ATL帮助类-CComVariant来做小化我们必须
写的代码量。
相对脚本实现的简单,DOM树的所有对象使用IDispatch接口暴露的属性和方法,而且这些方法
与属性都继承自不同的接口。在C++中,我们必须显示的查询某个接口是否支持特定的属性和方法。
例如一个image对象支持IHTMLElement和IHTMLImgElement两个接口,因此如果想获取一个图片
类的style对象,那么你第一步首先查询一个IHTMLElement接口,该接口定义一个get_style方法。
同样应该注意到一个COM规则是不确保在失败的时候仍然保持一个有效的指针;因此你必须检查
HRESULT在每次调用完COM后。而且很多DOM方法返回NULL并不是表示一个错误,所有
我们必须要小心的检查一个方法的返回值,和指针值。比检查指针和返回值更安全的,是总是把指针
在初始的时候设置为NULL,采取错误消息处理的代码风格,可以起到积极应对未预知错误的能力。
总结:
还有其他几种类型的BHOs按照更广泛的目标,然后所有的BHOs都有一个共同的特点:
一个与浏览器对象保持连接的对象。因为BHOs与IE整合来发挥功能,所以BHOs对那些
想扩展IE功能的开发者更有意义。