原文 MSDN 杂志:UI 前沿技术 - WPF 中的多点触控操作事件
就在过去几年,多点触控还只是科幻电影中表现未来主义的一种重要手法,现在俨然已经成为主流的用户界面技术。 多点触控显示屏现在成了新型智能手机和 Tablet 计算机的标准显示屏。 此外,它还可能在公共场所的计算机上普及,例如 Microsoft Surface 率先开发的网亭或桌面计算机。
实际存在的唯一不确定因素是多点触控在常规台式计算机上的普及。 这种普及的最大障碍或许是长时间在垂直屏幕上移动手指所产生的疲劳(称为“大猩猩手臂”)。 我个人希望多点触控的强大功能将切实推进桌面显示屏的重新设计。 我们可以设想台式计算机的显示屏可能类似于配置制图桌,并且可能和制图桌一样大。
但那可能发生在遥远的未来。 目前,开发人员需要掌握新的 API。 Windows 7 中的多点触控支持已通过低级别和高级别的接口渗透并应用到 Microsoft .NET Framework 的各个领域。
如果您考虑到在显示屏上使用多根手指可能引起表达的复杂性,您或许就会了解为何到现在还没有人确切知道多点触控的“正确”编程接口。 这需要一定时间。 同时,您具有若干选择。
Windows Presentation Foundation (WPF) 4.0 为在 Windows 7 下运行的程序提供了两个多点触控接口。 为了专门使用多点触控,程序员希望探索低级别接口,该接口包含由 UIElement 定义的多个路由事件(名为 TouchDown、TouchMove、TouchUp、TouchEnter 和 TouchLeave)以及向下、移动和向上事件的预览版本。 显然,这些事件是根据鼠标事件建模的,但需要一个整数 ID 属性来跟踪显示屏上的多根手指。 Microsoft Surface 在 WPF 3.5 的基础上构建,不过它支持范围更广的低级别触控接口,可区分触控输入的类型和形状。
本专栏的主题是 WPF 4.0 中的高级别多点触控支持,它包含一个名称以“Manipulation”一词开头的事件的集合。 这些操作事件执行多个关键的多点触控作业:
Silverlight 4 文档中列出了部分操作事件,但可能会让读者产生一丝迷惑。 Silverlight 本身不支持这些事件,但针对 Windows Phone 7 编写的 Silverlight 应用程序则支持这些事件。 图 1 列出了这些操作事件。
图 1 Windows Presentation Foundation 4.0 中的操作事件
event | 是否受 Windows Phone 7 支持? |
ManipulationStarting | 不能 |
ManipulationStarted | 能 |
ManipulationDelta | 能 |
ManipulationInertiaStarted | 不能 |
ManipulationBoundaryFeedback | 否 |
ManipulationCompleted | 是 |
基于 Web 的 Silverlight 4 应用程序将继续使用 Touch.FrameReported 事件,我曾在 2010 年 3 月出版的 MSDN 杂志“手指之舞:探讨 Silverlight 中的多点触控支持”一文中探讨过该事件。
除操作事件本身以外,WPF 中的 UIElement 类还支持与操作事件对应的可覆盖方法,例如,OnManipulationStarting。 在 Silverlight for Windows Phone 7 中,这些可覆盖方法由 Control 类定义。
照片查看器可能是多点触控的典型应用,在照片查看器中,您可以在一个平面上移动照片,用两根手指放大或缩小照片以及旋转照片。 这些操作有时称为平移、缩放和旋转,它们分别对应于平移、缩放和旋转的标准图形转换。
很明显,照片查看程序需要维护照片集合,支持添加新照片和删除照片,并且最好能始终在一个较小的图形帧中显示多张照片,但我准备忽略所有这些方面,而着重介绍多点触控交互。 有了操作事件,一切都变得非常简单,这让我感到非常吃惊,我相信你们也会有同感。
本专栏的所有源代码位于一个名为 WpfManipulationSamples 的可下载解决方案中。 第一个项目是 SimpleManipulationDemo,MainWindow.xaml 文件在图 2 中显示。
图 2 SimpleManipulationDemo 的 XAML 文件
<Window x:Class="SimpleManipulationDemo.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Simple Manipulation Demo"> <Window.Resources> <Style TargetType="Image"> <Setter Property="Stretch" Value="None" /> <Setter Property="HorizontalAlignment" Value="Left" /> <Setter Property="VerticalAlignment" Value="Top" /> </Style> </Window.Resources> <Grid> <Image Source="Images/112-1283_IMG.JPG" IsManipulationEnabled="True" RenderTransform="0.5 0 0 0.5 100 100" /> <Image Source="Images/139-3926_IMG.JPG" IsManipulationEnabled="True" RenderTransform="0.5 0 0 0.5 200 200" /> <Image Source="Images/IMG_0972.JPG" IsManipulationEnabled="True" RenderTransform="0.5 0 0 0.5 300 300" /> <Image Source="Images/IMG_4675.JPG" IsManipulationEnabled="True" RenderTransform="0.5 0 0 0.5 400 400" /> </Grid> </Window>
首先,请注意所有三个 Image 元素上的设置:
IsManipulationEnabled="True"
默认情况下,此属性为 false。 对于您希望在其上获得多点触控输入并生成操作事件的任何元素,您必须将其设置为 true。
操作事件是 WPF 路由事件,这意味着这些事件会使可视化树浮现出来。 在 此程序中,Grid 和 MainWindow 的 IsManipulationEnabled 属性均未设置为 true,但您仍可将操作事件的处理程序附加至 Grid 和 MainWindow 元素,或者在 MainWindow 类中覆盖 OnManipulation 方法。
另请注意,每个 Image 元素将其 RenderTransform 设置为一个六位数的字符串:
RenderTransform="0.5 0 0 0.5 100 100"
这是设置已初始化的 MatrixTransform 对象的 RenderTransform 属性的快捷方式。 在此特定示例中,设置为 MatrixTransform 的 Matrix 对象已经过初始化,可执行 0.5 个单位的缩放(使照片缩小至实际大小的一半)和朝右下方的 100 个单位的平移。 该窗口的代码隐藏文件会访问并修改此 MatrixTransform。
图 3 显示了完整的 MainWindow.xaml.cs 文件,该文件仅覆盖两个方法,即 OnManipulationStarting 和 OnManipulationDelta。 这些方法处理由 Image 元素生成的操作。
图 3 SimpleManipulationDemo 的代码隐藏文件
using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace SimpleManipulationDemo { public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); } protected override void OnManipulationStarting( ManipulationStartingEventArgs args) { args.ManipulationContainer = this; // Adjust Z-order FrameworkElement element = args.Source as FrameworkElement; Panel pnl = element.Parent as Panel; for (int i = 0; i < pnl.Children.Count; i++) Panel.SetZIndex(pnl.Children[i], pnl.Children[i] == element ? pnl.Children.Count : i); args.Handled = true; base.OnManipulationStarting(args); } protected override void OnManipulationDelta( ManipulationDeltaEventArgs args) { UIElement element = args.Source as UIElement; MatrixTransform xform = element.RenderTransform as MatrixTransform; Matrix matrix = xform.Matrix; ManipulationDelta delta = args.DeltaManipulation; Point center = args.ManipulationOrigin; matrix.ScaleAt( delta.Scale.X, delta.Scale.Y, center.X, center.Y); matrix.RotateAt( delta.Rotation, center.X, center.Y); matrix.Translate( delta.Translation.X, delta.Translation.Y); xform.Matrix = matrix; args.Handled = true; base.OnManipulationDelta(args); } } }
操作定义为一根或多根手指触控特定元素的动作。 完整的操作从 ManipulationStarting 事件开始,紧接着是 ManipulationStarted,并最终以 ManipulationCompleted 结束。 中间可能有多个 ManipulationDelta 事件。
每个操作事件都附带有其自己的事件参数集,该参数集封装在一个根据该事件命名并附加了 EventArgs 的类中,例如,ManipulationStartingEventArgs 和 ManipulationDeltaEventArgs。 这些类从我们熟悉的 InputEventArgs 派生,而后者又从 RoutedEventArgs 派生。 这些类包括指示事件来源的 Source 和 OriginalSource 属性。
在 SimpleManipulationDemo 程序中,Source 和 OriginalSource 均设置为生成操作事件的 Image 元素。 只有 IsManipulationEnabled 属性设置为 true 的元素才会在这些操作事件中显示为 Source 和 OriginalSource 属性。
此外,与操作事件相关联的每个事件参数类都包括一个名为 ManipulationContainer 的属性。 这是发生多点触控操作的元素。 操作事件中的所有坐标都相对于此容器。
默认情况下,ManipulationContainer 属性设置为与 Source 和 OriginalSource 属性相同的元素,也就是被操作的元素,不过这可能不是您所希望的。 通常,大家不希望操作容器与被操作的元素相同,因为动态移动、缩放和旋转报告触控信息的同一元素需要技巧性很强的交互。 您应将操作容器作为被操作元素的父项,或者作为沿可视化树向上追寻的某个元素。
在大多数操作事件中,ManipulationContainer 属性都是只读属性。 但元素接收的第一个操作事件例外。 在 ManipulationStarting 中,您可以将 ManipulationContainer 更改为更适合的容器。 在 SimpleManipulationDemo 项目中,此工作只需通过一行代码即可完成:
args.ManipulationContainer = this;
在所有后续事件中,ManipulationContainer 将是 MainWindow 元素,而不是 Image 元素,并且所有坐标都将相对于该窗口。 由于包含 Image 元素的 Grid 也与该窗口对齐,因此,此方法非常适用。
OnManipulationStarting 方法的其余部分通过重置该 Grid 中所有 Image 元素的 Panel.ZIndex 附加属性,专门用于在前台显示触控 Image 元素。 这是处理 ZIndex 的一种简单方法,但可能不是最好方法,因为它会发生突然变化。
SimpleManpulationDemo 处理的另一个唯一事件是 ManipulationDelta。 ManipulationDeltaEventArgs 类定义 ManipulationDelta 类型的两个属性。 (是的,该事件和类具有相同的名称。)这些属性是 DeltaManipulation 和 CumulativeManipulation。 顾名思义,DeltaManipulation 反映了自上一个 ManipulationDelta 事件以来发生的操作,CumulativeManipulation 表示从 ManipulationStarting 事件开始的完整操作。
ManipulationDelta 具有四个属性:
Vector 结构定义 double 类型的两个属性,分别名为 X 和 Y。 Silverlight for Windows Phone 7 中的操作支持的一个较显著的差异是缺少 Expansion 和 Rotation 属性。
Translation 属性指示水平方向和垂直方向的移动(或平移)。 对元素的单指操作可生成平移变化,但平移也可以是其他操作的一部分。
Scale 和 Expansion 属性均指示大小变化(缩放),这始终需要两根手指。 Scale 依据乘法进行缩放,Expansion 依据加法进行缩放。 使用 Scale 可设置缩放转换;使用 Expansion 可按照与设备无关的单位增大或减小某个元素的 Width 和 Height 属性。
在 WPF 4.0 中,Scale 矢量的 X 和 Y 值始终是相同的。 操作事件不会提供足够的信息以各向异性的方式(即,在水平方向和垂直方向各不相同)缩放元素。
默认情况下,旋转也需要两根手指,但我们将在稍后介绍如何启用单指旋转。 在任何特定 ManipulationDelta 事件中,可能需要设置所有四个属性。 可使用两根手指放大某个元素,同时旋转该元素并将其移动到另一位置。
缩放和旋转始终相对于某个特定的中心点。 Point 类型的 ManipulationOrigin 属性的 ManipulationDeltaEventArgs 中也提供了此中心。 此原点相对于在 ManipulationStarting 事件中设置的 ManipulationContainer 而言。
您在 ManipulationDelta 事件中的工作是按以下顺序根据增量值修改被操作对象的 RenderTransform 属性:首先缩放,然后旋转,最后平移。 (事实上,由于水平和垂直缩放比例是相同的,您可以切换缩放转换和旋转转换的顺序,得到的结果仍然相同。)
图 3 中的 OnManipulationDelta 方法显示了一种标准方法。 Matrix 对象从操作的 Image 元素上设置的 MatrixTransform 获取。 该对象通过调用 ScaleAt、RotateAt(二者相对于 ManipulationOrigin)和 Translate 进行修改。 Matrix 是一个结构而不是类,因此您必须用新值替换 MatrixTransform 中的旧值,以此作为结束。
此代码可略作更改。 如下所示,它使用以下语句围绕一个中心进行缩放:
matrix.ScaleAt(delta.Scale.X, delta.Scale.Y, center.X, center.Y);
这相当于平移到中心点的相反方向、进行缩放,然后重新平移:
matrix.Translate(-center.X, -center.Y); matrix.Scale(delta.Scale.X, delta.Scale.Y); matrix.Translate(center.X, center.Y);
同样,RotateAt 方法可以替换为:
matrix.Translate(-center.X, -center.Y); matrix.Rotate(delta.Rotation); matrix.Translate(center.X, center.Y);
两个相邻的 Translate 调用现在相互抵消,因此最终合成结果为:
matrix.Translate(-center.X, -center.Y); matrix.Scale(delta.Scale.X, delta.Scale.Y); matrix.Rotate(delta.Rotation); matrix.Translate(center.X, center.Y);
以上方法的效率可能更高。
图 4 显示了运行中的 SimpleManipulationDemo 程序。
图 4 SimpleManipulationDemo 程序
SimpleManpulationDemo 程序的一个有趣功能是您可以同时操作两个甚至更多的 Image 元素,条件是您具备相应的硬件支持和足够多的手指。 每个 Image 元素生成其自己的 ManipulationStarting 事件及其自己的 ManipulationDelta 事件系列。 代码通过事件参数的 Source 属性有效地区分多个 Image 元素。
因此,很重要的一点是不要在字段中设置暗示一次只能操作一个元素的任何状态信息。
由于每个 Image 元素都将自己的 IsManipulationEnabled 属性设置为 true,因此可以同时操作多个元素。 其中每个元素都可以生成唯一的操作事件系列。
当首次处理这些操作事件时,您可能需要深入研究是在 MainWindow 类还是充当容器的其他元素上将 IsManpulationEnabled 设置为 true。 此功能并非不可以实现,但在实际操作时略显复杂,并且也不是那么强大。 唯一的实际优点是:您不必在 ManipulationStarting 事件中设置 ManipulationContainer 属性。 当您必须在 ManipulatedStarted 事件中使用 ManipulationOrigin 属性对子元素进行点击测试,以确定正在操作哪个元素时,麻烦随之而来。
接下来,您需要将正在操作的元素存储为字段,以便在将来的 ManipulationDelta 事件中使用。 在这种情况下,由于您一次只能操作容器中的一个元素,因此完全可以将状态信息存储在字段中。
如上所示,在 ManipulationStarting 事件期间设置的一个关键属性是 ManipulationContainer。 其他属性对于自定义特定操作非常有用。
您可以使用 ManipulationModes 枚举的成员初始化 Mode 属性,从而限制可执行操作的类型。 例如,如果您将操作专用于水平滚动,则可能需要将事件仅限制为水平平移。 ManipulationModesDemo 程序通过显示列出各选项的 RadioButton 元素的列表,使您可以动态地设置模式,如图 5 所示。
图 5 ManipulationModeDemo 显示
当然,RadioButton 是 WPF 4.0 中直接响应触控的众多控件之一。
默认情况下,您需要两根手指才能旋转对象。 不过,如果真实照片位于真实桌面上,您可以将手指放在角上,并将其旋转一圈。 旋转大致上是围绕对象中心进行的。
您可以设置 ManipulationStartingEventArgs 的 Pivot 属性,对操作事件执行此操作。 默认情况下,Pivot 属性为 null;通过设置 ManipulationPivot 对象的该属性,可以启用单指旋转。 ManipulationPivot 的关键属性
是 Center,您可能会考虑将其作为操作元素的中心来计算:
Point center = new Point(element.ActualWidth / 2, element.ActualHeight / 2);
不过,此中心点必须相对于操作容器而言,在我向大家展示的程序中,这一容器就是处理事件的元素。 将该中心点从操作元素平移到容器非常简单:
center = element.TranslatePoint(center, this);
还需要设置另一条小小的信息。 如果您仅指定中心点,当您将手指恰好放在元素中心时,将会出现问题:丝毫的移动都会导致该元素疯狂地旋转! 因此,ManipulationPivot 还具有 Radius 属性。 如果手指位于中心点的半径单位内,将不会发生旋转。 ManipulationPivotDemo 程序将该半径设置为半英寸:
args.Pivot = new ManipulationPivot(center, 48);
现在,单根手指便可执行旋转和平移的组合操作。
至此,本文已介绍了使用 WPF 4.0 操作事件的基础知识。 当然,这些技术存在一些变化,我将在后续专栏中陆续为大家介绍,此外还将介绍操作延时的强大功能。
您还可以看看 Surface Toolkit for Windows Touch,该页为您的应用程序提供了触控优化控件。 特别是有了 ScatterView 控件,就不再需要对诸如操作照片等基本任务直接使用操作事件。 该控件包含一些新效果和行为,可确保您的应用程序的行为与其他触控应用程序相同。
Charles Petzold 是《MSDN 杂志》的长期特约编辑。 他目前正在撰写《Programming Windows Phone 7》,该书将在 2010 年秋季作为可免费下载的电子书发布。 现在,已通过其网站 charlespetzold.com 提供了预览版本。
衷心感谢以下技术专家对本文的审阅:Doug Kramer、Robert Levy 和 Anson Tsao