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






4.96/5 (32投票s)
适用于 Windows 7 中 WPF 的多点触控简单和复合手势识别实现。
注意:运行此程序需要 .NET 3.5 SP1 和支持多点触控的硬件,或者使用 Multi-touch Vista 来模拟多点触控。
目录
引言
Windows 7 中新引入的功能之一是触摸 API。它允许开发者超越鼠标和键盘。现在,应用程序可以与用户进行触摸交互。触摸 API 支持丰富的预定义手势,如缩放、平移和旋转。多点触控 API 提供原始触摸数据输入和对象的高级操作,包括惯性(一种物理模型,有助于定义现实生活中的操作、属性和动作,如速度、速率、加速度、减速度和重量)。
触摸手势是一款使用 Windows 7 多点触控 API 开发的应用程序,它在 WPF 应用程序中提供简单的手势识别支持。它可以识别基本的左、右、上、下滑动手势;此外,它还支持由基本手势组成的某些复合手势,如先左后右、先左后下等。该应用程序采用了与《为 .NET Windows 应用程序添加鼠标手势支持》文章中的方法略有不同的方法。
所需软件
- Visual Studio 2008 SP1
- Windows 7 SDK
- Windows 7 多点触控 .NET 互操作示例库
- Multi-Touch Vista。如果您没有触摸屏设备,那么此模拟器可以使用鼠标模拟触摸。
应用程序结构
该应用程序包含五个类和一个枚举,它们是:
- 类
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
会对事件采取适当的操作。在进一步操作之前,我们必须订阅 StylusDown
、StylusMove
和 StylusUp
事件。此外,声明并创建一个 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
类用于识别手势。它有两个静态函数:InterpretGesture
和 InterpretCompoundGesture
;第一个是 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 差,称为
xDiff
和yDiff
。如果xDiff
为零或几乎可以忽略不计(我们有一个最小阈值 10 像素),那么我们只沿着 Y 轴或垂直移动。因此,手势是向上还是向下。类似地,如果yDiff
为零或几乎可以忽略不计,那么我们只沿着 X 轴或水平移动。因此,手势是向左还是向右。此外,如果yDiff
为负(表示我们从较低值开始并移动到较高值),那么我们向上移动,否则我们向下移动。类似地,如果xDiff
为负,我们向右移动,否则我们向左移动。 - 复合手势:当我们同时在 X 轴和 Y 轴上移动时,这意味着这是一个复合手势。为了识别复合手势,我们首先识别它的移动方向(与我们为简单手势所做的相同),然后检查它相对于起点的总体移动。这样我们就可以按正确的顺序获得两个方向。还有一种方法可以做到同样的事情。我们可以计算角度,其中角度的正切通过 angle =
Math.ATan2(yDiff, xDiff)
计算,然后我们可以使用xDiff
、yDiff
和角度来识别手势。
未来展望
在接下来的文章中,我将尝试解释/实现:
- .NET 4.0 中的类似功能。.NET 4.0 提供了内置的多点触控支持,支持 WPF。
- 使用神经网络进行复杂手势识别。