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

用于触摸设备的触摸手势识别

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (32投票s)

2010 年 2 月 4 日

CPOL

8分钟阅读

viewsIcon

141362

downloadIcon

7273

适用于 Windows 7 中 WPF 的多点触控简单和复合手势识别实现。

注意:运行此程序需要 .NET 3.5 SP1 和支持多点触控的硬件,或者使用 Multi-touch Vista 来模拟多点触控。

Touch Gestures Screenshot

目录

引言

Windows 7 中新引入的功能之一是触摸 API。它允许开发者超越鼠标和键盘。现在,应用程序可以与用户进行触摸交互。触摸 API 支持丰富的预定义手势,如缩放、平移和旋转。多点触控 API 提供原始触摸数据输入和对象的高级操作,包括惯性(一种物理模型,有助于定义现实生活中的操作、属性和动作,如速度、速率、加速度、减速度和重量)。

触摸手势是一款使用 Windows 7 多点触控 API 开发的应用程序,它在 WPF 应用程序中提供简单的手势识别支持。它可以识别基本的左、右、上、下滑动手势;此外,它还支持由基本手势组成的某些复合手势,如先左后右、先左后下等。该应用程序采用了与《为 .NET Windows 应用程序添加鼠标手势支持》文章中的方法略有不同的方法。

所需软件

应用程序结构

该应用程序包含五个类和一个枚举,它们是:

    • App:此类是应用程序的入口点。我们创建一个 MainWindow 实例并运行它。我们还在域级别捕获所有未处理的异常。如果发生任何 try/catch 块未处理的异常,我们将在此处捕获该异常。
    • MainWindow:此类作为应用程序的用户界面。它在画布上显示所有支持的手势图像。当应用程序识别出手势时,将在手势图像周围显示一个用随机颜色绘制的边界(如上图所示)。边界将在预定义的时间间隔后消失。
    • GestureTracker:此类用于记录特定手势。它只需保留手势所经过的所有点的列表。此类还负责通过显示边界来突出显示识别出的手势图像。为此,它使用一个 DispatcherTimer 对象。在预定义的时间间隔后,计时器停止,矩形消失。
    • GestureTrackerManager:此类用于记录所有触摸手势。如果用户使用多个手指创建多个触摸手势(如果硬件设备支持),则此类会将所有触摸手势记录在字典中,以触摸 ID 作为键。此类还检查是否正在跟踪特定的触摸 ID。
    • GestureInterpreter:此类是一个实用/帮助类,用于识别手势。此类可以识别基本手势和复合手势,前提是有一个包含手势移动过的所有点的列表。
  • 枚举
    • TouchGestureType:此枚举提供了简单手势和复合手势的所有方向名称。对于基本手势,名称很明显:左、右、上、下。对于复合手势,名称类似于 LeftDown,这意味着手势先向左移动,然后向右下移动。

准备接收多点触控输入

.NET 3.5 不支持多点触控(多点触控事件和控件是 .NET 4.0 的一部分)。为了在 .NET 3.5 SP1 中使用多点触控,我们必须使用 Windows 7 集成库示例。为简单起见,它已放置在项目中的 Win7LibSample 文件夹中。

要向您的应用程序添加多点触控支持,请添加对 Windows7.Multitouch.dll 和 Windows7.Multitouch.WPF.dll 的引用(同时添加 using 语句)。为了检查设备上是否启用了多点触控,请在 MainWindow 的构造函数中使用下面提供的代码。

using Windows7.Multitouch;
using Windows7.Multitouch.WPF;

// check if multi-touch capability is available
if (!TouchHandler.DigitizerCapabilities.IsMultiTouchReady)
{
    MessageBox.Show("Multitouch is not availible");
    Environment.Exit(1);
}

// enable stylus (touch) events
this.Loaded += (sender, args) => { Factory.EnableStylusEvents(this); };

处理多点触控事件

触摸事件不由 MainWindow 类处理。它们被传递给 GestureTrackerManager 类,GestureTrackerManager 会对事件采取适当的操作。在进一步操作之前,我们必须订阅 StylusDownStylusMoveStylusUp 事件。此外,声明并创建一个 GestureTrackerManager 对象,该对象将用于正确处理事件。

private readonly GestureTrackerManager gestureTrackerManager;
gestureTrackerManager = new GestureTrackerManager(canvas);

// register for stylus (touch) events
StylusDown += gestureTrackerManager.Process_TouchDown;
StylusUp += gestureTrackerManager.Process_TouchUp;
StylusMove += gestureTrackerManager.Process_TouchMove;

