windows7的触摸功能开辟了一个电脑的全新世纪。从此,您可以丢掉鼠标和键盘,直接用手在屏幕上玩游戏、用笔来写字编辑文档,聊天。
windows7最重要特性之一就是:支持多点触摸。比尔盖茨说,不久,鼠标键盘会消失。
Windows 7 使用户无需使用中间设备,通过手指触摸方式就能够管理应用程序。与其他指点设备不同,这种新功能支持在不同指点位置上同时发生多个输入事件,支持复杂的场景,比如通过十指或由多个并行用户管理应用程序。然而,要成功实现此功能,我们必须调整应用程序的用户界面和行为,以支持这种新的输入模式。
下面我们来看下Windows7下多点触摸图片处理应用程序的具体酷炫操作界面:
画布功能的操作界面
多点触摸--将图片自由缩放
当我们选择好一张图片后,通过红外线触控功能使用手指可以将图片移动到屏幕内的任意位置。再通过多点触摸功能,使用两根手指就可以轻松的将图片进行自由的旋转、缩小和放大。
多图片为一体时任意移动
还可以同时选择多个图片一起操作。在自己想操作的所有图片范围内,用手指以一个起点开始画圈,最后终点要与起点重合,这样就可以自由触控被选中的所有图片,不过此时这些图片是一体化的,如果想取消全选只需要点击另外任何一张图片或者屏幕内的黑色范围就可取消。
选择并拖拽文件夹内的图片
在屏幕的下半部分,单击左侧的图片文件夹,会弹出该文件夹内所有的图片,通过手指上下移动,可以对所有的图片进行浏览,如果选择好其中的一张图片,直接用手指拖拽到操作界面上即可。另外通过屏幕中间最下方右边的标签功能,再结合触摸式键盘输入信息,可以给每一张图片添加标签说明,而被添加上标签的图片会自动保存到标签文件夹里,这样方便用户对图片的统一分配和整理。
好了上面介绍了这么多关于Windows7的多点触摸图片应用程序,下面我们来打造自己的多点触摸图片处理应用程序,目标是将一个基于鼠标的简单图片操作应用程序升级为支持多点触摸的现代应用程序,类似于 Microsoft Surface 行为。
开发多点触摸图片处理应用程序
要理解如何管理多点触摸输入,我们首先需要理解如何处理(基于鼠标的)单点输入。为此,我们准备了一个基于鼠标的图片处理应用程序,就是多点触摸动手实验初始应用程序。
了解解决方案
1. 打开位于 %TrainingKitInstallDir%/MultiTouch/Ex1-PictureHandling/Begin 下的初始解决方案,选择想要使用的语言C#。
2. 编译并运行它。可以进行的操作有:通过单击挑选一张图片;按住鼠标左键并移动鼠标来拖动图片;使用鼠标滚轮缩放图片。每次选择一张图片时,该图片就会出现在最前面。在开始编码之前,首先了解一下初始应用程序。
该应用程序用于处理图片。每张图片由一个 Picture 用户控件表示。这是一个非常简单的控件,它基于 WPF。Picture 用户控件的 XAML 如下:
XAML <UserControl x:Class="MultitouchHOL.Picture" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://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="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://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 。如果用户按住鼠标左键,_picture 成员将拥有当前跟踪的图片;否则,它将拥有空值。_prevLocation 是 Mouse Move 事件报告的上一个位置,用于计算偏移量。
5. MainWindow 构造函数创建主窗口,注册各种事件处理函数。
public MainWindow() { InitializeComponent(); //启用笔触事件和加载图片 this.Loaded += (s, e) => { LoadPictures(); }; //注册鼠标事件 MouseLeftButtonDown += ProcessDown; MouseMove += ProcessMove; MouseLeftButtonUp += ProcessUp; MouseWheel += ProcessMouseWheel; }
6. LoadPictures() 函数从用户的图片文件夹加载图片,并为所有图片创建一个 Picture 控件。它只在初始化画布之后才执行此操作。下面看一下 LoadPictures() 代码。
7. 现在看一下如何处理鼠标事件。
private void ProcessDown(object sender, MouseButtonEventArgs args) { _prevLocation = args.GetPosition(_canvas); _picture = FindPicture(_prevMouseLocation); BringPictureToFront(_picture); }
按下鼠标左键将启动一个新的图片拖动会话。首先我们必须获得相对于画布的指针位置。我们将此信息保存在 _prevLocation 数据成员中。
8. 下一步是在该位置找到一张图片。FindPicture() 函数利用 WPF VisualTree 点击测试功能来找到最顶层的图片。如果鼠标所在位置没有图片,则返回空值。
9. BringPictureToFront() 将所选图片的 Z 轴次序设置在其他图片的最顶层。
此处理程序的处理结果是 _picture 数据成员“记住”了所选的图片,_prevLocation 获取鼠标位置的代码片段。我们看一下当鼠标移动时会发生什么情况:
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; }
如果用户未按下鼠标左键或者未选择任何图片,该函数将不执行任何操作。否则,该函数将计算平移量并更新图片的 X 和 Y 属性。它还将更新 _prevLocation。
10. 我们需要注意的最后一个函数是 ProcessMouseWheel:
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; }
将鼠标事件替换为触摸事件
我们将删除鼠标事件并将其替换为触摸事件,以便使用我们的手指处理图片。
1. 将以下代码行添加到 MainWindow.xaml.cs 文件 (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 事件处理程序中添加对此函数的调用:
public MainWindow() { ... //启用笔触事件和加载图片 this.Loaded += (s, e) => { Factory.EnableStylusEvents(this); LoadPictures(); }; ...
3. 删除 ProcessMouseWheel 事件处理程序及相应的事件注册(我们将在稍后处理缩放)。
4. (仅适用于 C# 用户)删除 MouseLeftButtonDown、MouseMove 和 MouseLeftButtonUp 的事件注册代码。MainWindow 构造函数应该类似于以下代码:
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)
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; }
6. 注册触笔事件。
public MainWindow() { ... //注册(触摸)事件 StylusDown += ProcessDown; StylusUp += ProcessUp; StylusMove += ProcessMove; }
7. 编译并运行。使用手指代替鼠标!
注意: 如果尝试使用多个手指会发生什么情况?为什么?
同时处理多张图片
在本任务中,我们将添加多点触摸支持。触摸屏幕的每个手指都会获得一个唯一的触摸 ID。只要这根手指继续触摸屏幕,系统就会将相同的触摸 ID 与该手指关联。当手指离开屏幕表面时,该触摸 ID 将被系统释放并可被硬件再次使用。在我们的示例中,当一根手指触摸图片时,应该将该手指的唯一触摸 ID 与该图片关联,直到该手指离开屏幕。如果两个或更多手指同时触摸屏幕,那么每个手指都可以操作相关的图片。
当使用 Stylus 事件作为触摸事件时,可以从 Stylus 事件参数中提取出触摸 ID:args.StylusDevice.Id
WPF 将使用相关的 StylusDevice.Id(触摸 ID)不断为每个触摸屏幕的手指触发事件。
1. 我们需要同时跟踪多张图片。对于每张图片,触摸 ID、上一个位置与图片用户控件之间必须保持关联。我们将首先添加一个新的 PictureTracker 类:
注意:PictureTracker 类也在 %TrainingKitInstallDir%/MultiTouch/Assets/PictureHandling下以实验资源的形式提供,请选择您想要使用的语言(C#)。
/// <summary> /// 跟踪单个图片 /// </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) { //什么都不做,可能有另一个触摸ID仍下跌 } }
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
private readonly Dictionary<int, PictureTracker> _pictureTrackerMap
4. 添加以下 PictureTrackerManager 类:
注意:PictureTrackerManager 类也以实验资产的形式在 %TrainingKitInstallDir%/MultiTouch/Assets/PictureHandling 下提供,
class PictureTrackerManager { //图片之间的接触和ID跟踪 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; //我们已经根据笔触ID追踪到了图片 if (_pictureTrackerMap.TryGetValue(touchId, out pictureTracker)) return pictureTracker; //获取图片下的触摸位置 Picture picture = FindPicture(location); if (picture == null) return null; //我们根据其他ID来追踪图片 pictureTracker = (from KeyValuePair<int, PictureTracker> entry in _pictureTrackerMap where entry.Value.Picture == picture select entry.Value).FirstOrDefault(); //第一次 if (pictureTracker == null) { //创建 pictureTracker = new PictureTracker(); pictureTracker.Picture = picture; BringPictureToFront(picture); } //记得接触ID和图片之间的相关性实证分析 _pictureTrackerMap[touchId] = pictureTracker; return pictureTracker; } /// <summary> /// 在触摸位置中找到图片 /// </summary> /// <param name="pointF">触摸位置</param> /// <returns>空的图片或照片,如果没有在触摸位置存在</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); } }
5. 将以下字段声明添加到 MainWindow 类的开头:
private readonly PictureTrackerManager _pictureTrackerManager;
6. 修改 MainWindow 构造函数:
a. 在调用 InitializeComponent() 之后,添加管理器初始化:
_pictureTrackerManager = new PictureTrackerManager(_canvas);
b. 更改触笔事件注册代码
//Register for stylus (touch) events StylusDown += _pictureTrackerManager.ProcessDown; StylusUp += _pictureTrackerManager.ProcessUp; StylusMove += _pictureTrackerManager.ProcessMove;
7. 从 MainWindow 类删除 ProcessDown、ProcessMove 和 ProcessUp 事件处理程序。这里将不再需要它们,因为它们现在已包含在 PictureTrackerManager 类中。
8. 编译并运行。尝试同时抓取多张图片。尝试使用多个手指抓取一张图片。发生了什么情况?为什么?
<本内容未完待续...>