Internet Explorer 编程简述(十)响应来自HTML Element的事件通知——几个好用的类
关键字:HTML Element, Sink
1、概述
实现了对 Webbrowser 的 resuing 之后我们便会发现有时候我们还需要处理浏览器中的元素(HTML Element)。这种处理包括主动和被动两个方面,像《FAQ:如何访问WebBrowser的滚动条》、《FAQ:操纵下拉列表》、《FAQ:两种方法访问多层嵌套的frame》等文章所演示的就是主动的处理。通常我们从 Webbrowser 获得一个 Web 文档接口(IHTMLDocumentx),从它出发便可访问到浏览器所包含的一切 HTML 元素。而被动的处理则是在 COM 技术中称为 Sink 的技术,我更喜欢的说法是事件通知。当文档的下载进度发生变化时,我们可以获得 ProgressChange 通知,当 Webbrowser 下载完 HTML 文档时,我们可以获得 DocumentComplete 的通知,而当链接被点击,或图片被拖动时,我们如何获得通知呢?本文希望能够给出部分的答案。
2. HtmlObj Template
如何 Sink 一个 HTML Element 并不是本文的重点,其理论我不是太了解,也懒得去搞透彻,所以使用现成的库来实现。CodeProject 上的一篇文章《CHtmlObj Template》给出的一个模板类 CHtmlObj 就非常好用。下面的例子是针对 Html Anchor Element 的一个实例化。
#include "HtmlObj.h"
class CHtmlAnchorElement
: public CHtmlObj<IHTMLAnchorElement, &DIID_HTMLAnchorEvents>
{
public:
CHtmlAnchorElement(CHtmlDocument2* pParentDoc2);
virtual ~CHtmlAnchorElement();
virtual HRESULT OnInvoke(DISPID dispidMember,REFIID riid,
LCID lcid, WORD wFlags,
DISPPARAMS * pdispparams, VARIANT * pvarResult,
EXCEPINFO * pexcepinfo,
UINT * puArgErr);
};
HRESULT CHtmlAnchorElement::
OnInvoke(DISPID dispidMember,
REFIID riid, LCID lcid, WORD wFlags,
DISPPARAMS * pdispparams, VARIANT * pvarResult,
EXCEPINFO * pexcepinfo,
UINT * puArgErr)
{
HRESULT hr = E_NOTIMPL;
switch(dispidMember)
{
case DISPID_HTMLELEMENTEVENTS_ONMOUSEOVER :
{
// 当鼠标经过链接时,我们在这里获得通知
hr = S_OK;
// TODO: add code to handle on mouse over events
break;
}
case DISPID_HTMLELEMENTEVENTS_ONMOUSEOUT :
{
// 当鼠标从链接上移开时,我们在这里获得通知,
// 其它的 Dispatch ID 可根据需要添加
hr = S_OK;
// TODO: add code to handle on mouse out events
break;
}
default:
{
break;
}
}
return hr;
}
当我们得到某个链接的 HTML 接口指针,便可调用 CHtmlAnchorElement 继承自 CHtmlObj 的 SetSite(IUnknown *pUnkSite) 成员函数传入该接口指针。在 CHtmlObj 类内部用一个智能指针 m_spHtmlObj 来保存相应的 HTML Element 接口指针,所以当上面的 ONMOUSEHOVER 和 ONMOUSEOUT 两个事件通知到达时,从 m_spHtmlObj 就可以访问 IHTMLAnchorElement 的所有成员,如从 href 获得链接的 Url 等,此处不再赘述。
3. CHtmlElements 类
有了 CHtmlObj 之后我们又会发现实践中常常会需要多个相同类型的 CHtmlObj。比如包含Frame的网页中每个Frame的HTML Document都需要一个CHtmlObj来Sink其事件。所以我们还需要有效地管理这些相同类型的CHtmlObj。下面是我写的一个简单的模板类CHtmlElements,它通过CMap来管理多个CHtmlObj对象。
template<class THtmlElement> class CHtmlElements
{
typedef CMap<LPDISPATCH, LPDISPATCH, THtmlElement*, THtmlElement*> CMapDispToHtmlElement;
CMapDispToHtmlElement m_htmlElements;
BOOL IsSiteConnected( LPDISPATCH pDisp )
{
THtmlElement *pElement;
return m_htmlElements.Lookup( pDisp, pElement );
}
public:
CHtmlElements(void)
{}
~CHtmlElements(void)
{}
public:
void SetSite( LPDISPATCH pDisp )
{
// 检查以避免多余的 Sink
if ( IsSiteConnected( pDisp ) )
{
return;
}
// 通过模板类型创建相应的类的实例进行连接
THtmlElement *pElement = new THtmlElement;
pElement->SetSite( pDisp );
m_htmlElements.SetAt( pDisp, pElement );
}
//在合适的地方调用Clear释放所管理的内存
void Clear(void)
{
POSITION pos = m_htmlElements.GetStartPosition();
THtmlElement *pElement = NULL;
LPDISPATCH pDisp = NULL;
while (pos != NULL)
{
m_htmlElements.GetNextAssoc( pos, pDisp, pElement );
m_htmlElements.RemoveKey( pDisp );
delete pElement;
}
}
};
假设我们有一个象 CHtmlAnchorElement 那样派生自 CHtmlObj 的类 CHtmlDocument2,使用 CHtmlElements 时这样声明:
typedef CHtmlElements<CHtmlDocument2> CHtmlDocuments;
typedef CHtmlElements<CHtmlAnchorElement> CHtmlAnchors;
class CMyView : public CHtmlView
{
private:
CHtmlDocuments m_htmlDocs;
CHtmlAnchors m_htmlAnchors;
}
在 DocumentComplete 时就可以这样连接到浏览器的文档对象:
void CMyView ::OnDocumentComplete(LPDISPATCH pDisp, LPCTSTR lpszURL)
{
m_htmlDocs.SetSite(pDisp);
}
如果想一次性连接上文档中所有的 Anchor Element,可以通过 IHTMLDocument2::get_anchors 获得包含所有 IHTMLAnchorElement 接口指针的 IHTMLElementCollection,再遍历其中的每个元素,分别调用m_htmlAnchors.SetSite即可。当然,一次性的Sink全部链接可能并不是个好注意,我更愿意在CHtmlDocument2中响应事件再通过其它手段来访问当前位置的HTML Element。
4、结论
响应HTML Element的事件通知对于浏览器编程来说是一个非常强大的手段,它可以更深入细化地控制浏览器中的文档及其HTML元素,实现更为高级的功能,比如所谓的“超级拖放”(许多多窗口浏览器都提供了该功能,但实际上没有哪个浏览器完美地实现了对URL、文字及图片的拖放)。
5、参考资料
Codeproject: HtmlObj Template
Handling HTML Element Events in MFC applications
Integration with Shell Sound Events

近期评论