现在,让我们继续处理事件以及如何在 GestureTrackerManager 类中处理它们。这里有一个重要的点是,我们还向 GestureTrackerManager 传递了画布对象的只读引用,然后又传递给 GestureTracker 对象。这是为了在识别出手势后能够添加和删除图像周围的矩形。在 GestureTrackerManager 中,我们使用一个 Dictionary(以触摸 ID 作为键)来跟踪所有触摸手势。实际的点(手势移动过的轨迹)存储在 GestureTracker 类中。

// a map for tracking all gestures and associated device Ids
private readonly Dictionary<int,> gestureTrackerMap;

// a reference for the canvas in MainWindow.xaml
private readonly Canvas canvas;

在 Stylus 事件中,我们通过 args.GetPosition(canvas) 获取相对于画布的当前手势位置,然后检查我们是否已在跟踪该手势(使用 args 中的 StylusDevice.Id)。如果用户使用两根手指进行两个不同的手势,那么两个 ID 将不同。为了进行此检查,我们查看我们的字典中是否已包含该 ID;如果是,则获取相应的 GestureTracker 对象,并将其位置发送给它,以便它可以处理(通常是将其添加到点列表中)。如果我们还没有跟踪任何特定的 ID,那么我们将 ID 添加到字典中,并为其关联一个新的 GestureTracker 对象。这部分由 GetGestureTracker 函数处理。Stylus Down、Up 和 Move 事件如下所示:

public void Process_TouchDown(object sender, StylusEventArgs args)
{
    // get the location
    Point location = args.GetPosition(canvas);
    // get the tracker for this device from gestrue tracker map
    GestureTracker gestureTracker = GetGestureTracker(args.StylusDevice.Id);
    // process further
    gestureTracker.ProcessDown(location);
}

public void Process_TouchMove(object sender, StylusEventArgs args)
{
    // get the tracker for this device from gesture tracker map
    GestureTracker gestureTracker = GetGestureTracker(args.StylusDevice.Id);

    if (gestureTracker == null)
        return; // don't do anything

    // get the location
    Point location = args.GetPosition(canvas);
    // process further
    gestureTracker.ProcessMove(location);
}

public void Process_TouchUp(object sender, StylusEventArgs args)
{
    // get the location
    Point location = args.GetPosition(canvas);
    // get the tracker for this device from gesture tracker map
    GestureTracker gestureTracker = GetGestureTracker(args.StylusDevice.Id);

    if (gestureTracker == null)
        return;

    // process further i.e. handle the gesture
    gestureTracker.ProcessUp(location);

    // as the gesture is already handled so remove it from the gesture tracker
    gestureTrackerMap.Remove(args.StylusDevice.Id);
}

private GestureTracker GetGestureTracker(int deviceId)
{
    GestureTracker gestureTracker = null;
    // check if we are already tracking this device, if yes then return it
    // otherwise add it to the gesture tracker map and return a reference to that
    if (gestureTrackerMap.TryGetValue(deviceId, out gestureTracker))
        return gestureTracker;

    // first time for this device so add it to map
    if (gestureTracker == null)
    {
        gestureTracker = new GestureTracker(canvas);
        gestureTrackerMap.Add(deviceId, gestureTracker);
    }

    // finally return the gesture tracker
    return gestureTracker;
}

跟踪多个触摸输入

处理多个触摸输入非常容易。所有触摸事件都有通用的 StylusEventArgs,它会传递每个设备/触摸痕迹的 ID。为了处理所有这些,我们只需要跟踪所有 ID。在我们的代码中,我们使用 args.StylusDevice.Id。我们创建一个字典,其中包含所有这些唯一 ID 作为键,以及用于跟踪输入经过的所有点的相应 GestureTracker

如何识别触摸手势

GestureInterpreter 类用于识别手势。它有两个静态函数:InterpretGestureInterpretCompoundGesture;第一个是 public 函数,第二个是 private 函数。我们调用 InterpretGesture,并将来自 GestureTracker 类的点列表从 ProcessUp 函数传递过去。ProcessUp 函数的代码如下:

public void ProcessUp(Point location)
{
    // add point to the gesture's points list
    points.Add(location);

    // process and recognize the gesture
    gesture = GestureInterpreter.InterpretGesture(points);

    // find coordinates of the image for the recognized gesture
    Point imageCoordinates = FindImageCoordinatesByGesture();
    // add a boundary to the rectangle, so that it gives a highlight effect
    // pass the left and top coordinates of the image
    AddHighlightToImage(imageCoordinates.X, imageCoordinates.Y);
    
    // start a delay for 2 seconds and in the tick event
    // we remove the rectangle (border) from the image
    // which gives a un-highlighting effect
    timer.Start();
}

