http://www.codeproject.com/Articles/37044/Writing-a-BHO-in-Plain-C
介绍
Browser Helper Objects (也被称为 BHOs) 是com组件,扮演着ie插件的角色。BHOs可以在某种程度上定制IE,如:用户交互的修改,网页过滤的及下载管理。
这篇文章,我们将学习如何编写并安装一个简单的BHO,用原始C++而不是其他的框架,如ATL或MFC。
背景
COM-(组件对象模型)是一个语言中立的技术,广泛的应用于windows,更利于软件模块化和软件重用。
大部分COM的代码用用ATL或MFC框架来完成相应的工作(本文介绍原始C++编写)。然而,学习创建COM对象ATL和MFC常成为另外一个障碍,尤其对于像BHOs这样简单的COM来说。
这篇文章将介绍编写你自己的BHO需要知道的关于COM和BHOs相关知识。只用原始C++和windows API 来完成BHOs的编写,不需要复杂的框架如ATL或MFC。
开始
理解这篇文章的最佳途径是下载并阅读上面链接提供的源代码。源码已经充分注释且易于理解。
理解COM代码
COM术语
首先,让我们了解COM技术的梗概:
接口--一系列对其他对象可见的方法。它等同于C++纯虚类里的共有函数。
组件对象类--继承于一个或更多的接口,并重写所有的方法。它等同于一个从一个或多个纯虚类继承的C++实现类。
对象--一个组件对象类的实例。
GUID--全局唯一ID--GUID是一个128位的唯一数字。它可以通过guidgen.exe工具生成。
IID--接口ID--标识一个接口的GUID。
CLSID--类ID--一个标识COM组件的GUID 。你可以找到关于BHO‘s类ID那个例子,在common.h文件中叫做CLSID_IEPlugin_Str
.每一个COM组件有一个不同的标识,如果你打算在例子的基础上重写你自己的BHO,你应该生成一个新的ID。
IUnknown 接口
为了能创建组件对象,我们必须编写用来实现接口的组件对象类。所有COM对象必须实现一个叫做IUnknown接口。这个接口有三个最基本的函数,这个函数允许其他对象管理组建实现类和用来访问其他接口的对象。这三个函数分别为QueryInterface
, AddRef
, 和Release
. 既然所有可变的组建实现类我们都要从IUnknown继承实现。也就是说要创建这个组建实现类CUnkinown并且使所有我们其他的具体实现类继承它,这样我们就不需要为每个具体实现类单独实现IUnknown接口了。
COM DLL 导出
每个COM DLL 导出四个函数被COM系统用来从DLL创建并管理COM对象还有安装和卸载DLL。这些函数是:
DllGetClassObject
DllCanUnloadNow
DllRegisterServer
DllUnregisterServer
备注:你可以找到这些函数在main.cpp文件中,同时它们也在dll.def文件中声明。
我们的DLL必须有一个IClassFactory
接口的具体实现类。我们把这个具体实现类称作CClassFactory。
DllGetClassObject
函数创建CClassFactory对象并且返回指向它们的接口指针。IClassFactory接口更多的细节将被简单的介绍。
DllCanUnloadNow函数被COM调用用来决定我们的DLL可以被从一个进程中卸载。我们所要做的就是检测我们的DLL当前是否在管理其他对象。如果不是返回S_OK,否则返回S_FALE。我可以在我们的具体实现类的构造函数中自增DLL的全局引用计数器(DLLRefCount)在析构函数中自减全局引用计数器(DLLRefCount)。如果引用计数器为不为0,就意味着我们的具体实现类的实例仍然存在,此刻不能卸载dll。
DllRegisterServer 被一个程序调用,程序需要dll自我安装。我们必须在系统里注册我们自己的COM组件同时也作为BHO。我们通过创建一下的注册入口做到这些:
HKEY_CLASSES_ROOT\CLSID\<CLSID_IEPlugin_Str>
—这个建的默认值应该被设成COM组件的可读描述。本例中,它是"CodeProject Example BHO"。
HKEY_CLASSES_ROOT\CLSID\<CLSID_IEPlugin_Str>\InProcServer32
—这个键的存在标识这这个COM组件可以被作为DLL加载到需要使用它的进程里。我们需要为这个键设两个值,如下:
(default)
— REG_SZ
or REG_EXPAND_SZ
值指向包含COM组件的DLL路径。
ThreadingModel
—这指定COM组件的线程模型。它是一个更超前的概念,我们不需要担心它。我们仅需要把它设为"Apartment"。
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\ Explorer\Browser Helper Objects\<CLSID_IEPlugin_Str>
—这个键的存在以BHO的形式注册我们的COM组件。我们在名为NoExplorer键下创建一个值,把键设为REG_DWORD类型用1填充。通常BHOs也被通过explorer.exe加载,而这个值阻止被explorer.exe进行不必要的加载。
DllUnregisterServer
被调用来做和DllRegisterServer完全相反的功能--反注册我们的COM组件同时以BHO的形式删除它。为了做到这些,我们只需要删除我们在DllRegisterServer创建的注册表键值。
IClassFactory接口
COM用由我们的DLL里的DllGetClassObject
函数创建的IClassFactory对象来获取由DLL支持的接口实现的实例。IClassFactory接口定义了CreateInstance和LockServer的方法。我们调用我们继承自IClassFactory
接口的具体实现类CClassFactory
。
备注:你可以在ClassFactory.h中找到的CClassFactory的定义 ,在ClassFactory.cpp中找到实现。
CreateInstance
确如它所言--用我们支持给定接口ID的DLL创建一个具体实现类的实例。因为我们是BHO,我们只需要创建一个具体实现类的实例,这个实例支持IObjectWithSite
接口。
LockServer
被调用来锁或者解锁内存里的DLL。根据它是否将要解锁,我们LockServer
的实现只是简单的DLL全局引用计数DllRefCount变量的自增或自减。
理解BHO代码
IE如何加载BHO
当BHO将被IE加载的时候,它调用一个叫做CoCreateInstance的COM函数,传给它我们BHO的类ID和叫做IObjectWithSite的接口ID。IObjectWithSite在下文将被更多的介绍。COM相应地把我们的dll加载到IE进程里通过查找注册表里的类ID,然后调用我们暴露的接口DllGetClassObject函数去获得我们的CClassFactory
具体实现类实例。一旦COM有一个指向我们CClassFactory
对象的指针,COM调用它的CreateInstance方法,传给它ie提供的接口id。我们的CreateInstance实现创建一个IObjectWithSite实现的实例,这个实例叫做CObjectWithSite
,并且通过它获得了所请求的接口id的接口指针,然后把接口指针返回给COM,COM又把接口指针传给IE。IE然后用IObjectWithSite接口指针和我们的BHO交互。
IObjectWithSite接口
BHOs被要求实现IObjectWithSite接口,是IE用来和BHO交互的。这个接口有两个方法 SetSite
和 GetSite
.我们DLL里CObjectWithSite具体实现类实现了IObjectWithSite接口。
备注:你可以在ObjectWithSite.h文件里查找CObjectWithSite的声明。在ObjectWithSite.cpp中查找实现。
SetSite
被IE调用给我们一个网站对象的指针。网站对象就是一个COM对象,这个对象由IE创建并且可以被我们的BHO用来和IE通信。我们的SetSite的实现从网站对象那里获得一个指向IConnectionPointContainer接口的接口指针。我们然后用在IConnectionPointContainer接口里的一个方法FindConnectionPoint获取一个IConnectionPoint接口指针,这个指针指向一个支持DWebBrowserEvents2的Dispatch接口的对象。dispatch接口是一种特定的接口,这种接口派生自IDispatch并且通过它的Invoke方法接受事件通知。我们DWebBrowserEvents2的实现叫做CEventSink,下一节有更多的细节介绍。我们用IConnectionPoint接口的Advise方法告诉IE传事件到我们的CEventSink对象。
我们SetSite的实现也从网站对象那里获得了指向IWebBrowser2接口的接口指针。那个IWebBrowser2接口被IE的网站对象实现并且有让我们和IE交互的方法。
备注:因为我们这个BHO的例子只是从IE接收事件通知,不是真正的控制IE,所以我们不需要用IWebBrowser2里的任何方法。然而,我引进了获取IWebBrowser2接口的代码,这样你就可以在你需要的时候在你自己的BHO中用它。你可以在这里找到IWebBrowser2的相关文档。
GetSite被IE用来知道我们已经设置为网站对象的那个对象。IE传给我们一个接口ID给一个接口,这个接口需要我们当前设置的网站对象,我们只是简单的从我们的网站对象那里获得那个接口并把它返回这IE。
CEventSink Coclass
CEventSink具体实现类是我们DWebBrowserEvents2的IDispatch接口的实现。DWebBrowserEvents2派生自IDispatch但没有实现它自己的任何方法。相反,DWebBrowserEvents2存在只是为了它的DIID (dispatch IID)能够存在。这个DIID可以 识别DWebBrowserEvents2具体实现类接收自它自己的IDispatch的Invoke的事件。
备注:你可以在CEventSink.h中找到CEventSink的定义,在CEventSink.cpp中找到实现。
当IE需要通知我们一个事件,它会调用CEventSink的Invoke方法,用dispIdMember参数传事件ID并用pDispParams
参数传递其它事件信息。IDispatch除Invoke之外还有其他三个方法(GetTypeInfoCount
, GetTypeInfo
, 和GetIDsOfNames
)。但我们不需要为它们实现任何功能,因为我们只是接收事件。
另一个你可能意识到关于CEventSink的区别可能是:我们别的具体实现类都是派生自CUnknown。这是因为我们只需要一个CEventSink的DLL型全局静态实例EventSink。因为我们不需要实现任何引用计数功能,因此我们不需要CUnknown函数的引用计数和内存管理的功能。
Handling Events
IE调用CEventSink::Invoke
通知我们事件。dispIdMember参数包含一个将要被触发的事件的ID,pDispParams->rgvarg[]数组包含VARIANT
s类型的事件自身的参数。你可以在DWebBrowserEvents2文档里找到一个事件携带的任何参数,这个文档可以在这里被找到。参数被已列在事件文档里的相对顺序传到pDispParams->rgvarg[]数组。为了把这些参数从一个VARIANT转换为一个更为有用的类型,我们首先为每个将要使用的参数声明一种VARIANT类型并用VariantInit
API 函数初始化它们。.然后,我们可以用VariantChangeType
API 函数转换pDispParams->rgvarg[]数组里的VARIANT为一种更有用的VARIANT类型。一旦我们为所有我们需要的参数完成了这些转换,我们就能传参数值到我们自己的Event_*方法里,通过用我们的转换的VARIANT变量的数字。在事件处理方法返回是,我们通过对每个调用 VariantClear
方法释放我们使用的VARIANT的所有资源。如何做到这些的具体实例将在下面给出。
BeforeNavigate2 事件
我们的BHO例子只是处理一个事件,BeforeNavigate2
事件。BeforeNavigate2
文档可以在这里找到。这个事件记载IE导航到一个新的地址前被触发。
看下文档,我们发现BeforeNavigate2
给我们几个参数。我们将不关心pDisp参数,这个参数只是一个指向网站对象的IDispatch接口指针。
事件参数被作为变量装在pDispParams->rgvarg[]中。在我们可以用这些参数之前,尽管我们需要把那些变量转换成更容易使用的变量类型。我们首先需要用VariantInit初始化每个变量,当我们完成时我们通过VariantClear函数释放所有使用的资源。
1 2 3 4 5 6 7 8 | VARIANT v[5]; // Used to hold converted event parameters before // passing them onto the event handling method ... for (n=0;n<5;n++) VariantInit(&v[n]); // initialize the variant array ... // use the variant array here for (n=0;n<5;n++) VariantClear(&v[n]); // free the variant array |
那个url参数包含将要被浏览的URL。他是第五个参数(从0计数),因此我们用访问pDispParams->rgvarg[5]它。我们把这个变量转换成VT_BSTR类型,因为它可能不是那个格式,然后存放转换后的变量如v[0].我们通过v[0].bstrVal用 VT_BSTR
的方式可以访问URL。BSTR字符串在COM里传递字符串数据很常用。它是由标识字符串长度的四个字节的前缀和紧接其后的双字节以0结尾的Unicode字符串类型的字符串数据组成。BSTR变量总是指向字符串数据,不指向它之前的4-字节的前缀。很方便我们用C-风格的字符串使用它。一个双字节的字符串类型LPOLESTR在windows API的头文件里有声明,忽略程序
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· .NET开发智能桌面机器人:用.NET IoT库编写驱动控制两个屏幕
· 用纯.NET开发并制作一个智能桌面机器人:从.NET IoT入门开始
· 一个超经典 WinForm,WPF 卡死问题的终极反思
· ASP.NET Core - 日志记录系统(二)
· .NET 依赖注入中的 Captive Dependency
· 几个自学项目的通病,别因为它们浪费了时间!
· 在外漂泊的这几年总结和感悟,展望未来
· 如何在 ASP.NET Core 中实现速率限制?
· 博客园 & 1Panel 联合终身会员上线
· Kubernetes 知识梳理及集群搭建