七、探测谁在调用这个对象
如前所述,一个BHO对象会被Internet Explorer或者Windows资源管理器(前提:外壳版本4.71或者更高)所加载。所以我专门设计了一个BHO来处理HTML网页,因此这个BHO与资源管理器毫无关系。如果一个Dll不想被调用者一起加载,只需在DllMain()中实现了探明谁在调用该对象后返回FALSE即可。参看下面代码:
if (dwReason == DLL_PROCESS_ATTACH)
{
TCHAR pszLoader[MAX_PATH];
//返回调用者模块的名称,第一个参数应为NULL,详见msdn。
GetModuleFileName(NULL, pszLoader, MAX_PATH);
_tcslwr(pszLoader);
if (_tcsstr(pszLoader, _T("explorer.exe")))
return FALSE;
}
一旦知道了当前进程是Windows资源管理器,可立即退出。
注意,再多加一些条件语句是危险的!事实上,另外一些进程试图装入该DLL时将被放弃。如果你做另外一个试验,比方说针对Internet
Explorer的执行文件iexplorer.exe,这时第一个受害者就是regsvr32.exe(该程序用于自动注册对象)。
if (!_tcsstr(pszLoader, _T("iexplore.exe")))
你不能够再次注册该DLL库了。 事实上,当 regsvr32.exe 试图装入DLL以激活函数DllRegisterServer()时,该调用将被放弃。
八、与Web浏览器取得联系
SetSite()方法正是BHO对象被初始化的地方,此外,在这个方法中你可以执行所有的仅仅允许发生一次的任务。当你用Internet
Explorer打开一个URL时,你应该等待一系列的事件以确保要求的文档已完全下载并被初始化。唯有在此时,你才可以通过对象模型暴露的接口(如果存在的话)存取文档内容。这就是说你要取得一系列的指针。第一个就是指向IWebBrowser2(该接口用来生成WebBrowser对象)的指针。第二个指针与事件有关。该模块必须作为一个浏览器的事件侦听器来实现,目的是为接收下载以及与文档相关的事件。下面用ATL灵敏指针加以封装:
CComQIPtr< IWebBrowser2, &IID_IWebBrowser2> m_spWebBrowser2;
CComQIPtr<IConnectionPointContainer,
&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窗口的移动或尺寸调整。这里的钩子用来实现热键功能,用户可以按动热键来显示/隐藏代码窗口。
九、从Internet Explorer浏览器取得事件
当你导向一个新的URL时,浏览器最需要完成的是两种事件:下载文档并为之准备HOST环境。也就是说,它必须初始化某对象并使该对象从外部可以利用。针对不同的文档类型,或者装入一个已注册的Microsoft
ActiveX?
服务器来处理该文档(如Word对于.doc文件的处理)或者初始化一些内部组件来分析文档内容并生成和显示该文档。对于HTML网页就是这样,其内容由于DHTML对象作用而变得可用。当文档全部下载结束,DownloadComplete事件被激活。这并不是说,这样利用对象模型就可以安全地管理文档的内容了。事实上,DocumentComplete
事件仅指明一切已经结束,文档已准备好了 (注意DocumentComplete事件仅在你第一次存取URL时到达,如果你执行了刷新动作,你仅仅收到一个DocumentComplete事件)。
为了截获浏览器发出的事件, BHO需要通过IConnectionPoint 接口连接到浏览器上 并且实现传递接口IDispatch指针以处理各种事件。现在利用前面取得的IConnectionPointContainer指针来调用FindConnectionPoint方法――它返回一个指针指向连接点对象(正是通过这个连接点对象来取得要求的外向接口,此时是DIID_DWebBrowserEvent2)。
下列代码显示了连接点的发生情况:
HRESULT CViewSource::Connect(void)
{
HRESULT hr;
CComPtr<IConnectionPoint> spCP;
//为Web浏览器事件而接收(receive)连接点
hr = m_spCPC->FindConnectionPoint(DIID_DWebBrowserEvent2,
&spCP);
if (FAILED(hr))
return hr;
// 把事件处理器传递到容器。每次事件发生容器都将激活我们实现的IDispatch接口上的相应的函数。
hr = spCP->Advise( reinterpret_cast<IDispatch*>(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的Web浏览器控件并被连接到浏览器控件以接收所有它产生的事件。当网页被全部下载并正确初始化后,我们就可以通过DHTML文档模型存取它。Web浏览器的文档属性返回一个指向文档对象的IDispatch接口的指针:
CComPtr<IDispatch> pDisp;
HRESULT hr = m_spWebBrowser2->get_Document(&pDisp);
get_Document() 方法取得的仅仅是一个接口指针。我们要进一步确定在IDispatch
指针背后存在一个HTML文档对象。用VB实现的话,可以用下面代码:
Dim doc As Object
Set doc = WebBrowser1.Document
If TypeName(doc)="HTMLDocument" Then
'' 获取文档内容并予以显示
Else
'' Disable the display dialog
End If
现在要了解一下get_Document()返回的IDispatch指针 。Internet
Explorer不仅仅是一个HTML浏览器,而且还是一个ActiveX文档容器。
这样一来,难以保证当前浏览对象就是一个HTML文档。不过办法还是有的――你想,如果IDispatch指针真正指向一个HTML文档,查询IHTMLDocument2
接口一定成功。
IHTMLDocument2接口包装了DHTML对象模型用来展现HTML页面的所有功能。下面代码实现这些功能:
CComPtr<IDispatch> pDisp;
HRESULT hr = m_spWebBrowser2->get_Document(&pDisp);
CComQIPtr<IHTMLDocument2, &IID_IHTMLDocument2> spHTML;
spHTML = pDisp;
if (spHTML) {
// 获取文档内容并予以显示
}
else {
// disable the Code Window controls
}
如果IHTMLDocument2接口查询失败,spHTML指针将是NULL。
现在考虑如何获得当前显示窗口的源代码。正如一个HTML页把它所有的内容封装在标签<BODY>中,DHTML对象模型要求你取得一个指向Body对象的指针:
CComPtr<IHTMLElement> m_pBody;
hr = spHTML->get_body(&m_pBody);
奇怪的是,DHTML对象模型不让你取得标签<BODY>之前的原始内容,如<HEAD>。其内容被处理并存于一些属性中,但你还是不能从HTML原始文件中提取这部分的RAW文本。这过,仅从BODY部分取得的内容足够了。为了取得包含在<BODY>…</BODY>间的HTML代码部分,可以把outerHTML属性内容读取到一个BSTR变量中:
BSTR bstrHTMLText;
hr = m_pBody->get_outerHTML(&bstrHTMLText);
在此基础上,在代码窗口中显示源码就是一种简单的事情了:生成一个窗口,进行字符的UNICODE至ANSI转化和设置编辑框控件的问题。下面代码实现这些功能:
HRESULT CViewSource::GetDocumentContent()
{
USES_CONVERSION;
// 获取 WebBrowser的文档对象
CComPtr<IDispatch> pDisp;
HRESULT hr = m_spWebBrowser2->get_Document(&pDisp);
if (FAILED(hr))
return hr;
//
确保我们取得的是一个IHTMLDocument2接口指针
//让我们查询一下 IHTMLDocument2 接口 (使用灵敏指针)
CComQIPtr<IHTMLDocument2,
&IID_IHTMLDocument2> 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刷新后全部复原。你还要处理一下DownloadComplete事件以刷新代码窗口
(注意, DownloadComplete 事件发生在 DocumentComplete事件之前)。你应该忽略网页的首次DownloadComplete事件,而是在执行刷新动作时才关注这一事件。布尔成员变量m_bDocumentCompleted正是用来区别这两种情形的。
十一、管理代码窗口
用来显示当前HTML页原始码的代码窗口涉及另外一个ATL 基本编程问题-对话框窗口,它位于ATL对象向导的"Miscellaneous"选项卡下。
我调整了代码窗口的大小来响应WM_INITDIALOG消息,使它占居桌面空间的下部区域,正好是在任务栏的上面。在浏览器启动时你可以选择显示或不显示这个窗口。缺省情况下是显示的,但这可以通过清除"Show
window at startup"复选框项来实现。当然喜欢的话,你可以随时关闭。按键F12即可重新显示代码窗口。F12是通过在SetSite()中安装的键盘钩子实现的。启动环境存于WINDOWS注册表中,我选择外壳库文件shlwapi.dll中函数SHGetValue来实现注册表的读写操作。这同使用Reg开头的Win32函数操作相比,简单极了。请看:
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 和活动桌面的诞生一起产生的,是WIN98及以后版本的标准组成,你可以放心使用。
十二、注册BHO对象
因为BHO 是一个COM 服务器,所以既应该作为COM 服务器注册又应该作为BHO对象注册。ATL向导自动生成.rgs文件,第一种情况的注册就免除了。下面的文件代码段是用来实现作为BHO对象注册的(CLSID为例中生成)。
HKLM {
SOFTWARE {
Microsoft {
Windows {
CurrentVersion {
Explorer {
''BHO'' {
ForceRemove {1E1B2879-88FF-11D2-8D96-D7ACAC95951F}
}
}
}
}
}
}
}
注意ForceRemove一词能够实现在卸载对象时删除这一行相应的键值。BHO键下聚集了所有的BHO对象。对于这么多的一串家伙是从来不作缓冲调用的。这样以来,安装与测试BHO就是不费时的事情了。
十三、总结
本文描述了BHO对象,通过它你可以把自己的代码注入浏览器的地址空间中。你必须做的事情是写一个支持IObjectWithSite 接口的COM
服务器。在这一点上,你的BHO对象可以实现浏览器机制范围内的各种合法目的。本文所及示例涉及了COM事件,DHTML对象模型以及WEB浏览器编程接口。虽然内容稍宽一些,但它正显示了现实世界中的BHO对象的应用。如,你想知道浏览器在显示什么,那么您就需要了解接收事件并要熟悉WEB浏览器才行。
另外:Windows资源管理器也是与BHO对象交互的,这一点在编程时要特别注意。本文所附源程序为MSDN所带,在Windows2000/VC6下调试通过(编译通过后,重新启动IE即得到结果)。
标签:
本站文章除注明转载外,均为本站原创或翻译。欢迎任何形式的转载,但请务必注明出处、不得修改原文相关链接,如果存在内容上的异议请邮件反馈至chenjj@evget.com