如果手势被识别,那么我们将通过在图像周围显示一个矩形来高亮显示手势图像,并启动一个 DispatcherTimer 在预定时间内运行。当时间到了,我们就通过从画布上移除矩形来取消高亮显示。高亮显示和取消高亮显示的 G 代码非常直接。一旦我们识别出手势,我们就会将手势名称存储在一个本地字符串变量中,并用于通过名称查找手势图像的坐标。

在创建矩形对象时,我们创建一个新的 GUID 并从中删除减号,然后在其前面加上 rect。这是为了在注册和注销矩形对象名称时保持名称的唯一性。高亮显示和取消高亮显示图像的代码如下:

// highlight the image by adding a rectangle boundary around it
private void AddHighlightToImage(double left, double top)
{
    Rectangle rectangle = new Rectangle();
    // create a random name for this rectangle
    // this is just to ensure that we don't
    // fail while adding/registering the 
    // rectangle as a children of canvas
    rectangleName = string.Format("rect{0}", 
                           GenerateNewGuidLessMinus());
    rectangle.Name = rectangleName;

    // set some generic properties
    rectangle.Width = 72;
    rectangle.Height = 72;
    rectangle.StrokeThickness = 2;
    rectangle.RadiusX = 3;
    rectangle.RadiusY = 3;

    // create a random color for this rectangle
    Random random = new Random((int)DateTime.Now.Millisecond);
    SolidColorBrush brush = new SolidColorBrush(Color.FromRgb(
                            (byte)random.Next(0, 255),
                            (byte)random.Next(0, 255),
                            (byte)random.Next(0, 255)));
    rectangle.Stroke = brush;

    // set the coordinates of the rectangle
    Canvas.SetLeft(rectangle, left);
    Canvas.SetTop(rectangle, top);
    // register the name of rectangle with canvas
    // and add it to childres list of canvas
    canvas.RegisterName(rectangleName, rectangle);
    canvas.Children.Insert(0, rectangle);
}

private void highlightTimer_Tick(object sender, EventArgs e)
{
    // remove the boundary from the image
    // or actually remove the rectangle that we added earlier from canvas 
    RemoveHighlightFromImage();
    // disable and stop the timer
    timer.Stop();
}

// remove the highlight boundary from the image
// in actual remove the rectangle that we added a few seconds ago
private void RemoveHighlightFromImage()
{
    // find the rectangle by the registered name
    Rectangle rectangle = (Rectangle)canvas.FindName(rectangleName);
    // change the color of boundary
    rectangle.Stroke = new SolidColorBrush(Colors.White);
    // remove from childres list and unregister the name
    canvas.Children.Remove(rectangle);
    canvas.UnregisterName(rectangleName);
}

手势识别背后的数学原理

好的,接下来是典型部分(实际上非常简单)。我们有两种手势:简单手势和复合手势。让我们尝试理解如何计算它们。

  • 简单手势:我们有两个方向,水平和垂直。让我们将它们分别视为 X 轴和 Y 轴。首先,我们计算第一个点和最后一个点之间的 X 和 Y 差,称为 xDiffyDiff。如果 xDiff 为零或几乎可以忽略不计(我们有一个最小阈值 10 像素),那么我们只沿着 Y 轴或垂直移动。因此,手势是向上还是向下。类似地,如果 yDiff 为零或几乎可以忽略不计,那么我们只沿着 X 轴或水平移动。因此,手势是向左还是向右。此外,如果 yDiff 为负(表示我们从较低值开始并移动到较高值),那么我们向上移动,否则我们向下移动。类似地,如果 xDiff 为负,我们向右移动,否则我们向左移动。
  • 复合手势:当我们同时在 X 轴和 Y 轴上移动时,这意味着这是一个复合手势。为了识别复合手势,我们首先识别它的移动方向(与我们为简单手势所做的相同),然后检查它相对于起点的总体移动。这样我们就可以按正确的顺序获得两个方向。还有一种方法可以做到同样的事情。我们可以计算角度,其中角度的正切通过 angle = Math.ATan2(yDiff, xDiff) 计算,然后我们可以使用 xDiffyDiff 和角度来识别手势。

未来展望

在接下来的文章中,我将尝试解释/实现:

  • .NET 4.0 中的类似功能。.NET 4.0 提供了内置的多点触控支持,支持 WPF。
  • 使用神经网络进行复杂手势识别。

其他链接和参考

© . All rights reserved.