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

使用鼠标手势优化屏幕区域

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (14投票s)

2007 年 12 月 7 日

CPOL

9分钟阅读

viewsIcon

64284

downloadIcon

601

本文介绍了一种创建能够感知 .NET Compact Framework 上鼠标手势的 Panel 的方法。

Screenshot - MouseGestures.jpg

引言

使用鼠标手势的用户输入是一种技术,其中鼠标绘制的路径而不是按下按钮(例如)来启动操作。

在 UI 控件空间有限或 UI 控件会使 UI 显得杂乱且不美观的情况下,可以使用鼠标手势来优化屏幕使用。

本文旨在介绍如何实现一个 System.Windows.Forms.Panel,它可以捕获和识别在其上绘制的鼠标手势,而无需考虑任何子组件。

Using the Code

本文的示例应用程序是一个用于浏览和重新定向图片的应用程序,该应用程序的预期目的是修复刚用 Windows Mobile 设备上的摄像头拍摄的照片。鼠标手势在屏幕空间非常有限的 PDA 上更有用。因此,下载中包含两套源项目,一套用于 .NET Compact Framework,另一套用于 .NET Framework。
它们被标记为DeviceDesktop

要求

该项目旨在实现以下五项要求

  1. System.Windows.Forms.Panel 应作为基础控件。
  2. 所有必需的鼠标事件都应由基础控件自动处理。
  3. 手势路径必须完全可配置。
  4. 鼠标手势识别速度必须足够快,以便在 Windows Mobile 设备上运行。
  5. 基础控件必须独立于任何子控件工作。

由于该项目主要针对 .NET Compact Framework,我为实现的一些要求与适用于桌面环境的要求略有不同。

如何识别手势(某种程度上)

我猜测有几种方法可以实现识别鼠标手势的组件,但对于这个项目,为了满足要求4,我采用了一种相当简单直接的方法。

我决定使用一系列矩形,而不是对鼠标刚刚绘制的路径进行分析。该方法可以检查鼠标路径上的一个点是否在其中一个矩形内。很简单!
但需要一些额外的复杂性,因为仅检查一组矩形将意味着该方法无法区分从左到右的笔触和从右到左的笔触。通过将矩形保存在有序列表中,可以检查鼠标路径是否按正确的顺序与矩形相交。

此时,敏锐的读者可能会说:“嘿,等等!那不是真正的鼠标手势!”
这是完全正确的,因为我的方法是检查矩形内的点,它实际上并不识别手势,而是识别固定的路径。这是因为我希望实现简单且快速,而在 PDA 上,其小屏幕和用于鼠标的手写笔对此并不重要。

这意味着,要识别一个手势,它必须在屏幕(实际上是在 MouseGesture 面板)上的特定区域开始并经过。

Rectangles and a mouse path

上图显示了一个测试应用程序的屏幕截图,该应用程序运行了一个 Bornander.UI.MouseGesturePanel,处于调试视图中,在这种模式下,它会渲染每个已设置为识别的鼠标手势的矩形,而不是渲染控件的子控件。
在此示例中,只添加了一个手势作为可识别手势,面板会渲染矩形、其名称以及(括号内)决定其在手势中出现顺序的矩形的索引。(红色箭头不是由面板渲染的,它是之后添加的,以显示这些矩形旨在捕获的手势)。
我将在本文稍后更详细地描述识别手势的方法。

记录鼠标路径

需要解决的第一个问题是如何记录鼠标(或手写笔)刚刚绘制的路径。起初这个问题可能看起来很简单,只需记录 MouseDownMouseUp 事件之间发生的所有 MouseMove 事件,但由于要求5,这将不可行。鼠标事件是按控件发生的,这会引起问题,因为一条路径可能从一个控件开始(生成第一个 MouseDown),移动到几个其他控件(生成一系列 MouseMove),然后在释放鼠标按钮时结束(生成最后一个 MouseUp 事件)。
此外,每个控件都以控件局部坐标报告鼠标事件,这意味着控件的左上角始终是0, 0,而与控件在其父控件中的位置无关。

为了解决这个问题,我实现了一个名为 Bornander.UI.MouseGesturePanelSystem.Windows.Forms.Panel,它递归地将鼠标侦听器挂接到其所有子控件及其子控件。这样,它就可以收集在它或其任何子控件上绘制的任何鼠标路径。
应在所有控件已添加到面板后调用 MouseGesturePanel.Initialize 方法。这将启动递归,添加所有鼠标事件侦听器。

