65.9K
CodeProject 正在变化。 阅读更多。
Home

Internet Explorer 的鼠标手势

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.84/5 (91投票s)

2003年9月28日

14分钟阅读

viewsIcon

1310096

downloadIcon

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* 窗口:

  1. 将 WebBrowser 控件嵌入到(例如)一个 MFC 对话框应用程序中,并在 `OnInitDialog` 方法中子类化窗口。
  2. 以某种方式将我的代码注入 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)
© . All rights reserved.