Internet Explorer 的鼠标手势






4.84/5 (91投票s)
2003年9月28日
14分钟阅读

1310096

13366
为 Internet Explorer 添加鼠标手势识别功能。
目录
引言
我读了很多关于 Opera 的文章,所以几个月前我终于下载了它。Opera 有很多我喜欢的功能,但真正杀手级的功能是鼠标手势支持。在使用鼠标手势浏览了几小时后,我彻底被迷住了。
不幸的是,我发现许多页面,特别是那些我必须经常用于工作的页面,渲染得不正确(我不确定是 Opera 渲染页面不正确,还是 HTML 格式不正确——我怀疑是后者)。
本文讨论了我如何为 Internet Explorer 添加鼠标手势支持。所提供的代码并非旨在详述实现的每一个步骤(希望 源代码能够做到这一点);相反,它旨在提供涉及步骤的快速概述。
重要提示:要构建源项目,您需要安装 WTL 7.0。您可以在此处 下载。另请参阅 WTL 的简易安装。
实现
我确定要为 Internet Explorer 添加鼠标手势支持,我需要能够做到三件事:
- 跟踪浏览器窗口中的鼠标移动
- 将鼠标轨迹传递给手势识别算法
- 为最常见的鼠标手势提供操作
跟踪鼠标
使用 Visual Studio 自带的 Spy 应用程序,我发现渲染 HTML 的窗口(因此也是我想跟踪鼠标移动的窗口)的类是 *Internet Explorer_Server*。我需要做的就是以某种方式子类化这个窗口(请参阅 创建您自己的控件 - 子类化艺术 和 使用 WTL 在 ATL 对话框中子类化控件):我将能够为所有鼠标消息添加消息处理程序,一切就绪。
我认为有两种方法可以子类化 *Internet Explorer_Server* 窗口:
- 将 WebBrowser 控件嵌入到(例如)一个 MFC 对话框应用程序中,并在 `OnInitDialog` 方法中子类化窗口。
- 以某种方式将我的代码注入 Internet Explorer。
第二种选择远更具吸引力——谁会想要另一个网络浏览器应用程序呢?
我熟悉 Jeffrey Richter 将 DLL 注入另一个进程的技术(请参阅 API 钩子揭秘),当时我偶然发现了 John Osborn 的 弹出窗口拦截器。在他的文章中,John 讨论了如何使用 ATL 编写浏览器帮助对象(BHO - 一个会附加到每个 Internet Explorer 实例的 DLL)。这正是我想要的。
我快速创建了一个 ATL 项目,实现了 `IObjectWithSite` 接口,很快我就获得了对浏览器 `HWND` 的访问权限。
STDMETHODIMP BrowserHelperObject::SetSite( IUnknown *pUnkSite ) { CComQIPtr< IWebBrowser2 > pWebBrowser = pUnkSite; if( pWebBrowser ) { HWND hWnd; pWebBrowser->get_HWND( (long*)&hWnd ); } }
这个 `HWND` 是一个窗口的句柄,该窗口的类是 `IEFrame`。快速使用 Spy 查看后发现 IE 6.0 实现的(窗口)类继承结构如下:
创建子窗口
在我们浏览器帮助对象的 `SetSite` 方法被调用时,*Shell DocObject View* 和 *Internet Explorer_Server* 窗口尚未创建。
插件的 1.0 版本,通过(临时)子类化 `IEFrame` 窗口并监听 `WM_PARENTNOTIFY` 消息来监视 *Shell DocObject View* 窗口的创建。
class Observer { public: virtual void OnChildCreate( HWND hWnd ) { enum { BUFFER_SIZE = 128 }; TCHAR buffer[ BUFFER_SIZE ]; ::GetClassName( hWnd, BUFFER_SIZE ); } }; class ParentNotifyTracker : public CWindow< ParentNotifyTracker > { public: void Advise( HWND hWnd, Observer *pObserver ) { // // Method to subclass the window hWnd and to associate // the m_pObserver member with the call back interface pointer // pObserver // } BEGIN_MSG_MAP( ParentNotifyTracker ) MESSAGE_HANDLER( WM_PARENTNOTIFY, OnParentNotify ) END_MSG_MAP() LRESULT OnParentNotify( UINT, WPARAM wParam, LPARAM lParam, BOOL &bHandled ) { if( LOWORD( wParam ) == WM_CREATE ) { m_pObserver->OnChildCreate( (HWND)lParam ); } // // set bHandled to FALSE and return zero to ensure the message is // propagated through to the real parent // bHandled = FALSE; return 0; } };
这对于 IE 6.0 效果很好,但我发现当处于多框架文档时,IE 5.0 实现的窗口继承结构与 IE 6.0 不同(IE 6.0 始终使用上面简单的继承结构)。
每个框架都显示在较低级别的 *Internet Explorer_Server* 窗口中。最初,我以为我只需要扩展 ParentNotifyTracker 机制来监视 *Shell Embedding* 窗口的创建,并在多个 *Internet Explorer_Server* 窗口中跟踪鼠标手势。
不幸的是,当 *Shell Embedding* 窗口创建时,它**不会**发送 `WM_PARENTNOTIFY` 消息(似乎 *Shell Embedding* 窗口设置了 `WS_EX_NOPARENTNOTIFY` 风格)。需要一种替代方法来监视窗口创建。
Windows 钩子
通过阅读 API 钩子揭秘,我知道我可以添加一个钩子来捕获所有窗口创建消息。但是,正如 MSDN(以及 API 钩子文章)中所述:
钩子会减慢系统速度,因为它们会增加系统处理每条消息的开销。您应该只在必要时安装钩子,并尽快将其移除。
据我所知,Internet Explorer 是一个多 SDI 应用程序;也就是说,多个文档(或网络浏览器)在同一进程中打开,每个文档都有自己的线程来处理消息。因此,我只需要钩住特定线程的所有消息(而不是安装全局钩子)即可接收窗口创建的通知。虽然这比子类化窗口开销更大,但使用这种技术似乎没有明显的负载增加。
如果有足够的兴趣,我将考虑撰写一篇文章,更详细地描述我如何利用钩子(我用线程本地存储做了一些有趣的事情),但目前 *WindowHook.cpp* 和 *WindowHook.h* 中的源代码应该足以供您参考。
在插件的 1.0 版本中,我没有意识到 Internet Explorer 显示 HTML 文档,然后更新显示(例如)XML 文档时,会创建一个新的 *Internet Explorer_Server* 窗口。此时,插件停止了鼠标跟踪。使用钩子机制使我能够看到所有 *Internet Explorer_Server* 窗口的创建/销毁。
MouseTracker
一旦我们收到 *Shell DocObject View* 窗口创建的通知,我们就可以使用相同的类来监视该窗口,以创建 *Internet Explorer_Server* 窗口。现在我们已经获得了 *Internet Explorer_Server* 窗口,我们可以永久子类化该窗口以接收所有鼠标事件。
typedef std::vector< POINT > Path; class MouseTracker : public CWindowImpl< MouseTracker > { struct Watcher { virtual void OnLeftButtonUp( const Path &path ); virtual void OnRightButtonUp( const Path &path ); }; BEGIN_MSG_MAP( MouseTracker ) MESSAGE_HANDLER( WM_MOUSEMOVE, OnMouseMove ) MESSAGE_HANDLER( WM_RBUTTONDOWN, OnRightButtonDown ) MESSAGE_HANDLER( WM_RBUTTONUP, OnRightButtonUp ) MESSAGE_HANDLER( WM_LBUTTONDOWN, OnLeftButtonDown ) MESSAGE_HANDLER( WM_LBUTTONUP, OnLeftButtonUp ) MESSAGE_HANDLER( WM_MOUSEWHEEL, OnMouseWheel ) END_MSG_MAP() };
当用户按下鼠标按钮时(`WM_LBUTTONDOWN` 和 `WM_RBUTTONDOWN`),我们开始跟踪鼠标。当按钮被按下时,我们将鼠标访问的每个点(来自 `WM_MOUSEMOVE` 消息)存储在某个合适的容器中(例如 STL vector),并在用户松开鼠标时完成跟踪(`WM_LBUTTONUP` 和 `WM_RBUTTONUP`)。当用户释放鼠标时,我们通过 Watcher 回调接口上的 `OnLeftButtonUp` 和 `OnRightButtonUp` 方法将鼠标轨迹通知给我们的客户端。
如果用户将鼠标移出窗口矩形并在跟踪时松开鼠标,我们将不会收到按钮抬起通知(它将被发送到当前鼠标所在的任何窗口)。但是,一旦我们开始跟踪手势,我们就希望一直跟踪直到用户松开鼠标按钮。我们通过在开始跟踪时调用 `::SetCapture`(`WM_LBUTTONDOWN` 和 `WM_RBUTTONDOWN`)并在跟踪完成后调用 `::ReleaseCapture`(`WM_LBUTTONUP` 和 `WM_RBUTTONUP`)来解决此问题。
BrowserWatcher
浏览器监视器类维护一个列表(但使用 `std::map` 实现),其中包含 `MouseTracker` 对象 - 每个创建的 *Internet Explorer_Server* 窗口一个。每当 *Internet Explorer_Server* 被销毁时,相应的 `MouseTracker` 对象将从列表中移除并销毁。
手势识别
到目前为止,我们已经能够子类化 IE 浏览器窗口并在其中记录鼠标移动。现在需要理解这些移动的含义,但从哪里开始呢?我在 CodeProject 网站上快速搜索了一下,找到了 Konstantin Boukreev 的关于 鼠标手势识别 的精彩文章。只需稍作调整,我就能将他的 GestureApp 的源代码集成到我的浏览器帮助对象中。
enum GesturePattern; class GestureTracker : public MouseTracker::Watcher { public: struct Observer { virtual void OnMouseGesture( GesturePattern pattern ); }; // MouseTracker::Watcher private: void OnLeftButtonUp( const Path &path ); void OnRightButtonUp( const Path &path ) { int pattern = m_gesture.Recognize( path ); if( pattern != -1 ) { m_pObserver->OnGesture( pattern ); } } private: Gesture m_gesture; Observer *m_pObserver; };
有关完整实现细节,请参阅源代码中的 *GestureTracker.cpp* 和 *GestureTracker.h* 源代码。
非常感谢 Konstantin 允许我将他的源代码重用于我的插件。
浏览器操作
我的目的是复制 Opera 提供的大部分功能。绝大多数情况下,这都非常容易实现,无论是利用 `IWebBrowser` 接口公开的功能,还是直接向 IE 框架发送 Windows 消息。
命中测试 - IE 6.0
我最想复制的手势是能够在新窗口中打开链接(按住右键向下移动)。起初,这似乎很容易实现:
void HitTest( IWebBrowser2 *pWB, const POINT &pt ) { HRESULT hr; // // retrieve the active document from the Web Browser // CComPtr< IDispatch > pDisp; hr = pWB->get_Document( &pDisp ); // // is it an HTML document? // CComQIPtr< IHTMLDocument2 > pDoc = pDisp; if( pDoc ) { // // retrieve the element (if any) at the point // CComPtr< IHTMLElement > pElement; hr = pDoc->elementFromPoint( pt.x, pt.y, &pElement ); // // is the element (or any of its parents) an anchor element // (the user could have clicked on an image embedded within an // an anchor: // // <a href="blah"><img src="img.jpg"></a> // // In the first instance, elementFromPoint will return the IMG // element. // // The implementation of GetHRefFromAnchor is omitted here, but // it ascends an element hierarchy until reaches an anchor element // or the top of the document. GetHRefAnchor // returns the HREF attribute // of the anchor element, or an empty // string if no anchor could be found. // CComBSTR url = GetHRefFromAnchor( pElement ); if( url.Length() ) { CComPtr< IWebBrowser2 > pApp; hr = pApp.CoCreateInstance( __uuidof( InternetExplorer ) ); // not real function signature - // for illustration only!! hr = pApp->Navigate2( &url ); } } }
这在查看单框架页面时效果很好,但在查看多框架页面(例如 Google 的 USENET 浏览器)时,我经常会导航到我的默认主页,更糟糕的是,会导航到一个随机链接。
考虑一个单框架文档:
假设我们的链接位置是 (x0,y0),相对于浏览器窗口的左上角。
如果我们将同一个页面嵌入到多框架文档中:
假设我们的链接位置是 (x1,y1),相对于浏览器窗口的左上角。
现在当我们使用 (x1,y1) 调用 `elementFromPoint` 时,我们将得到一个指向框架元素的指针(它碰巧也是 `IWebBrowser2` 类型)。我们需要针对这个新的浏览器窗口调用 `elementFromPoint`,但要将我们的点调整为相对于主文档中框架的偏移量 (xf,yf)。
我们的代码现在看起来像这样:
void HitTest( IWebBrowser2 *pWB, const POINT &pt ) { HRESULT hr; CComPtr< IDispatch > pDisp; hr = pWB->get_Document( &pDisp ); CComQIPtr< IHTMLDocument2 > pDoc = pDisp; if( pDoc ) { CComPtr< IHTMLElement > pElement; hr = pDoc->elementFromPoint( pt.x, pt.y, &pElement ); CComQIPtr< IWebBrowser2 > pFrame = pElement; if( pFrame ) { // // GetElementOffset retrieves the offset of the given // element relative to the origin of the web browser // SIZE offset = GetElementOffset( pElement ); POINT ptInFrame = { pt.x - offset.cx, pt.y - offset.cy }; return HitTest( pFrame, ptInFrame ); } CComBSTR url = GetHRefFromAnchor( pElement ); if( url.Length() ) { CComPtr< IWebBrowser2 > pApp; hr = pApp.CoCreateInstance( __uuidof( InternetExplorer ) ); // not real function signature - // for illustration only!! hr = pApp->Navigate2( &url ); } } }
命中测试 - IE 5.0
所有这些对于 IE 6.0 都非常有效,但您还记得 IE 5.0 的多框架窗口继承结构与 IE 6.0 不同。
由于我们现在有了不同的窗口继承结构,我不得不重新研究“在新窗口中打开链接”功能的命中测试(记住我们不再有嵌入式框架元素,而是有嵌入式窗口)。快速谷歌搜索后,我找到了以下机制来检索与特定窗口关联的文档:
const UINT WM_HTML_GETOBJECT = ::RegisterWindowMessage( _T( "WM_HTML_GETOBJECT" ) ); HRESULT GetDocument( HWND hWnd, IHTMLDocument2 **ppDocument ) { DWORD res = 0; if( ::SendMessageTimeout( hWnd, WM_HTML_GETOBJECT, 0, 0, SMTO_ABORTIFHUNG, 1000, &res ) == 0 ) { return E_FAIL; } return ::ObjectFromLresult( res, IID_IHTMLDocument2, 0, reinterpret_cast< void ** >( ppDocument ) ); }
我们的命中测试现在可以这样实现:
HRESULT HitTest( HWND hWnd, const POINT &pt ) { HRESULT hr; CComPtr< IHTMLDocument2 > pDoc; hr = GetDocument( hWnd, &pDoc ); if( pDoc ) { CComPtr< IHTMLElement > pElement; hr = pDoc->elementFromPoint( pt.x, pt.y, &pElement ); // // Extract the url and create the web browser window as before // } return hr; }
我已经用 IE 5.5 运行了该插件(谢天谢地!)它具有与 IE 6.0 相同的窗口类继承结构。请注意,IE 4.0 **不支持**浏览器帮助对象,因此该插件在该平台上将无法工作。
与其他浏览器帮助对象冲突
许多插件 1.0 版本的用户告诉我,每当他们使用鼠标右键执行任何手势时,插件就会崩溃。当然,在我(们)的开发机器上一切正常 ;-)!!
tong_du 指出,当他安装了 Google 工具栏时,鼠标手势就失效了。他卸载 Google 工具栏后,鼠标手势就能正常工作。
Jeff Fitzsimons 调试了代码,发现 `SynthesiseRightClick` 方法未能(临时)取消子类化窗口,导致插件发送 `WM_RBUTTONDOWN`/`WM_RBUTTONUP` 导致 `SynthesiseRightClick` 函数被递归调用,从而导致堆栈溢出。(参见 Re: Right click problem: too。)
调用 `UnsubclassWindow` 失败是因为窗口存储的 `WNDPROC`(参见 `GetWindowLong` 和 `GWL_WNDPROC`)与窗口的 thunk 中存储的 `WNDPROC` 不同(参见 ATL Internals 的 p423-429 页,了解 ATL 如何使用 thunk 的解释)。我猜测,但怀疑 `UnsubclassWindow` 失败的原因是另一个 ATL `CWindowImplBaseT` 也子类化了 *Internet Explorer_Server*。
我通过在 `MouseWatcher` 类中设置一个标志(`m_isSuspended`)来解决这个问题,当设置该标志时,所有消息都会被忽略。请参阅 *MouseTracker.h* 中的 `BEGIN_MSG_MAP_EX` 宏。
非常感谢 tong_du、Jeff Fitzsimons 和 Rich Buckley 在跟踪此问题时提供的帮助。谢谢你们,伙计们!!!
与从右到左脚本冲突
一些插件用户注意到,在使用从右到左脚本(如希伯来语或阿塞拜疆语(西里尔语))时,键盘输入会出错。在几个月无法在我的开发机(或多台)上重现这个问题后,我终于找到了问题所在。最初的鼠标手势插件使用了 MBCS 字符集。一旦我将插件重建为使用 Unicode 字符集,键盘输入无论有无插件都能正常工作。
我认为在一个窗口上同时拥有 MBCS 和 Unicode 消息处理程序不是个好主意!非常感谢 lkj1、orlink 和 Eli Stern 在我尝试解决这个 bug 时提供了宝贵的反馈。
使用鼠标手势
安装/注册鼠标手势插件后,您只需打开一个新的 Internet Explorer 实例。如果一切正常,您应该会在 IE 的“工具”菜单中看到一个新的条目“鼠标手势...”
单击此选项应会弹出以下对话框:
为了让您了解如何使用鼠标手势:
- 将鼠标移动到位图上(目前显示一辆汽车绕圈行驶)。
- 按住鼠标右键,然后向下拖动鼠标 - 您不必移动太远。
- 松开鼠标右键。
希望位图已更新为显示一个向下箭头。
就是这么简单。滚动“手势”组合框中的项目,以查看当前支持的手势以及与手势关联的操作。
要将您自己的操作与特定手势关联,只需选择手势(或在图像上执行它),选择操作,然后单击“应用”。
用户操作
为了支持更广泛的操作,鼠标手势插件允许您将键盘快捷键与手势关联。
将键盘焦点设置到“快捷键”编辑框,然后按住所需的键。编辑框将更新以显示快捷键。请注意,无效的快捷键(如单独的 Ctrl 键)是不允许的,并且在快捷键窗口中将显示为“无”。
现在,每当您执行相应的手势时,鼠标手势插件都会将等效的按键发送到 Internet Explorer。
'在新窗口中打开' 操作
关于“在新前台/后台窗口中打开”操作的一个特别说明:
- 如果手势是在超链接上启动的,则该超链接的目标将在新窗口中打开(与右键单击超链接并选择“在新窗口中打开”相同)。
- 如果手势是在高亮文本区域上启动的,那么鼠标手势插件会将该文本视为超链接目标,并尝试在新窗口中打开该页面。
- 如果手势在浏览器窗口中的任何其他位置启动,则会在您的主页上打开一个新窗口。
鼠标轨迹
插件最受欢迎的增强功能之一是能够绘制鼠标轨迹。这似乎很有趣,所以当我最终决定去做这件事时,我查看了该领域已有的工作。我发现了一个适用于 Firefox 浏览器的插件,但它生成的轨迹充其量是基础的。我想要看起来像 XP 而不是 Win3.1 的效果!
经过大量原型设计,我提出了一个符合所有要求的方案。要查看鼠标轨迹的工作原理,请选择“鼠标手势”配置对话框的“鼠标轨迹”选项卡。
确保选中“启用鼠标轨迹”,并在感叹号图标上执行鼠标手势。轨迹应该看起来像这样:
当您结束手势时,轨迹会逐渐淡入背景。您可能需要调整设置,以确定适合您 PC 的最佳设置组合。**注意:**您需要一个相当强大的显卡才能获得完整的鼠标轨迹体验。在 2MB 显卡上也可以绘制轨迹,但这可能会导致鼠标稍微卡顿。
有关鼠标轨迹窗口实现的详细信息,请参阅附件源代码中的 *MouseTrail.h* 和 *TrailWindow.h*。
改进
我最终打算添加的一些额外功能:
- 为使用嵌入式 Microsoft Web Browser 控件的应用程序(如 MSDN 和 Outlook)添加手势支持。
- 更新安装程序,以便在 IE 或 Explorer 打开 DLL 实例时将其移除。
欢迎提出任何其他建议。
修订历史
- 2003年9月25日:初始版本 (1.0)
- 2003年11月4日:版本 1.0.0.4 - 错误修复
- 2004年4月9日:版本 1.0.1.1 - 更新以支持从右到左脚本并兼容 Visual Studio .NET 2003
- 2004年5月27日:版本 1.0.3.1 - 错误修复(特别感谢 **dchris_med**!)
- 2004年6月10日:版本 1.0.4.1 - 添加了新操作和手势
- 2004年6月24日:版本 1.1.0.2 - 添加了对键盘快捷键的支持(和错误修复)
- 2005年9月7日:版本 1.2.0.1 - 添加了鼠标轨迹,支持 IE7(Beta 1)