public partial class MouseGesturePanel : Panel
{
    public event MouseGestureRecognizedHandler MouseGestureRecognized;

    #region Private members

    // Instead of creating new handlers for each control to listen to
    // these are used.
    private MouseEventHandler globalMouseDownEventHandler;
    private MouseEventHandler globalMouseUpEventHandler;
    private MouseEventHandler globalMouseMoveEventHandler;

    // A list of all the points collected during the last mouse stroke.
    private List<Point> points = null;

    // A list of all the gestures that this panel can recognize.
    private List<Gesture> gestures = new List<Gesture>();

    // If this is set to true by MouseGesturePanel.ToggleGestureDebugView
    // then the Panels controls are removed and the gesture rectangles
    // are rendered instead. This is purely for debugging purposes.
    private bool debugView = false;

    // This is a helper for the debug view mode.
    private List<Control> debugControlStore = new List<Control>();

    #endregion

    public MouseGesturePanel()
    {
        InitializeComponent();

        globalMouseDownEventHandler = new MouseEventHandler(GlobalMouseDownHandler);
        globalMouseUpEventHandler = new MouseEventHandler(GlobalMouseUpHandler);
        globalMouseMoveEventHandler = new MouseEventHandler(GlobalMouseMoveHandler);
    }

    /// <summary>
    /// Recursive method that add mouse event handlers for a
    /// Control and all its children.
    /// Remember; to iterate is human, to recurse is divine!
    /// </summary>
    private void AddHandlers(Control control)
    {
        // First attempt to remove the event handlers just to be safe
        control.MouseDown -= globalMouseDownEventHandler;
        control.MouseUp -= globalMouseUpEventHandler;
        control.MouseMove -= globalMouseMoveEventHandler;

        // Add the handler to the current control
        control.MouseDown += globalMouseDownEventHandler;
        control.MouseUp += globalMouseUpEventHandler;
        control.MouseMove += globalMouseMoveEventHandler;

        // Recurse for each child control and add the handlers to them
        // as well.
        foreach (Control childControl in control.Controls)
        {
            AddHandlers(childControl);
        }
    }

    /// <summary>
    /// This method initializes the gesture listening events, this should
    /// be called by the code creating this Panel when all controls
    /// have been added to it.
    /// </summary>
    public void Initialize()
    {
        AddHandlers(this);
    }

    private void FireMouseGestureRecognized(Gesture gesture)
    {
        if (MouseGestureRecognized != null)
            MouseGestureRecognized(gesture);
    }

    ...
}

显然,让 MouseGesturePanel 仅仅挂接到这些事件是不够的,当事件触发时需要进行某种形式的处理。

第一个触发的事件是 MouseDown,对此的处理很简单,只需准备处理 MouseMove 事件。这是通过创建一个空的 List<Point> 来完成的,以保存将构成鼠标路径的点。

private void GlobalMouseDownHandler(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
        points = new List<Point>();
}        

第二个触发的事件是在拖动鼠标时触发的一系列 MouseMove 事件。与第一个事件一样,处理这些也很简单;只需将位置存储在 MouseGesturePanel.GlobalMouseDownHandler 中准备好的列表中。
一个名为 Utilities.GetAbsolute静态实用方法将 MouseEventArgs e 参数中传递的事件控件局部坐标转换为 MouseGesturePanel 局部坐标。

private void GlobalMouseMoveHandler(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        // Convert the point to absolute coordinates, that is coordinates
        // that are local to this panel rather than the control that captured them.
        Point absolutePoint =
        Utilities.GetAbsolute(new Point(e.X, e.Y), (Control)sender, this);
        points.Add(absolutePoint);
    }
}    

Utilities.GetAbsolute 方法只需从当前控件向上迭代到控件树,直到到达作为最后一个参数传递的控件,然后用控件的位置偏移该点。

public static Point GetAbsolute(Point point, Control sourceControl,
        Control rootControl)
{
    Point tempPoint = new Point();
    for (Control iterator = sourceControl; iterator != rootControl;
    iterator = iterator.Parent)
    {
        tempPoint.Offset(iterator.Left, iterator.Top);
    }

    tempPoint.Offset(point.X, point.Y);
    return tempPoint;
} 

