就在過去幾年,多點觸控還只是科幻電影中表現未來主義的一種重要手法,現在俨然已經成為主流的用戶界面技術。多點觸控顯示屏現在成了新型智能手機和 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 中的操作事件
事件 是否受 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 的一種簡單方法,但可能不是最好方法,因為它會發生突然變化。
ManipulationDelta 和 DeltaManipulation
SimpleManpulationDemo 處理的另一個唯一事件是 ManipulationDelta。ManipulationDeltaEventArgs 類定義 ManipulationDelta 類型的兩個屬性。(是的,該事件和類具有相同的名稱。)這些屬性是 DeltaManipulation 和 CumulativeManipulation。顧名思義,DeltaManipulation 反映了自上一個 ManipulationDelta 事件以來發生的操作,CumulativeManipulation 表示從 ManipulationStarting 事件開始的完整操作。
ManipulationDelta 具有四個屬性:
Translation 屬性,類型為 Vector
Scale 屬性,類型為 Vector
Expansion 屬性,類型為 Vector
Rotation 屬性,類型為 double
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 控件,就不再需要對諸如操作照片等基本任務直接使用操作事件。該控件包含一些新效果和行為,可確保您的應用程序的行為與其他觸控應用程序相同。
下載代碼示例:http://code.msdn.microsoft.com/mag201008UF