本文章是由機器翻譯。

UI Frontiers

觸控筆勢 Windows 電話

查爾斯 Petzold

下載程式碼範例

限的身分會耗費太多他從專業生涯觀察演化過程的 Api,我已經被相當激發由 multi-touch 所佔用的 API 宇宙該小角。我不確定我甚至會想要計算的不同 multi-touch Api 散至 Windows 簡報基礎 (WPF)]、 [Microsoft 介面]、 [Silverlight]、 [XNA] 和 [Windows 電話,但較最明顯的是 multi-touch"統一的理論 」 是仍然難以捉摸。

當然,這個眾多的觸控 Api 不應該 ; 令人驚訝的一項技術仍然是較小。此外,觸控不比滑鼠複雜。這是部分可能的互動的多個根手指受限於,但它也會反映純粹只是人工的裝置,例如滑鼠與全部自然手指之間的差異。我們人類的存留期使用我們手指的經驗,我們希望他們與世界互動以已知的方式,即使我們所接觸的視訊顯示器光滑表面。

應用程式設計者,若為 Windows 電話 7 會定義四個 — 是,四個 — 不同觸控式介面。

針對 Windows 電話 7 所撰寫的 Silverlight 應用程式可以選擇取得低階觸控輸入,透過靜態的 Touch.FrameReported 事件,或透過各種操作較高層級的輸入路由事件。這些操作事件是大部分是在 WPF 中,類似事件子集,但它們充分不同,會造成主要問題讓您頭痛。

XNA 應用程式的 Windows 電話 7 使用靜態的 TouchPanel 類別來取得觸控輸入,但是這樣的單一類別實際上包含兩個觸控介面:GetState 方法會取得低階手指活動和 ReadGesture 方法會取得較高層級的筆勢。ReadGesture 方法支援筆勢不是像手寫筆的筆勢,例如核取記號和圓形。它們所描述的名稱,例如點選、 拖曳和情況更簡單筆勢。為了與 XNA 架構應用程式,而不是透過事件傳遞觸控輸入是輪詢一次。

筆勢前來 Silverlight

我當然假設讓我很驚訝地看到一個加入至混合第三個 Windows 電話 7 的 Silverlight 已經有足夠數量的觸控 Api,— 雖然中找有點太晚我在我的活頁簿中描述的工具組「 程式設計 Windows 電話 7 」 (在 [微軟出版品,2010年)。

您可能已經知道,釋放透過 CodePlex 的工具套件有已補充了 WPF 和 Silverlight 的過去幾年來的各種版本。這些工具套件可讓取得新的類別,開發人員一般船週期之外,通常我們 「 偷窺視 」 增強功能在可能的架構來合併在未來版本提供給 Microsoft。完整的原始程式碼是額外的加分。

Windows 電話 7 現在也受惠於這自訂。此 Silverlight 的 Windows 工具電話組 (可在 silverlight.codeplex.com) 包含日期,TimePicker 和 ToggleSwitch 控制已經熟悉給使用者的 Windows 電話 7;WrapPanel (方便處理電話方向變更)。和觸控筆勢支援。

此工具組中的這個新 Silverlight 筆勢支援被刻意要 XNA TouchPanel.ReadGesture 方法,類似,除了它透過路由的事件,而非輪詢的傳送。

相似程度為何?比預期更因此!查看原始程式碼,我很驚訝地發現這些新的 Silverlight 筆勢事件已完全衍生自 XNA TouchPanel.ReadGesture 方法的呼叫。我不認為 Windows 電話上的 Silverlight 應用程式允許呼叫這個 XNA 方法,但就是這樣。

雖然 Silverlight 和 XNA 筆勢很相當類似,筆勢相關聯的屬性不會。XNA 屬性,請使用向量,比方說,並且由於 Silverlight 並不會包含向量結構 (我覺得實在太離譜省略),屬性必須以特定簡單的方式重新定義的設計階段支援。

如我先前使用的這些筆勢的事件,他們已經走來 Silverlight 的 Windows 電話我最愛觸控 API。我已經發現它們是完整的我需要做些什麼,也相當容易使用。

我要提供這些筆勢要執行的實際工作來示範。

筆勢服務和接聽程式

此資料行的所有原始程式碼都在可下載的 Visual Studio 方案,名為 GestureDemos,其中包含三個專案中。您必須有安裝,不用多說,Windows 電話 7 開發工具以及工具 Silverlight 的 Windows 電話組。

在安裝此工具組之後, 您可以使用它在 Windows 電話專案中加入 Microsoft.Phone.Controls.Toolkit 組件的參考。在 [加入參考] 對話方塊中,它應該會列在下。NET] 索引標籤。

在 XAML 檔案中,您必須像這樣 (但全部在同一行) 的 XML 命名空間宣告:

xmlns:toolkit=
"clr-namespace:Microsoft.Phone.Controls;
assembly=Microsoft.Phone.Controls.Toolkit"

以下是 12 的可用動作事件,大致我將討論這些 (我未分組在同一行的事件和相關在順序下發生) 的順序:

GestureBegin, GestureCompleted
Tap
DoubleTap
Hold
DragStarted, DragDelta, DragCompleted
Flick
PinchStarted, PinchDelta, PinchCompleted

假設您想要處理點選並按住發生在框格或方格的任何子物件的事件。 您可以指定在 XAML 中檔案如下:

<Grid ... >
  <toolkit:GestureService.GestureListener>
    <toolkit:GestureListener 
      Tap="OnGestureListenerTap"
      Hold="OnGestureListenerHold" />
  </toolkit:GestureService.GestureListener>
    ...
</Grid>

您指定的事件和處理常式的 GestureListener 標籤內之子系的 GestureService 類別的 GestureListener 附加屬性。

或者在程式碼中所需的命名空間指示詞的 Microsoft.Phone.Controls 命名空間和下列程式碼:

GestureListener gestureListener = 
  GestureService.GetGestureListener(element);
gestureListener.Tap += OnGestureListenerTap;
gestureListener.Hold += OnGestureListenerHold;

在任何情況下,如果您要設定這個筆勢接聽程式面板上,請確定的 Background 屬性至少設定為 [透明! 事件只是將會繼續其預設背景為 null 的面板。

點選並按住

筆勢的所有事件都伴隨事件引數的型別 GestureEventArgs 或從 GestureEventArgs 衍生的型別。 OriginalSource 屬性指示符合螢幕; 第一個手指接觸到的最上層的項目GetPosition 方法會提供相對於任何項目該手指的目前座標。

筆勢事件是路由傳送,這表示它們可以往上尋找視覺化樹狀結構,並在其中處理已安裝 GestureListener 將項目。 事件處理常式如往常般運作,可以將 GestureEventArgs Handled 屬性設定為 true 可防止事件旅行中的進一步向上視覺化樹狀結構。 不過,這只會影響使用這些筆勢事件其他項目的大小。 將 Handled 設定為 true 並不會防止者取得觸控輸入,透過其他介面的視覺化樹狀結構中較高層的項目。

GestureBegin 事件表示為手指有接觸到先前 fingerless 的螢幕。當所有手指都離開畫面時,發出信號指示 GestureCompleted。 這些事件可能是方便初始化或清除,但您將通常會更著重於這兩個事件之間發生的動作事件。

我不會花多久的時間點上較簡單的筆勢。 當為手指接觸到螢幕上拿然後起最多在大約 1.1 秒內,而不從原始位置太遠移動,就會發生兩。 如果關閉連續兩次點選的時間,第二個是透過為 DoubleTap。 當為手指在螢幕上按下和約 1.1 秒會維持在大約相同的地方,就會發生按住不放。 在這次結尾處會產生保留事件而不需等待提升手指。

拖筆觸

拖曳序列,其中包含 「 DragStarted 」 事件、 零個或多個 DragDelta 事件以及 DragCompleted 事件 — 為手指接觸到螢幕、 移動和上拿起時,就會發生。 因為它無法識別拖曳會變成第一次接觸到螢幕時,DragStarted 事件被等到手指實際開始移動超過兩下閾值。 DragStarted 事件可能手指而不移動的第二個有關時,已在螢幕上加保留事件。

因為手指已開始移動引發 DragStarted 事件時,DragStartedEventArgs 物件可以包括型別方向 (水平或垂直) 方向屬性。 伴隨的 DragDelta 事件 DragDeltaEventArgs 物件包含更多的資訊:HorizontalChange 和 VerticalChange 屬性,這會很方便,將加入 TranslateTransform 或 Canvas.Left 和 Canvas.Top 的 x 和 y 屬性附加屬性。

筆觸事件發生於手指離開螢幕,因為它是仍移動,建議使用者想要發生的慣性。 事件引數包括角度 (順時針起正數 x 軸) 和 HorizontalVelocity 和 VerticalVelocity 的值,兩個像素為單位每秒。

筆觸事件可能會發生在隔離;它可能會發生 DragStarted 和 DragCompleted 事件,而不需任何 DragDelta 事件; 之間或或者它可能會依照一系列的前 DragCompleted 的 DragDelta 事件。 通常您會想要處理拖曳事件並彈事件一起使用,幾乎像是筆觸是在拖曳。 不過,您必須新增您自己的慣性邏輯。

這會示範在 DragAndFlick 專案中。 顯示包含使用者只需拖曳周圍食指的橢圓形。 如果手指離開螢幕與 flicking 的影片,然後筆觸事件發生於並筆觸處理常式會將一些資訊儲存並安裝 CompositionTarget.Rendering 事件的處理常式。 這個事件,在 [視訊顯示重新整理與同步處理發生於 — 會持續移動時減速套速度的橢圓形。

關閉側邊彈跳是有點異常處理:程式會維護一個位置,彷彿橢圓形只保留在相同的方向移動在停止;該位置會摺成它可以跳躍的區域。

Pinch 我,我必須 Dreaming

情況序列發生於兩根手指接觸螢幕;它通常會解譯為展開或收縮螢幕上物件,也可能旋轉。

沒有 pinching 的作業是由組成其中一項處理,multi-touch 的最陰險區域沒有問題,很少會以查看更高層級介面在提供足夠的資訊失敗。 最有名 Windows 電話 7 ManipulationDelta 事件是特別難以使用。

當處理筆勢,拖曳序列和情況序列是互斥的。 它們不會重疊,但它們可能接連發生。 比方說,按到螢幕的手指並拖曳它。 會產生 DragStarted 和多個 DragDelta 事件。 現在只要按下為第二個手指至畫面。 您將會以完成拖曳順序後面加上 PinchStarted 和多個 PinchDelta 事件 DragCompleted。 第一個手指會持續移動時,現在提起第二個手指。 這就是完成情況序列,後面加上 DragStarted 和 DragDelta PinchCompleted。 根據手指碰觸螢幕的數目而定,您正在基本上間交替拖曳序列和情況序列。

這個情況筆勢的一個很有幫助特徵是它不會捨棄資訊。 您可以完全重建兩根手指的位置,因此您可以隨時回到先從首要原則如果您要使用的事件引數的屬性。

在情況順序,一根手指的目前位置,讓我們稱之為主要手指 — 是隨時可見的 GetPosition 方法。 討論區,呼叫該傳回值 pt1。 對於 PinchStarted 事件,PinchStartedGestureEventArgs 類別有兩個額外的屬性名稱為距離與角度表示的第二個的手指相對於第一個位置。 您可以輕易地計算使用下列陳述式的實際位置:

Point pt2 = new Point(pt1.X + args.Distance * Cos(args.Angle),
                      pt1.Y + args.Distance * Sin(args.Angle));

[角度] 屬性是以度為單位,因此您將需要 Cos 並 Sin 方法之前呼叫 Math.Cos 和 Math.Sin 將轉換為弳度為單位。 PinchStarted 處理常式已完成之前,您也要將距離與角度屬性儲存在欄位中,可能是已命名的 pinchStartDistance 和 pinchStartAngle。

PinchDelta 事件會伴隨 PinchGestureEventArgs 物件。 同樣地,GetPosition 方法提供了主要根手指也許已經從原始位置移動的位置。 至於第二個根手指的事件引數會提供 DistanceRatio 和 TotalAngleDelta 屬性。

DistanceRatio 是原始的距離,這表示您可以計算的目前距離手指的目前距離比率如下:

double distance = args.DistanceRatio * pinchStartDistance;

TotalAngleDelta 差別手指與原始角度之間的目前角度。 您可以計算目前角度如下:

double angle = args.TotalAngleDelta + pinchStartAngle;

現在您可以計算為第二個手指之前的位置:

Point pt2 = new Point(pt1.X + distance * Cos(angle),
                      pt1.Y + distance * Sin(angle));

您不需要在 PinchDelta 以處理進一步的 PinchDelta 事件處理期間將任何額外的資訊儲存到欄位。

TwoFingerTracking 專案會示範這個邏輯,藉由顯示追蹤一或兩根手指在螢幕上的藍色和綠色橢圓形。

縮放及旋轉

PinchDelta 事件也會提供足夠的資訊,在物件上執行縮放及旋轉。 我必須提供我自己矩陣乘法的方法,但是這唾手可得程度有關。

若要示範,ScaleAndRotate 專案實作什麼現在是 「 傳統 」 型別,可讓您拖曳示範之用,縮放及選擇性地旋轉相片]。 若要執行這些轉換,我定義具有 double-barreled RenderTransform 的影像項目中所示的 圖 1

圖 1] 中 ScaleAndRotate 的影像項目

<Image Name="image"
  Source="PetzoldTattoo.jpg"
  Stretch="None"
  HorizontalAlignment="Left"
  VerticalAlignment="Top">
  <Image.RenderTransform>
    <TransformGroup>
      <MatrixTransform x:Name="previousTransform" />
        <TransformGroup x:Name="currentTransform">
          <ScaleTransform x:Name="scaleTransform" />
          <RotateTransform x:Name="rotateTransform" />
          <TranslateTransform x:Name="translateTransform" />
        </TransformGroup>
    </TransformGroup>
  </Image.RenderTransform>
</Image>

當情況或拖曳作業正在進行中時,三個轉型中巢狀的 TransformGroup 管理來移動螢幕上的圖片、 放大物件和旋轉它。 DragCompleted 或 PinchCompleted 事件發生時,名為 previousTransform MatrixTransform 在矩陣乘以複合轉換為 TransformGroup 的 Value 屬性。 在此 TransformGroup 三個轉型會再設回其預設值。

縮放比例和旋轉均相對於中心點,也就是轉換發生時,會保留在相同的位置的點。 縮放或旋轉相對於在不同的位置縮放或相對於其右下角旋轉相片比其左上角最後一張相片。

ScaleAndRotate 程式碼所示 圖 2。 我使用主要手指為縮放與旋轉中心。這些中心點為單位設定 PinchStarted 處理期間轉換,以及他們不變更情況序列的持續期間。 在 PinchDelta 事件 DistanceRatio 和 TotalAngleDelta 屬性會提供相對於該中心的旋轉] 以及 [縮放比例資訊。 任何在主要的手指 (這必須偵測到已儲存的欄位) 的移動中變更,然後就會變成整體的轉譯因素。

圖 2 ScaleAndRotate 程式碼

public partial class MainPage : PhoneApplicationPage
{
    bool isDragging;
    bool isPinching;
    Point ptPinchPositionStart;
    public MainPage()
    {
        InitializeComponent();
    }
    void OnGestureListenerDragStarted(object sender, DragStartedGestureEventArgs args)
    {
        isDragging = args.OriginalSource == image;
    }
    void OnGestureListenerDragDelta(object sender, DragDeltaGestureEventArgs args)
    {
        if (isDragging)
        {
            translateTransform.X += args.HorizontalChange;
            translateTransform.Y += args.VerticalChange;
        }
    }
    void OnGestureListenerDragCompleted(object sender, 
      DragCompletedGestureEventArgs args)
    {
        if (isDragging)
        {
            TransferTransforms();
            isDragging = false;
        }
    }
    void OnGestureListenerPinchStarted(object sender, 
      PinchStartedGestureEventArgs args)
    {
        isPinching = args.OriginalSource == image;
        if (isPinching)
        {
            // Set transform centers
            Point ptPinchCenter = args.GetPosition(image);
            ptPinchCenter = previousTransform.Transform(ptPinchCenter);
            scaleTransform.CenterX = ptPinchCenter.X;
            scaleTransform.CenterY = ptPinchCenter.Y;
            rotateTransform.CenterX = ptPinchCenter.X;
            rotateTransform.CenterY = ptPinchCenter.Y;
            ptPinchPositionStart = args.GetPosition(this);
        }
    }
    void OnGestureListenerPinchDelta(object sender, PinchGestureEventArgs args)
    {
        if (isPinching)
        {
            // Set scaling
            scaleTransform.ScaleX = args.DistanceRatio;
            scaleTransform.ScaleY = args.DistanceRatio;
            // Optionally set rotation
            if (allowRotateCheckBox.IsChecked.Value)
                rotateTransform.Angle = args.TotalAngleDelta;
            // Set translation
            Point ptPinchPosition = args.GetPosition(this);
            translateTransform.X = ptPinchPosition.X - ptPinchPositionStart.X;
            translateTransform.Y = ptPinchPosition.Y - ptPinchPositionStart.Y;
        }
    }
    void OnGestureListenerPinchCompleted(object sender, PinchGestureEventArgs args)
    {
        if (isPinching)
        {
            TransferTransforms();
            isPinching = false;
        }
    }
    void TransferTransforms()
    {
        previousTransform.Matrix = Multiply(previousTransform.Matrix, 
          currentTransform.Value);
        // Set current transforms to default values
        scaleTransform.ScaleX = scaleTransform.ScaleY = 1;
        scaleTransform.CenterX = scaleTransform.CenterY = 0;
        rotateTransform.Angle = 0;
        rotateTransform.CenterX = rotateTransform.CenterY = 0;
        translateTransform.X = translateTransform.Y = 0;
    }
    Matrix Multiply(Matrix A, Matrix B)
    {
        return new Matrix(A.M11 * B.M11 + A.M12 * B.M21,
                          A.M11 * B.M12 + A.M12 * B.M22,
                          A.M21 * B.M11 + A.M22 * B.M21,
                          A.M21 * B.M12 + A.M22 * B.M22,
                          A.OffsetX * B.M11 + A.OffsetY * B.M21 + B.OffsetX,
                          A.OffsetX * B.M12 + A.OffsetY * B.M22 + B.OffsetY);
    }
}

這當然正是我曾曾寫過,最簡單的情況程式碼及事實也許是我可以為這個新的筆勢介面提供最佳簽署。

也許 multi-touch 統一的理論不偏離畢竟。

Charles Petzold 是的 longtime 特約編輯 MSDN Magazine*.*他的新活頁簿,「 程式設計在 Windows 電話 7 」 (微軟出版品,2010年),是用作免費下載在 bit.ly/cpebookpdf.

感謝至下列技術專家檢閱這份文件: Richard Bailey