触发的最后一个事件需要更多的处理,因为直到此时,MouseGesturePanel 才真正尝试识别手势。MouseGesturePanel 遍历它拥有的所有 Gesture,并将当前鼠标路径传递给它们,以查看是否有匹配项。如果找到匹配项,则会触发一个事件来通知侦听器已识别出手势。

private void GlobalMouseUpHandler(object sender, MouseEventArgs e)
{
    if (e.Button == MouseButtons.Left)
    {
        foreach (Gesture gesture in gestures)
        {
            if (gesture.IsGesture(points))
            {
                // Check on which level this gesture is supposed to fire
                switch (gesture.EventFireLevel)
                {
                    case GestureRecognizeEventFireLevel.Panel:
                    FireMouseGestureRecognized(gesture);
                    break;

                    case GestureRecognizeEventFireLevel.Gesture:
                    gesture.FireMouseGestureRecognized();
                    break;
                }
                return;
            }
        }
    }
}

中间的 switch 语句的原因是我希望能够在两个级别上侦听手势识别事件

  1. 面板级别
  2. 手势级别

这意味着有两种不同的地方和两种不同的方式来挂接和侦听这些事件。

面板级别

如果需要为手势执行的处理很简单,即代码行数很少,则在此级别挂接手势识别侦听器很方便。这是用于处理预设手势,所有这些手势都非常简单。这意味着同一个事件会为不同的手势触发,并且必须在处理程序方法中区分这些手势。

手势级别

在此级别,Gesture 会触发事件,这意味着事件始终由同一个 Gesture 触发。

示例

这是一个同时使用两个级别的事件的示例

class TestClass
{
    private Gesture simpleGestureA;
    private Gesture simpleGestureB;
    private Gesture simpleGestureC;
    private Gesture complexGesture;

    public TestClass()
    {
        MouseGesturePanel mouseGesturePanel = new MouseGesturePanel();

        // First, three simple Gestures
        simpleGestureA = new Gesture("A", mouseGesturePanel,
                                     GestureRecognizeEventFireLevel.Panel);
        simpleGestureB = new Gesture("B", mouseGesturePanel,
                                     GestureRecognizeEventFireLevel.Panel);
        simpleGestureC = new Gesture("D", mouseGesturePanel,
                                     GestureRecognizeEventFireLevel.Panel);
        // Then a complicated Gesture
        complexGesture = new Gesture("Complex", mouseGesturePanel,
                                     GestureRecognizeEventFireLevel.Gesture);
        // Hook a listener to the MouseGesturePanel, this will fire
        // for each recognized Gesture that has Panel as fire level.
        mouseGesturePanel.MouseGestureRecognized +=
            new MouseGestureRecognizedHandler(HandleSimpleGestures);

        // Hook a listener to the Gesture, this will only fire
        // when complexGesture has been recognized.
        complexGesture.MouseGestureRecognized +=
            new MouseGestureRecognizedHandler(HandleComplexGesture);
    }

    // Handler for the complex gesture
    void HandleComplexGesture(Gesture gesture)
    {
        if (FooBar.Bar() && Foo.Bar != 0)
        {
            switch (FooBar.Value)
            {
                case FooBar.Foo:
                    FooBar.Foo();
                    break;
                case FooBar.Bar:
                    FooBar.Bar();
                    break;
            }
        }
    }

    // Handler for all the simple gestures
    void HandleSimpleGestures(Gesture gesture)
    {
        if (gesture == simpleGestureA)
            FooBar.Foo();

        if (gesture == simpleGestureB)
            FooBar.Bar();

        if (gesture == simpleGestureC)
            Foo.Bar();
    }
}    

定义手势

手势由名称(主要用于调试目的)和构成鼠标路径的一系列矩形组成。为了允许 MouseGesturePanel 调整大小,矩形不是以绝对坐标指定的,而是以相对坐标指定的,并且在 MouseGesturePanel 调整大小时会将其转换为绝对坐标。
这意味着,在创建 Gesture 并向其添加矩形时,它们会作为 System.Drawing.RectangleF 添加,并且所有值(TopLeftWidthHeight)都应落在0.0 < value < 1.0 的范围内。
因此,要创建位于屏幕左下角且覆盖屏幕区域 1/16 的正方形矩形,应使用以下代码

