本文章是由機器翻譯。

UI 前沿技術

WPF 中的多點觸控操作事件

Charles Petzold

下載代碼示例

就在過去幾年,多點觸控還只是科幻電影中表現未來主義的一種重要手法,現在儼然已經成為主流的使用者介面技術。多點觸控顯示幕現在成了新型智慧手機和 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 類還支援與操作事件對應的可覆蓋方法,例如,On­ManipulationStarting。在 Silverlight for Windows Phone 7 中,這些可覆蓋方法由 Control 類定義。

多點觸控示例

照片檢視器可能是多點觸控的典型應用,在照片檢視器中,您可以在一個平面上移動照片,用兩根手指放大或縮小照片以及旋轉照片。這些操作有時稱為平移、縮放和旋轉,它們分別對應于平移、縮放和旋轉的標準圖形轉換。

很明顯,照片查看程式需要維護照片集合,支援添加新照片和刪除照片,並且最好能始終在一個較小的圖形幀中顯示多張照片,但我準備忽略所有這些方面,而著重介紹多點觸控交互。有了操作事件,一切都變得非常簡單,這讓我感到非常吃驚,我相信你們也會有同感。

本專欄的所有原始程式碼位於一個名為 WpfManipulationSamples 的可下載解決方案中。第一個專案是 SimpleManipulationDemo,MainWindow.xaml 檔在圖 2 中顯示。

圖 2 SimpleManipulationDemo 的 XAML 檔

<Window x:Class="SimpleManipulationDemo.MainWindow"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://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 元素將其 Render­Transform 設置為一個六位數的字串:

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);
    }
  }
  }

操作基礎知識

操作定義為一根或多根手指觸控特定元素的動作。 完整的操作從 Manipulation­Starting 事件開始,緊接著是 ManipulationStarted,並最終以 ManipulationCompleted 結束。 中間可能有多個 ManipulationDelta 事件。

每個操作事件都附帶有其自己的事件參數集,該參數集封裝在一個根據該事件命名並附加了 EventArgs 的類中,例如,ManipulationStartingEventArgs 和 ManipulationDeltaEventArgs。 這些類從我們熟悉的 InputEventArgs 派生,而後者又從 RoutedEvent­Args 派生。 這些類包括指示事件來源的 Source 和 OriginalSource 屬性。

在 SimpleManipulationDemo 程式中,Source 和 Original­Source 均設置為生成操作事件的 Image 元素。 只有 IsManipulation­Enabled 屬性設置為 true 的元素才會在這些操作事件中顯示為 Source 和 OriginalSource 屬性。

此外,與操作事件相關聯的每個事件參數類都包括一個名為 Manipulation­Container 的屬性。 這是發生多點觸控操作的元素。 操作事件中的所有座標都相對於此容器。

預設情況下,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 屬性的 ManipulationDeltaEvent­Args 中也提供了此中心。此原點相對於在 ManipulationStarting 事件中設置的 ManipulationContainer 而言。

您在 ManipulationDelta 事件中的工作是按以下順序根據增量值修改被操作物件的 Render­Transform 屬性:首先縮放,然後旋轉,最後平移。scaling first, then rotation, and finally translation.(事實上,由於水準和垂直縮放比例是相同的,您可以切換縮放轉換和旋轉轉換的順序,得到的結果仍然相同。)

图 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 事件及其自己的 Manipulation­Delta 事件系列。代碼通過事件參數的 Source 屬性有效地區分多個 Image 元素。

因此,很重要的一點是不要在欄位中設置暗示一次只能操作一個元素的任何狀態資訊。

由於每個 Image 元素都將自己的 IsManipulationEnabled 屬性設置為 true,因此可以同時操作多個元素。其中每個元素都可以生成唯一的操作事件系列。

當首次處理這些操作事件時,您可能需要深入研究是在 MainWindow 類還是充當容器的其他元素上將 IsManpulationEnabled 設置為 true。此功能並非不可以實現,但在實際操作時略顯複雜,並且也不是那麼強大。唯一的實際優點是:您不必在 ManipulationStarting 事件中設置 ManipulationContainer 屬性。當您必須在 ManipulatedStarted 事件中使用 ManipulationOrigin 屬性對子元素進行點擊測試,以確定正在操作哪個元素時,麻煩隨之而來。

接下來,您需要將正在操作的元素存儲為欄位,以便在將來的 ManipulationDelta 事件中使用。在這種情況下,由於您一次只能操作容器中的一個元素,因此完全可以將狀態資訊存儲在欄位中。

操作模式

如上所示,在 ManipulationStarting 事件期間設置的一個關鍵屬性是 ManipulationContainer。其他屬性對於自訂特定操作非常有用。

您可以使用 Manipulation­Modes 枚舉的成員初始化 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