多点触摸 - 管理(动手实验)
概述
Windows 7 使用户无需使用中间设备,通过手指触摸方式就能够管理应用程序。这扩展了平板电脑基于触笔的功能。与其他指点设备不同,这种新功能支持在不同指点位置上同时发生多个输入事件,支持复杂的场景,比如通过十指或由多个并行用户管理应用程序。然而,要成功实现此功能,我们必须调整应用程序的用户界面和行为,以支持这种新的输入模式。
本次动手实验 (Hands-On Lab, HOL) 的目标是将一个基于鼠标的简单图片操作应用程序升级为支持多点触摸的现代应用程序,类似于 Microsoft Surface 行为。
图 1
启用了多点触摸的应用程序
目标
本动手实验将学习如何管理多点触摸事件,包括:
• 理解同时操作多个对象的含义
• 检查多点触摸硬件是否存在及其就绪情况
• 在 WPF 3.5SP1 中实现多点触摸事件
• 通过内置的 WPF 触笔事件使用多点触摸事件
• 使用操作 (Manipulation) 和惯性 (Inertia) 处理器
设置
为了方便起见,我们以 Visual Studio 代码片段的形式提供将在本动手实验中使用的许多代码。本实验所需的设置包括安装这些代码片段。为此:
1. 运行位于本实验的 Setup 文件夹下的 MultiTouchLab.vsi 安装程序。
2. 按照向导说明安装代码片段。
系统要求
要完成本实验,必须拥有以下工具:
• Microsoft Visual Studio 2008 SP1
• Windows 7
• Windows 7 Integration Library 示例(Windows Touch:开发人员资源)
• 一台多点触摸硬件设备
练习 1 :开发多点触摸图片处理应用程序
要理解如何管理多点触摸输入,我们首先需要理解如何处理(基于鼠标的)单点输入。为此,我们准备了一个基于鼠标的图片处理应用程序,就是多点触摸动手实验初始应用程序。
任务 1 – 了解解决方案
1. 打开位于 %TrainingKitInstallDir%\MultiTouch\Ex1-PictureHandling\Begin 下的初始解决方案,选择想要使用的语言(C# 或 VB)。
2. 编译并运行它。可以进行的操作有:通过单击挑选一张图片;按住鼠标左键并移动鼠标来拖动图片;使用鼠标滚轮缩放图片。每次选择一张图片时,该图片就会出现在最前面。在开始编码之前,首先了解一下初始应用程序。
该应用程序用于处理图片。每张图片由一个 Picture 用户控件表示。这是一个非常简单的控件,它基于 WPF。Picture 用户控件的 XAML 如下:
XAML
<UserControl x:Class="MultitouchHOL.Picture"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">
<Image Source="{Binding Path=ImagePath}" Stretch="Fill" Width="Auto"
Height="Auto" RenderTransformOrigin="0.5, 0.5">
<Image.RenderTransform>
<TransformGroup>
<RotateTransform Angle="{Binding Path=Angle}"></RotateTransform>
<ScaleTransform ScaleX="{Binding Path=ScaleX}"
ScaleY="{Binding Path=ScaleY}">
</ScaleTransform>
<TranslateTransform X="{Binding Path=X}" Y="{Binding Path=Y}"/>
</TransformGroup>
</Image.RenderTransform>
</Image>
</UserControl>
注意: 此用户控件的代码仅包括 ImagePath、Angle、ScaleX、ScaleY、X 和 Y 依赖属性的声明。ImagePath 是有效的图像文件或资源的路径。Angle 是图像的旋转角度。ScaleX 和 ScaleY 是图像的缩放系数,而 X、Y 是图像的中心位置。
3. 现在看一下 MainWindow 类。此 XAML 文件声明 MainWindow:
XAML
<Window x:Class="MultitouchHOL.MainWindow"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
Title="MultitouchHOL" Height="300" Width="300" WindowState="Maximized"
xmlns:mt="clr-namespace:MultitouchHOL">
<Canvas Name="_canvas">
</Canvas>
</Window>
注意:此窗口仅包含一个画布元素 (_canvas)。画布是包含 Picture 用户控件实例的面板。
4. 现在打开 MainWindow.xaml.cs (C#) 或 MainWindow.xaml.vb 文件 (Visual Basic)。如果用户按住鼠标左键,_picture 成员将拥有当前跟踪的图片;否则,它将拥有空值。_prevLocation 是 Mouse Move 事件报告的上一个位置,用于计算偏移量。
5. MainWindow 构造函数创建主窗口,注册各种事件处理函数。
C#
public MainWindow()
{
InitializeComponent();
//Enable stylus events and load pictures
this.Loaded += (s, e) => { LoadPictures(); };
//Register for mouse events
MouseLeftButtonDown += ProcessDown;
MouseMove += ProcessMove;
MouseLeftButtonUp += ProcessUp;
MouseWheel += ProcessMouseWheel;
}
Visual Basic
Public Sub New()
InitializeComponent()
End Sub
注意: 在 Visual Basic 中,事件处理注册在事件处理程序声明中定义,使用 Handles 关键字。
6. LoadPictures() 函数从用户的图片文件夹加载图片,并为所有图片创建一个 Picture 控件。它只在初始化画布之后才执行此操作。下面看一下 LoadPictures() 代码。
7. 现在看一下如何处理鼠标事件。
C#
private void ProcessDown(object sender, MouseButtonEventArgs args)
{
_prevLocation = args.GetPosition(_canvas);
_picture = FindPicture(_prevMouseLocation);
BringPictureToFront(_picture);
}
Visual Basic
Private Sub ProcessDown(ByVal sender As Object, ByVal args As MouseButtonEventArgs) Handles Me.MouseLeftButtonDown
_prevLocation = args.GetPosition(_canvas)
_picture = FindPicture(_prevLocation)
BringPictureToFront(_picture)
End Sub
按下鼠标左键将启动一个新的图片拖动会话。首先我们必须获得相对于画布的指针位置。我们将此信息保存在 _prevLocation 数据成员中。
8. 下一步是在该位置找到一张图片。FindPicture() 函数利用 WPF VisualTree 点击测试功能来找到最顶层的图片。如果鼠标所在位置没有图片,则返回空值。
9. BringPictureToFront() 将所选图片的 Z 轴次序设置在其他图片的最顶层。
此处理程序的处理结果是 _picture 数据成员“记住”了所选的图片,_prevLocation 获取鼠标位置的代码片段。我们看一下当鼠标移动时会发生什么情况:
C#
private void ProcessMove(object sender, MouseEventArgs args)
{
if (args.LeftButton == MouseButtonState.Released || _picture == null)
return;
Point newLocation = args.GetPosition(_canvas);
_picture.X += newLocation.X - _prevMouseLocation.X;
_picture.Y += newLocation.Y - _prevMouseLocation.Y;
_prevLocation = newLocation;
}
Visual Basic
Private Sub ProcessMove(ByVal sender As Object, ByVal args As MouseEventArgs) Handles Me.MouseMove
If args.LeftButton = MouseButtonState.Released OrElse _picture Is Nothing Then Return
Dim newLocation = args.GetPosition(_canvas)
_picture.X += newLocation.X - _prevLocation.X
_picture.Y += newLocation.Y - _prevLocation.Y
_prevLocation = newLocation
End Sub
如果用户未按下鼠标左键或者未选择任何图片,该函数将不执行任何操作。否则,该函数将计算平移量并更新图片的 X 和 Y 属性。它还将更新 _prevLocation。
10. 我们需要注意的最后一个函数是 ProcessMouseWheel:
C#
private void ProcessMouseWheel(object sender, MouseWheelEventArgs args)
{
Point location = args.GetPosition(_canvas);
Picture picture = FindPicture(location);
if (picture == null)
return;
BringPictureToFront(picture);
double scalingFactor = 1 + args.Delta / 1000.0;
picture.ScaleX *= scalingFactor;
picture.ScaleY *= scalingFactor;
}
Visual Basic
Private Sub ProcessMouseWheel(ByVal sender As Object, ByVal args As MouseWheelEventArgs) Handles Me.MouseWheel
Dim location = args.GetPosition(_canvas)
Dim picture = FindPicture(location)
If picture Is Nothing Then Return
BringPictureToFront(picture)
Dim scalingFactor = 1 + args.Delta / 1000.0
picture.ScaleX *= scalingFactor
picture.ScaleY *= scalingFactor
End Sub
此函数获取鼠标指针位置,找到该位置下的图片,将其呈现在最前面。然后它从鼠标滚轮偏移量中得到偏移系数。最后只需更新图片缩放比例。
任务 2 – 测试多点触摸硬件是否存在及其就绪情况
在本任务中,我们将开始编写多点触摸程序。尽管 WPF 3.5 不支持多点触摸(多点触摸事件和控件将包含在 WPF 4.0 中),但可以通过某种方式来在当前版本中使用多点触摸。为此,我们必须使用 Windows 7 Integration Library 示例。此集成库是一个示例,演示了如何在 .NET 代码中使用 Win32 本机 API。
注意: 可以从网址 https://code.msdn.microsoft.com/Project/Download/FileDownload.aspx?ProjectName=WindowsTouch&DownloadId=5038 获取 Windows 7 Integration Library 示例。为了简单起见,这些库在 %TrainingKitInstallDir%\MultiTouch\Assets\Win7LibSample 下以实验资源的形式提供,请选择您想要使用的语言(C# 或 VB)。
1. 添加对 Windows7.Multitouch.dll 和 Windows7.Multitouch.WPF.dll 的引用。
2. 将以下代码添加到 MainWindow 构造函数中:
(代码片段 – MultiTouch – IsMultiTouchReady CSharp)
C#
if (!Windows7.Multitouch.TouchHandler.DigitizerCapabilities.IsMultiTouchReady)
{
MessageBox.Show("Multitouch is not availible");
Environment.Exit(1);
}
(代码片段 – MultiTouch – IsMultiTouchReady VB)
Visual Basic
If Not Windows7.Multitouch.TouchHandler.DigitizerCapabilities.IsMultiTouchReady Then
MsgBox("Multitouch is not availible")
Environment.Exit(1)
End If
3. 查看 TouchHandler.DigitizerCapabilities 的其他属性。
图 2
查看 TouchHandler.DigitizerCapabilities 属性
任务 3 – **将鼠标事件替换为触摸事件 **
在本练习中,我们将删除鼠标事件并将其替换为触摸事件,以便使用我们的手指处理图片。
1. 将以下代码行添加到 MainWindow.xaml.cs 文件 (C#) 或 MainWindow.xaml.vb 文件 (Visual Basic) 开头:
C#
using Windows7.Multitouch;
using Windows7.Multitouch.WPF;
Visual Basic
Imports Windows7.Multitouch
Imports Windows7.Multitouch.WPF
2. 我们想要在 WPF 3.5 SP1 中实现多点触摸事件。为此,必须告诉系统以触笔事件的形式发出触摸事件。Windows 7 Integration Library 的 WPF Factory 类拥有一个函数来实现此功能,那就是 EnableStylusEvent。在 MainWindow Loaded 事件处理程序中添加对此函数的调用:
C#
public MainWindow()
{
...
//Enable stylus events and load pictures
this.Loaded += (s, e) => { Factory.EnableStylusEvents(this); LoadPictures(); };
...
Visual Basic
Private Sub Window_OnLoaded() Handles Me.Loaded
Factory.EnableStylusEvents(Me)
LoadPictures()
End Sub
3. 删除 ProcessMouseWheel 事件处理程序及相应的事件注册(我们将在稍后处理缩放)。
4. (仅适用于 C# 用户)删除 MouseLeftButtonDown、MouseMove 和 MouseLeftButtonUp 的事件注册代码。MainWindow 构造函数应该类似于以下代码:
C#
public MainWindow()
{
InitializeComponent();
if (!Windows7.Multitouch.TouchHandler.DigitizerCapabilities.IsMultiTouchReady)
{
MessageBox.Show("Multitouch is not availible");
Environment.Exit(1);
}
this.Loaded += (s, e) => { Factory.EnableStylusEvents(this); LoadPictures(); };
}
5. 更改以下事件处理程序的签名和代码:
注意:此事件处理程序的签名已经更改。我们使用StylusEventArgs 代替与鼠标相关的事件参数。
(代码片段 – MultiTouch – StylusEventHandlers CSharp)
C#
public void ProcessDown(object sender, StylusEventArgs args)
{
_prevLocation = args.GetPosition(_canvas);
_picture = FindPicture(_prevLocation);
BringPictureToFront(_picture);
}
public void ProcessMove(object sender, StylusEventArgs args)
{
if (_picture == null)
return;
Point newLocation = args.GetPosition(_canvas);
_picture.X += newLocation.X - _prevLocation.X;
_picture.Y += newLocation.Y - _prevLocation.Y;
_prevLocation = newLocation;
}
public void ProcessUp(object sender, StylusEventArgs args)
{
_picture = null;
}
(代码片段 – MultiTouch – StylusEventHandlers VB)
Visual Basic
Public Sub ProcessDown(ByVal sender As Object, ByVal args As StylusEventArgs)
_prevLocation = args.GetPosition(_canvas)
_picture = FindPicture(_prevLocation)
BringPictureToFront(_picture)
End Sub
Public Sub ProcessMove(ByVal sender As Object, ByVal args As StylusEventArgs)
If _picture Is Nothing Then Return
Dim newLocation = args.GetPosition(_canvas)
_picture.X += newLocation.X - _prevLocation.X
_picture.Y += newLocation.Y - _prevLocation.Y
_prevLocation = newLocation
End Sub
Public Sub ProcessUp(ByVal sender As Object, ByVal args As StylusEventArgs)
_picture = Nothing
End Sub
6. 注册触笔事件。
C#
public MainWindow()
{
...
//Register for stylus (touch) events
StylusDown += ProcessDown;
StylusUp += ProcessUp;
StylusMove += ProcessMove;
}
Visual Basic
Public Sub ProcessDown(ByVal sender As Object, ByVal args As StylusEventArgs) Handles Me.StylusDown
...
End Sub
Public Sub ProcessMove(ByVal sender As Object, ByVal args As StylusEventArgs) Handles Me.StylusMove
...
End Sub
Public Sub ProcessUp(ByVal sender As Object, ByVal args As StylusEventArgs) Handles Me.StylusUp
...
End Sub
7. 编译并运行。使用手指代替鼠标!
注意: 如果尝试使用多个手指会发生什么情况?为什么?
任务 4 – 同时处理多张图片
在本任务中,我们将添加多点触摸支持。触摸屏幕的每个手指都会获得一个唯一的触摸 ID。只要这根手指继续触摸屏幕,系统就会将相同的触摸 ID 与该手指关联。当手指离开屏幕表面时,该触摸 ID 将被系统释放并可被硬件再次使用。在我们的示例中,当一根手指触摸图片时,应该将该手指的唯一触摸 ID 与该图片关联,直到该手指离开屏幕。如果两个或更多手指同时触摸屏幕,那么每个手指都可以操作相关的图片。
当使用 Stylus 事件作为触摸事件时,可以从 Stylus 事件参数中提取出触摸 ID:
C# | Visual Basic
args.StylusDevice.Id
WPF 将使用相关的 StylusDevice.Id(触摸 ID)不断为每个触摸屏幕的手指触发事件。
1. 我们需要同时跟踪多张图片。对于每张图片,触摸 ID、上一个位置与图片用户控件之间必须保持关联。我们将首先添加一个新的 PictureTracker 类:
注意:PictureTracker 类也在 %TrainingKitInstallDir%\MultiTouch\Assets\PictureHandling下以实验资源的形式提供,请选择您想要使用的语言(C# 或 VB)。
(代码片段 – MultiTouch – PictureTrackerClass CSharp)
C#
/// <summary>
/// Track a single picture
/// </summary>
class PictureTracker
{
private Point _prevLocation;
public Picture Picture { get; set; }
public void ProcessDown(Point location)
{
_prevLocation = location;
}
public void ProcessMove(Point location)
{
Picture.X += location.X - _prevLocation.X;
Picture.Y += location.Y - _prevLocation.Y;
_prevLocation = location;
}
public void ProcessUp(Point location)
{
//Do Nothing, We might have another touch-id that is
//still down
}
}
(代码片段 – MultiTouch – PictureTrackerClass VB)
Visual Basic
''' <summary>
''' Track a single picture.
''' </summary>
Imports System.Windows
Class PictureTracker
Private _prevLocation As Point
Private _picture As Picture
Public Property Picture() As Picture
Get
Return _picture
End Get
Set(ByVal value As Picture)
_picture = value
End Set
End Property
Public Sub ProcessDown(ByVal location As Point)
_prevLocation = location
End Sub
Public Sub ProcessMove(ByVal location As Point)
Picture.X += location.X - _prevLocation.X
Picture.Y += location.Y - _prevLocation.Y
_prevLocation = location
End Sub
Public Sub ProcessUp(ByVal location As Point)
' Do Nothing, We might have another touch-id that is.
' Still down.
End Sub
End Class
2. 现在我们需要一个词典,以将活动的触摸 ID 映射到相应的 PictureTracker 实例。我们将创建一个 PictureTrackerManager 类来包含该词典并处理各种触摸事件。无论何时触发了触摸事件,PictureTrackerManager 都将尝试找到关联的 PictureTracker 实例并要求它处理该触摸事件。换言之,PictureTrackerManager 将获得触摸事件。它寻找作为实际事件目标的 PictureTracker 实例并将触摸事件分派给它。现在的问题是如何找到正确的 PictureTracker 实例。我们需要考虑一些不同的场景:
a. 发生 ProcessDown 事件时,有 3 种选择:
i. 手指触摸一个空位置。不会发生任何事件。
ii. 手指触摸新图片。必须创建一个新 PictureTracker 实例,必须在触摸 ID 映射中创建一个新条目。
iii. 第 2 个(或更多)手指触摸已经被跟踪的图片。我们必须将新的触摸 ID 与相同的 PictureTracker 实例相关联。
b. 发生 ProcessMove 事件时,有 2 种选择:
i. 手指的触摸 ID 未与一个 PictureTracker 相关联。不应该发生任何事件。
ii. 手指的触摸 ID 与一个 PictureTracker 关联。我们需要将事件转发给它。
c. 发生 ProcessUp 事件时,有 2 种选择:
i. 删除了一个手指触摸 ID,但是至少还存在一个相关的触摸 ID。我们需要从映射中删除此条目。
ii. 删除了最后一个相关的触摸 ID。我们需要从映射中删除该条目。图片跟踪器不再使用并且会被当作垃圾收集走。
3. 通过分析这些情形,我们可以定义 PictureTrackerManager 的设计条件:
a. 它必须拥有一个映射:触摸 ID PictureTracker
C#
private readonly Dictionary<int, PictureTracker> _pictureTrackerMap
Visual Basic
Private ReadOnly _pictureTrackerMap As Dictionary(Of Integer, PictureTracker)
b. 它必须使用 VisualTree 点击测试或通过在映射中查找来找到 PictureTracker
c. 它必须将事件转发给正确的 PictureTracker
4. 添加以下 PictureTrackerManager 类:
注意:PictureTrackerManager 类也以实验资产的形式在 %TrainingKitInstallDir%\MultiTouch\Assets\PictureHandling 下提供,请选择您想要使用的语言(C# 或 VB)。
(代码片段 – MultiTouch – PictureTrackerManagerClass CSharp)
C#
class PictureTrackerManager
{
//Map between touch ids and picture trackers
private readonly Dictionary<int, PictureTracker> _pictureTrackerMap = new Dictionary<int, PictureTracker>();
private readonly Canvas _canvas;
public PictureTrackerManager(Canvas canvas)
{
_canvas = canvas;
}
public void ProcessDown(object sender, StylusEventArgs args)
{
Point location = args.GetPosition(_canvas);
PictureTracker pictureTracker = GetPictureTracker(args.StylusDevice.Id, location);
if (pictureTracker == null)
return;
pictureTracker.ProcessDown(location);
}
public void ProcessUp(object sender, StylusEventArgs args)
{
Point location = args.GetPosition(_canvas);
PictureTracker pictureTracker = GetPictureTracker(args.StylusDevice.Id);
if (pictureTracker == null)
return;
pictureTracker.ProcessUp(location);
_pictureTrackerMap.Remove(args.StylusDevice.Id);
}
public void ProcessMove(object sender, StylusEventArgs args)
{
PictureTracker pictureTracker = GetPictureTracker(args.StylusDevice.Id);
if (pictureTracker == null)
return;
Point location = args.GetPosition(_canvas);
pictureTracker.ProcessMove(location);
}
private PictureTracker GetPictureTracker(int touchId)
{
PictureTracker pictureTracker = null;
_pictureTrackerMap.TryGetValue(touchId, out pictureTracker);
return pictureTracker;
}
private PictureTracker GetPictureTracker(int touchId, Point location)
{
PictureTracker pictureTracker;
//See if we already track the picture with the touchId
if (_pictureTrackerMap.TryGetValue(touchId, out pictureTracker))
return pictureTracker;
//Get the picture under the touch location
Picture picture = FindPicture(location);
if (picture == null)
return null;
//See if we track the picture with other ID
pictureTracker = (from KeyValuePair<int, PictureTracker> entry in _pictureTrackerMap
where entry.Value.Picture == picture
select entry.Value).FirstOrDefault();
//First time
if (pictureTracker == null)
{
//create new
pictureTracker = new PictureTracker();
pictureTracker.Picture = picture;
BringPictureToFront(picture);
}
//remember the corelation between the touch id and the picture
_pictureTrackerMap[touchId] = pictureTracker;
return pictureTracker;
}
/// <summary>
/// Find the picture in the touch location
/// </summary>
/// <param name="pointF">touch location</param>
/// <returns>The picture or null if no picture exists in the touch
/// location</returns>
private Picture FindPicture(Point location)
{
HitTestResult result = VisualTreeHelper.HitTest(_canvas, location);
if (result == null)
return null;
Image image = result.VisualHit as Image;
if (image == null)
return null;
return image.Parent as Picture;
}
private void BringPictureToFront(Picture picture)
{
if (picture == null)
return;
var children = (from UIElement child in _canvas.Children
where child != picture
orderby Canvas.GetZIndex(child)
select child).ToArray();
for (int i = 0; i < children.Length; ++i)
{
Canvas.SetZIndex(children[i], i);
}
Canvas.SetZIndex(picture, children.Length);
}
}
(代码片段 – MultiTouch – PictureTrackerManagerClass VB)
Visual Basic
Imports System.Windows
Imports System.Windows.Controls
Class PictureTrackerManager
' Map between touch ids and picture trackers
Private ReadOnly _pictureTrackerMap As New Dictionary(Of Integer, PictureTracker)
Private ReadOnly _canvas As Canvas
Public Sub New(ByVal canvas As Canvas)
_canvas = canvas
End Sub
Public Sub ProcessDown(ByVal sender As Object, ByVal args As StylusEventArgs)
Dim location = args.GetPosition(_canvas)
Dim pictureTracker = GetPictureTracker(args.StylusDevice.Id, location)
If pictureTracker Is Nothing Then Return
pictureTracker.ProcessDown(location)
End Sub
Public Sub ProcessUp(ByVal sender As Object, ByVal args As StylusEventArgs)
Dim location = args.GetPosition(_canvas)
Dim pictureTracker = GetPictureTracker(args.StylusDevice.Id)
If pictureTracker Is Nothing Then Return
pictureTracker.ProcessUp(location)
_pictureTrackerMap.Remove(args.StylusDevice.Id)
End Sub
Public Sub ProcessMove(ByVal sender As Object, ByVal args As StylusEventArgs)
Dim pictureTracker = GetPictureTracker(args.StylusDevice.Id)
If pictureTracker Is Nothing Then Return
Dim location = args.GetPosition(_canvas)
pictureTracker.ProcessMove(location)
End Sub
Private Function GetPictureTracker(ByVal touchId As Integer) As PictureTracker
Dim pictureTracker As PictureTracker = Nothing
_pictureTrackerMap.TryGetValue(touchId, pictureTracker)
Return pictureTracker
End Function
Private Function GetPictureTracker(ByVal touchId As Integer, ByVal location As Point) As PictureTracker
Dim pictureTracker As PictureTracker = Nothing
' See if we already track the picture with the touchId
If _pictureTrackerMap.TryGetValue(touchId, pictureTracker) Then Return pictureTracker
' Get the picture under the touch location
Dim picture = FindPicture(location)
If picture Is Nothing Then Return Nothing
' See if we track the picture with other ID
pictureTracker = (From entry In _pictureTrackerMap _
Where entry.Value.Picture Is picture _
Select entry.Value).FirstOrDefault()
' First time
If pictureTracker Is Nothing Then
' Create new
pictureTracker = New PictureTracker()
pictureTracker.Picture = picture
BringPictureToFront(picture)
End If
' Remember the corelation between the touch id and the picture
_pictureTrackerMap(touchId) = pictureTracker
Return pictureTracker
End Function
''' <summary>
''' Find the picture in the touch location
''' </summary>
''' <param name="pointF">touch location</param>
''' <returns>The picture or null if no picture exists in the touch
''' location</returns>
Private Function FindPicture(ByVal location As Point) As Picture
Dim result = VisualTreeHelper.HitTest(_canvas, location)
If result Is Nothing Then Return Nothing
Dim image = TryCast(result.VisualHit, Image)
If image Is Nothing Then Return Nothing
Return TryCast(image.Parent, Picture)
End Function
Private Sub BringPictureToFront(ByVal picture As Picture)
If picture Is Nothing Then Return
Dim children = (From child In _canvas.Children _
Where child IsNot picture _
Order By Canvas.GetZIndex(child) _
Select child).ToArray()
For i = 0 To children.Length - 1
Canvas.SetZIndex(children(i), i)
Next i
Canvas.SetZIndex(picture, children.Length)
End Sub
End Class
5. 将以下字段声明添加到 MainWindow 类的开头:
C#
private readonly PictureTrackerManager _pictureTrackerManager;
Visual Basic
Private ReadOnly _pictureTrackerManager As PictureTrackerManager
6. 修改 MainWindow 构造函数:
a. 在调用 InitializeComponent() 之后,添加管理器初始化:
C#
_pictureTrackerManager = new PictureTrackerManager(_canvas);
Visual Basic
_pictureTrackerManager = New PictureTrackerManager(_canvas)
b. 更改触笔事件注册代码
(代码片段 – MultiTouch – PictureTrackerManagerEventHandlers CSharp)
C#
//Register for stylus (touch) events
StylusDown += _pictureTrackerManager.ProcessDown;
StylusUp += _pictureTrackerManager.ProcessUp;
StylusMove += _pictureTrackerManager.ProcessMove;
(代码片段 – MultiTouch – PictureTrackerManagerEventHandlers VB)
Visual Basic
' Register for stylus (touch) events
AddHandler Me.StylusDown, AddressOf _pictureTrackerManager.ProcessDown
AddHandler Me.StylusMove, AddressOf _pictureTrackerManager.ProcessMove
AddHandler Me.StylusUp, AddressOf _pictureTrackerManager.ProcessUp
7. 从 MainWindow 类删除 ProcessDown、ProcessMove 和 ProcessUp 事件处理程序。这里将不再需要它们,因为它们现在已包含在 PictureTrackerManager 类中。
8. 编译并运行。尝试同时抓取多张图片。尝试使用多个手指抓取一张图片。发生了什么情况?为什么?
任务 5 – 使用多点触摸操作处理图片
到目前为止,使用触摸事件处理图片与使用鼠标功能并没有太大区别。在本任务中,我们将:
• 添加使用多个手指操作图片的能力
• 同时平移、缩放和旋转图片
• 同时操作多张图片
我们已经知道如何将正确的事件分派给相应的 PictureTracker,但我们还不知道如何决定在发生多个事件之后需要采取的操作。这正是 Windows 7 多点触摸机制的用武之地。它拥有一个操作处理器来使用触摸 ID 事件并生成合适的操作事件。您只需实例化一个操作处理器,注册其事件,并为它提供触摸 ID + 位置事件对。
操作处理器是一个 COM 对象。要在 .NET 中使用它,可以使用 Windows 7 Integration Library 示例。ManipulationProcessor .NET 包装器类构造函数获得一个枚举值,该值告诉它要报告哪些操作。在我们的示例中,我们希望报告所有操作。该处理器有 3 个事件:ManipulationStarted、ManipulationCompleted 和 ManipulationDelta。ManipulationDelta 是我们所关注的事件。它提供了平移、旋转和缩放的偏移量。
1. 更改整个 PictureTracker 类。
(代码片段 – MultiTouch – PictureTrackerManipulationProcessorClass CSharp)
C#
class PictureTracker
{
private readonly ManipulationProcessor _processor =
new ManipulationProcessor(ProcessorManipulations.ALL);
public PictureTracker()
{
_processor.ManipulationStarted += (s, e) =>
{
System.Diagnostics.Trace.WriteLine("Manipulation has started: " + Picture.ImagePath);
};
_processor.ManipulationCompleted += (s, e) =>
{
System.Diagnostics.Trace.WriteLine("Manipulation has completed: " + Picture.ImagePath);
};
_processor.ManipulationDelta += ProcessManipulationDelta;
}
public Picture Picture { get; set; }
public void ProcessDown(int id, Point location)
{
_processor.ProcessDown((uint)id, location.ToDrawingPointF());
}
public void ProcessMove(int id, Point location)
{
_processor.ProcessMove((uint)id, location.ToDrawingPointF());
}
public void ProcessUp(int id, Point location)
{
_processor.ProcessUp((uint)id, location.ToDrawingPointF());
}
//Update picture state
private void ProcessManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
if (Picture == null)
return;
Picture.X += e.TranslationDelta.Width;
Picture.Y += e.TranslationDelta.Height;
Picture.Angle += e.RotationDelta * 180 / Math.PI;
Picture.ScaleX *= e.ScaleDelta;
Picture.ScaleY *= e.ScaleDelta;
}
}
(代码片段 – MultiTouch – PictureTrackerManipulationProcessorClass VB)
Visual Basic
Class PictureTracker
Private _picture As Picture
Public Property Picture() As Picture
Get
Return _picture
End Get
Set(ByVal value As Picture)
_picture = value
End Set
End Property
Private WithEvents _processor As New ManipulationProcessor(ProcessorManipulations.ALL)
Public Sub New()
End Sub
Private Sub Processor_OnManipulationStarted() Handles _processor.ManipulationStarted
System.Diagnostics.Trace.WriteLine("Manipulation has started: " & Picture.ImagePath)
End Sub
Private Sub Processor_OnManipulationCompleted() Handles _processor.ManipulationCompleted
System.Diagnostics.Trace.WriteLine("Manipulation has completed: " & Picture.ImagePath)
End Sub
' Update picture state
Private Sub ProcessManipulationDelta(ByVal sender As Object, ByVal e As ManipulationDeltaEventArgs) Handles _processor.ManipulationDelta
If Picture Is Nothing Then Return
Picture.X += e.TranslationDelta.Width
Picture.Y += e.TranslationDelta.Height
Picture.Angle += e.RotationDelta * 180 / Math.PI
Picture.ScaleX *= e.ScaleDelta
Picture.ScaleY *= e.ScaleDelta
End Sub
Public Sub ProcessDown(ByVal id As Integer, ByVal location As Point)
_processor.ProcessDown(CUInt(id), location.ToDrawingPointF())
End Sub
Public Sub ProcessMove(ByVal id As Integer, ByVal location As Point)
_processor.ProcessMove(CUInt(id), location.ToDrawingPointF())
End Sub
Public Sub ProcessUp(ByVal id As Integer, ByVal location As Point)
_processor.ProcessUp(CUInt(id), location.ToDrawingPointF())
End Sub
End Class
2. 将以下命名空间指令添加到 PictureTracker 类中:
C#
using Windows7.Multitouch.Manipulation;
using Windows7.Multitouch.WPF;
Visual Basic
Imports Windows7.Multitouch.Manipulation
Imports Windows7.Multitouch.WPF
注意: 通过添加此命名空间,可以使用 ManipulatorProcessor 类和 System.Windows.Point 扩展方法 ToDrawingPointF。
3. 我们实例化了一个新的 ManipulationProcessor,注册了事件处理器,而且最重要的是,通过更新图片用户控件处理了 ManipulationDelta 事件。现在我们需要对 PictureTrackerManager 事件处理代码稍作修改,并转发触摸 ID 和触摸位置。ManipulationProcessor 需要将触摸 ID 作为操作流程的输入。更改 PictureTrackerManager 中的以下代码:
C#
public void ProcessDown(object sender, StylusEventArgs args)
{
Point location = args.GetPosition(_canvas);
PictureTracker pictureTracker = GetPictureTracker(args.StylusDevice.Id, location);
if (pictureTracker == null)
return;
pictureTracker.ProcessDown(args.StylusDevice.Id, location);
}
public void ProcessUp(object sender, StylusEventArgs args)
{
Point location = args.GetPosition(_canvas);
PictureTracker pictureTracker = GetPictureTracker(args.StylusDevice.Id);
if (pictureTracker == null)
return;
pictureTracker.ProcessUp(args.StylusDevice.Id, location);
_pictureTrackerMap.Remove(args.StylusDevice.Id);
}
public void ProcessMove(object sender, StylusEventArgs args)
{
PictureTracker pictureTracker = GetPictureTracker(args.StylusDevice.Id);
if (pictureTracker == null)
return;
Point location = args.GetPosition(_canvas);
pictureTracker.ProcessMove(args.StylusDevice.Id, location);
}
Visual Basic
Public Sub ProcessDown(ByVal sender As Object, ByVal args As StylusEventArgs)
Dim location = args.GetPosition(_canvas)
Dim pictureTracker = GetPictureTracker(args.StylusDevice.Id, location)
If pictureTracker Is Nothing Then Return
pictureTracker.ProcessDown(args.StylusDevice.Id, location)
End Sub
Public Sub ProcessUp(ByVal sender As Object, ByVal args As StylusEventArgs)
Dim location = args.GetPosition(_canvas)
Dim pictureTracker = GetPictureTracker(args.StylusDevice.Id)
If pictureTracker Is Nothing Then Return
pictureTracker.ProcessUp(args.StylusDevice.Id, location)
_pictureTrackerMap.Remove(args.StylusDevice.Id)
End Sub
Public Sub ProcessMove(ByVal sender As Object, ByVal args As StylusEventArgs)
Dim pictureTracker = GetPictureTracker(args.StylusDevice.Id)
If pictureTracker Is Nothing Then Return
Dim location = args.GetPosition(_canvas)
pictureTracker.ProcessMove(args.StylusDevice.Id, location)
End Sub
4. 编译并运行代码。尝试同时操作多张图片。
任务 6 – 添加 PictureTracker 缓存
当用户首次触摸一张图片时,应用程序创建一个新 PictureTracker 实例,该实例然后创建 ManipulationProcessor COM 对象。只要用户移开触摸该图片的最后一个指头(触摸 ID),PictureTracker 实例就会被当作垃圾收集,进而释放底层 COM 对象。分析常见的应用程序使用情形就会发现,只有少数图片可能被同时操作。据此可以得出结论:我们需要 PictureTracker 实例的一个缓存。该缓存将包含空闲的 PictureTracker 实例。当(发生 ProcessDown 事件时)需要新 PictureTracker 实例时,我们将首先尝试从缓存拉取实例,只有当缓存为空时才生成新实例。当完成对图片的操作时,我们将 PictureTracker 实例移入缓存。因为 ManipulationCompleted 是一个 ManipulationProcessor 事件,所以我们将要求 PictureTracker 处理该事件并将其转发给 PictureTrackerManager。这需要一个从 PictureTracker 到它的 PictureTrackerManager 的新引用(我们使用构造函数来传递该引用)。
1. 将堆栈数据成员添加到 PictureTrackerManager 类的开头:
C#
class PictureTrackerManager
{
//Cache for re-use of picture trackers
private readonly Stack<PictureTracker> _pictureTrackers = new Stack<PictureTracker>();
...
Visual Basic
Class PictureTrackerManager
' Cache for re-use of picture trackers
Private ReadOnly _pictureTrackers As New Stack(Of PictureTracker)()
...
2. 更改 GetPictureTracker() 函数。我们需要使用缓存,还需要将此引用传递给 PictureTracker 构造函数:
C#
private PictureTracker GetPictureTracker(int touchId, Point location)
{
...
//First time
if (pictureTracker == null)
{
//take from stack
if (_pictureTrackers.Count > 0)
pictureTracker = _pictureTrackers.Pop();
else //create new
pictureTracker = new PictureTracker(this);
pictureTracker.Picture = picture;
BringPictureToFront(picture);
}
...
}
Visual Basic
Private Function GetPictureTracker(ByVal touchId As Integer, ByVal location As Point) As PictureTracker
...
' First time
If pictureTracker Is Nothing Then
' take from stack
If _pictureTrackers.Count > 0 Then
pictureTracker = _pictureTrackers.Pop()
Else ' create new
pictureTracker = New PictureTracker(Me)
End If
pictureTracker.Picture = picture
BringPictureToFront(picture)
End If
...
End Function
3. 添加一个逻辑,以在操作完成时将 PictureTracker 实例推回堆栈中。将以下代码粘贴到 PictureTrackerManager 类中。
C#
//Manipulation is completed, we can reuse the object
public void Completed(PictureTracker pictureTracker)
{
pictureTracker.Picture = null;
_pictureTrackers.Push(pictureTracker);
}
Visual Basic
' Manipulation is completed, we can reuse the object
Public Sub Completed(ByVal pictureTracker As PictureTracker)
pictureTracker.Picture = Nothing
_pictureTrackers.Push(pictureTracker)
End Sub
4. 现在需要更改 PictureTracker 类,使其适应 PictureTrackerManager 中的代码更改。
a. 将 PictureTrackerManager 实例获取到构造函数中,然后存储它。
C#
class PictureTracker
{
private readonly ManipulationProcessor _processor =
new ManipulationProcessor(ProcessorManipulations.ALL);
private readonly PictureTrackerManager _pictureTrackerManager;
public PictureTracker(PictureTrackerManager pictureTrackerManager)
{
_pictureTrackerManager = pictureTrackerManager;
...
Visual Basic
Class PictureTracker
...
Private ReadOnly _pictureTrackerManager As PictureTrackerManager
Private WithEvents _processor As New ManipulationProcessor(ProcessorManipulations.ALL)
Public Sub New(ByVal pictureTrackerManager As PictureTrackerManager)
_pictureTrackerManager = pictureTrackerManager
End Sub
...
b. 在 ManipulationCompleted 事件中调用 PictureTrackerManager.Completed 函数:
C#
public PictureTracker(PictureTrackerManager pictureTrackerManager)
{
_pictureTrackerManager = pictureTrackerManager;
_processor.ManipulationCompleted += (s, e) =>
{
System.Diagnostics.Trace.WriteLine("Manipulation has completed: " + Picture.ImagePath);
_pictureTrackerManager.Completed(this);
};
...
Visual Basic
Private Sub Processor_OnManipulationCompleted() Handles _processor.ManipulationCompleted
System.Diagnostics.Trace.WriteLine("Manipulation has completed: " & Picture.ImagePath)
_pictureTrackerManager.Completed(Me)
End Sub
5. 编译并运行!
任务 7 – 添加惯性
只剩最后一项任务了。使用缩放、平移和旋转操作可以提供一种自然的用户体验。在实际生活中,当推动一个物体,然后松开手时,该物体会继续移动,直到因为无法克服摩擦力而停止。可以使用 Inertia 让我们的图片对象拥有相同的行为。Windows 7 多点触摸子系统提供了一个 InertiaProcessor COM 对象。InertiaProcessor 可以发起与 ManipulationProcessor 相同的操作事件。Windows 7 Integration Library 示例提供了一个包装器,它将操作处理器和惯性处理器捆绑在一起。ManipulationInertiaProcessor 可以替代 ManipulationProcessor 并提供额外的 InertiaProcessor 属性来公开 InertiaProcessor 功能。要发起更多事件,ManipulationInertiaProcessor 需要一个计时器。为了克服线程的 UI 相似性问题,我们最好拥有一个基于 GUI 的计时器。Windows 7 Integration Library 可以为我们创建这样的计时器。
当用户的最后一个手指离开图片对象时,ManipulationInertiaProcessor 会发起 OnBeforeInertia 事件。在这里设置 Inertia 开始参数。可以选择一个默认的开始速度,或者跟踪当前的对象速度并从中提取出速度数字。
1. 我们想要跟踪对象的平移、旋转和缩放速度。将以下类添加到 PictureTracker 类中:
(代码片段 – MultiTouch – InertiaParamClass CSharp)
C#
//Keep track of object velocities
private class InertiaParam
{
public VectorF InitialVelocity { get; set; }
public float InitialAngularVelocity { get; set; }
public float InitialExpansionVelocity { get; set; }
public System.Diagnostics.Stopwatch _stopwatch = new System.Diagnostics.Stopwatch();
public void Reset()
{
InitialVelocity = new VectorF(0, 0);
InitialAngularVelocity = 0;
InitialExpansionVelocity = 0;
_stopwatch.Reset();
_stopwatch.Start();
}
public void Stop()
{
_stopwatch.Stop();
}
//update velocities, velocity = distance/time
public void Update(ManipulationDeltaEventArgs e, float history)
{
float elappsedMS = (float)_stopwatch.ElapsedMilliseconds;
if (elappsedMS == 0)
elappsedMS = 1;
InitialVelocity = InitialVelocity * history + ((VectorF)e.TranslationDelta * (1F - history)) / elappsedMS;
InitialAngularVelocity = InitialAngularVelocity * history + (e.RotationDelta * (1F - history)) / elappsedMS;
InitialExpansionVelocity = InitialExpansionVelocity * history + (e.ExpansionDelta * (1F - history)) / elappsedMS;
_stopwatch.Reset();
_stopwatch.Start();
}
}
(代码片段 – MultiTouch – InertiaParamClass VB)
Visual Basic
' Keep track of object velocities.
Private Class InertiaParam
Private _initialVelocity As VectorF
Public Property InitialVelocity() As VectorF
Get
Return _initialVelocity
End Get
Set(ByVal value As VectorF)
_initialVelocity = value
End Set
End Property
Private _initialAngularVelocity As Single
Public Property InitialAngularVelocity() As Single
Get
Return _initialAngularVelocity
End Get
Set(ByVal value As Single)
_initialAngularVelocity = value
End Set
End Property
Private _initialExpansionVelocity As Single
Public Property InitialExpansionVelocity() As Single
Get
Return _initialExpansionVelocity
End Get
Set(ByVal value As Single)
_initialExpansionVelocity = value
End Set
End Property
Public _stopwatch As New System.Diagnostics.Stopwatch()
Public Sub Reset()
InitialVelocity = New VectorF(0, 0)
InitialAngularVelocity = 0
InitialExpansionVelocity = 0
_stopwatch.Reset()
_stopwatch.Start()
End Sub
Public Sub [Stop]()
_stopwatch.Stop()
End Sub
'update velocities, velocity = distance/time
Public Sub Update(ByVal e As ManipulationDeltaEventArgs, ByVal history As Single)
Dim elappsedMS = CSng(_stopwatch.ElapsedMilliseconds)
If elappsedMS = 0 Then elappsedMS = 1
InitialVelocity = InitialVelocity * history + (CType(e.TranslationDelta, VectorF) * (1.0F - history)) / elappsedMS
InitialAngularVelocity = InitialAngularVelocity * history + (e.RotationDelta * (1.0F - history)) / elappsedMS
InitialExpansionVelocity = InitialExpansionVelocity * history + (e.ExpansionDelta * (1.0F - history)) / elappsedMS
_stopwatch.Reset()
_stopwatch.Start()
End Sub
End Class
2. 将 OnBeforeInertia() 事件处理程序添加到 PictureTracker 类中:
(代码片段 – MultiTouch – OnBeforeInertia CSharp)
C#
//Fingers removed, start inertia
void OnBeforeInertia(object sender, BeforeInertiaEventArgs e)
{
//Tell the tracker manager that the user removed the fingers
_pictureTrackerManager.InInertia(this);
_processor.InertiaProcessor.InertiaTimerInterval = 15;
_processor.InertiaProcessor.MaxInertiaSteps = 500;
_processor.InertiaProcessor.InitialVelocity = _inertiaParam.InitialVelocity;
_processor.InertiaProcessor.DesiredDisplacement = _inertiaParam.InitialVelocity.Magnitude * 250;
_processor.InertiaProcessor.InitialAngularVelocity = _inertiaParam.InitialAngularVelocity * 20F / (float)Math.PI;
_processor.InertiaProcessor.DesiredRotation = Math.Abs(_inertiaParam.InitialAngularVelocity *
_processor.InertiaProcessor.InertiaTimerInterval * 540F / (float)Math.PI);
_processor.InertiaProcessor.InitialExpansionVelocity = _inertiaParam.InitialExpansionVelocity * 15;
_processor.InertiaProcessor.DesiredExpansion = Math.Abs(_inertiaParam.InitialExpansionVelocity * 4F);
}
(代码片段 – MultiTouch – OnBeforeInertia VB)
Visual Basic
' Fingers removed, start inertia
Private Sub OnBeforeInertia(ByVal sender As Object, ByVal e As BeforeInertiaEventArgs)
'Tell the tracker manager that the user removed the fingers
_pictureTrackerManager.InInertia(Me)
_processor.InertiaProcessor.InertiaTimerInterval = 15
_processor.InertiaProcessor.MaxInertiaSteps = 500
_processor.InertiaProcessor.InitialVelocity = _inertiaParam.InitialVelocity
_processor.InertiaProcessor.DesiredDisplacement = _inertiaParam.InitialVelocity.Magnitude * 250
_processor.InertiaProcessor.InitialAngularVelocity = _inertiaParam.InitialAngularVelocity * 20.0F / CSng(Math.PI)
_processor.InertiaProcessor.DesiredRotation = Math.Abs(_inertiaParam.InitialAngularVelocity * _processor.InertiaProcessor.InertiaTimerInterval * 540.0F / CSng(Math.PI))
_processor.InertiaProcessor.InitialExpansionVelocity = _inertiaParam.InitialExpansionVelocity * 15
_processor.InertiaProcessor.DesiredExpansion = Math.Abs(_inertiaParam.InitialExpansionVelocity * 4.0F)
End Sub
3. 更改 PictureTracker 类,创建 ManipulationInertiaProcessor 并注册 OnBeforeInertia 事件:
C#
/// <summary>
/// Track a single picture
/// </summary>
class PictureTracker
{
...
//Calculate the Inertia start velocity
private readonly InertiaParam _inertiaParam = new InertiaParam();
private readonly ManipulationInertiaProcessor _processor = new ManipulationInertiaProcessor(ProcessorManipulations.ALL, Factory.CreateTimer());
public PictureTracker(PictureTrackerManager pictureTrackerManager)
{
_pictureTrackerManager = pictureTrackerManager;
//Start inertia velocity calculations
_processor.ManipulationStarted += (s, e) =>
{
_inertiaParam.Reset();
};
//All completed, inform the tracker manager that the current tracker
//can be reused
_processor.ManipulationCompleted += (s, e) =>
{
_inertiaParam.Stop();
pictureTrackerManager.Completed(this);
};
_processor.ManipulationDelta += ProcessManipulationDelta;
_processor.BeforeInertia += OnBeforeInertia;
}
...
Visual Basic
Class PictureTracker
...
Private ReadOnly _pictureTrackerManager As PictureTrackerManager
'Calculate the Inertia start velocity
Private ReadOnly _inertiaParam As New InertiaParam()
Private WithEvents _processor As New ManipulationInertiaProcessor(ProcessorManipulations.ALL, Factory.CreateTimer())
Public Sub New(ByVal pictureTrackerManager As PictureTrackerManager)
_pictureTrackerManager = pictureTrackerManager
End Sub
Private Sub Processor_OnManipulationStarted() Handles _processor.ManipulationStarted
_inertiaParam.Reset()
End Sub
Private Sub Processor_OnManipulationCompleted() Handles _processor.ManipulationCompleted
_inertiaParam.Stop()
_pictureTrackerManager.Completed(Me)
End Sub
...
Private Sub OnBeforeInertia(ByVal sender As Object, ByVal e As BeforeInertiaEventArgs) Handles _processor.BeforeInertia
...
End Sub
...
4. 我们还需要更改 PictureTrackerManager。在新的条件下,图片可能由惯性处理器使用,即使没有手指在触摸该对象。我们需要在操作完成时立即从映射中删除触摸 ID,但是只有在惯性处理器使图片完全停止时,我们才能够重用 PictureTracker 对象。将 InInertia() 函数添加到 PictureTrackerManager 类中:
(代码片段 – MultiTouch – InInertia CSharp)
C#
//We remove the touchID from the tracking map since the fingers are
//no longer touching the picture
public void InInertia(PictureTracker pictureTracker)
{
//remove all touch id from the map
foreach (int id in
(from KeyValuePair<int, PictureTracker> entry in _pictureTrackerMap
where entry.Value == pictureTracker
select entry.Key).ToList())
{
_pictureTrackerMap.Remove(id);
}
}
(代码片段 – MultiTouch –InInertia VB)
Visual Basic
' We remove the touchID from the tracking map since the fingers are
' no longer touching the picture
Public Sub InInertia(ByVal pictureTracker As PictureTracker)
' remove all touch id from the map
For Each id In (From entry In _pictureTrackerMap _
Where entry.Value Is pictureTracker _
Select entry.Key).ToList()
_pictureTrackerMap.Remove(id)
Next id
End Sub
5. 编译并运行。尝试将图片拉出屏幕。试验各种 Inertia 参数,看它们如何更改图片行为。
小结
本实验改进了一个基于鼠标的简单图片处理应用程序,将它升级成了类似于 Surface 的成熟的图片操作应用程序。您了解了如何检查是否存在多点触摸硬件,还看到了 Manipulation Processor 的魔力并在实验最后阶段添加了 Inertia 功能。
祝您实验愉快!