Gesture gesture = new Gesture("Test", mouseGesturePanel,
            GestureRecognizeEventFireLevel.Gesture);
gesture.AddRectangle(new RectangleF(0.0f, 0.75f, 0.25f, 0.25f)); 

Gesture 本身会自动为其 MouseGesturePanel resize 事件添加侦听器,并在该面板大小更改时重新计算绝对矩形。然后,实际识别手势就像迭代所有点并检查它们是否按正确的顺序落在矩形内一样简单。

public class Gesture
{
    /// <summary>
    /// Helper method that returns the index for the rectangle that a
    /// point falls within.
    /// </summary>
    /// <param name="point">The point to test</param>
    /// <returns>The index of the rectangle or -1 if not inside any rectangle.</returns>
    private int GetRectangleIndexForPoint(Point point)
    {
        for (int i = 0; i < absoluteRectangles.Count; ++i)
        {
            if (absoluteRectangles[i].Contains(point))
                return i;
        }
        return -1;
    }

    /// <summary>
    /// Determines whether a list of Points make up this specific gesture.
    /// The path is considered to be this gesture if all mousePath that fall within
    /// rectangles of this gesture fall in those rectangles in order. Points outside
    /// the gesture rectangles are ignored, as are multiple mousePath in the same
    /// rectangle.
    /// </summary>
    /// <param name="mousePath">The list of point representing the path the mouse
    /// just moved.</param>
    /// <returns>True if the path represents this gesture.</returns>
    public bool IsGesture(List<Point> mousePath)
    {
        // With no mousePath at all making up the path it
        // cannot possible be a valid gesture
        if (mousePath.Count > 0)
        {
            // We'll proceed with checking for gesture match only
            // if the path starts in the first rectangle of the gesture
            if (GetRectangleIndexForPoint(mousePath[0]) == 0)
            {
                int currentRectangle = 0;
                foreach (Point point in mousePath)
                {
                    // Get the rectangle index that this point is inside,
                    // this will return -1 if it's not inside any of this
                    // gestures rectangles.
                    int pointRectangleIndex = GetRectangleIndexForPoint(point);
                    if (pointRectangleIndex != -1)
                    {
                        // If if
                        if ((pointRectangleIndex == currentRectangle) ||
                            (pointRectangleIndex == currentRectangle + 1))
                        {
                            if (pointRectangleIndex == currentRectangle + 1)
                                currentRectangle = pointRectangleIndex;
                        }
                        else
                            return false;
                    }
                }

                // Only return true if the last hit rectangle was the final one
                return currentRectangle == absoluteRectangles.Count - 1;
            }
        }

        return false;
    }
}    

这就是有效鼠标手势的全部内容。至少在 PDA 上是这样。

示例应用程序

我创建的用于测试此实现的示例应用程序,如引言中所述,是一个可用于修复照片方向(横向或纵向)的应用程序。它允许用户浏览目录中的所有 *.jpg 文件,并为每张图片,用户都可以旋转它。如果对结果满意,旋转后,用户可以以新的旋转方式保存图片。简单,不太有用,但对这个项目来说是一个很好的测试平台,因为它是一种不希望充斥着按钮的应用程序(我认为)。

用户可用的命令(或手势)是

手势 描述
Exit gesture 退出
Next image gesture 下一篇
Previous image gesture 以前
Rotate left gesture 左转
Rotate right gesture 右转
Show preview gesture 显示预览
Hide preview gesture 隐藏预览
Save gesture 保存

最终结果

那么实现效果如何?

我认为我设法很好地实现了所有要求,使用测试应用程序时效果非常好,并且您可以很快习惯新的导航方式。
我不太满意的是该方法识别的是固定路径而不是手势,这限制了它在桌面环境中的使用。

未来改进

我有一些关于如何构建一个基于当前绘制的鼠标手势(而不是 MouseGesturePanel 客户端区域)的动态区域的想法,这理论上可以使该方法检测到实际的手势。如果我有时间,我会尝试实现它,并在本文中进行更新。

关注点

示例应用程序中的图片使用一种快速(至少对于 .NET Compact Framework 而言)的方法进行旋转,我已经在此处进行了描述。

欢迎所有评论和建议。

历史

  • 2007-12-07: 初始版本
© . All rights reserved.