概述
Windows 7 使用戶無需使用中間設備,通過手指觸摸方式就能夠 管理應用程序。這擴展了平板電腦基於觸筆的功能。與其他指點設備不同,這種 新功能支持在不同指點位置上同時發生多個輸入事件,支持復雜的場景,比如通 過十指或由多個並行用戶管理應用程序。然而,要成功實現此功能,我們必須調 整應用程序的用戶界面和行為,以支持這種新的輸入模式。
本次動手實驗 (Hands-On Lab, HOL) 的目標是將一個基於鼠標的簡單圖片操 作應用程序升級為支持多點觸摸的現代應用程序,類似於 Microsoft Surface 行 為。
圖 1
啟用了多點觸摸的應用程序
目標
本動手實 驗將學習如何管理多點觸摸事件,包括:
• 理解同時操作多個對象 的含義
• 檢查多點觸摸硬件是否存在及其就緒情況
• 在 WPF 3.5SP1 中實現多點觸摸事件
• 通過內置的 WPF 觸筆事件使 用多點觸摸事件
• 使用操作 (Manipulation) 和慣性 (Inertia) 處 理器
設置
為了方便起見,我們以 Visual Studio 代碼片段的形式 提供將在本動手實驗中使用的許多代碼。本實驗所需的設置包括安裝這些代碼片 段。為此:
1.運行位於本實驗的 Setup 文件夾下的 MultiTouchLab.vsi 安裝程序。
2.按照向導說明安裝代碼片段。
系統要求
要完成本實驗,必須 擁有以下工具:
• Microsoft Visual Studio 2008 SP1
• Windows 7
• Windows 7 Integration Library 示例(Windows Touch:開發人員資源)
• 一台多點觸摸硬件設備
練習 1 :開發多點觸摸圖片處理應用程序
要理解如何管理多點觸摸輸入,我們首先需要理解如何處理(基於鼠標的 )單點輸入。為此,我們准備了一個基於鼠標的圖片處理應用程序,就是多點觸 摸動手實驗初始應用程序。
任務 1 – 了解解決方案
1.打開 位於 %TrainingKitInstallDir%\MultiTouch\Ex1-PictureHandling\Begin 下的 初始解決方案,選擇想要使用的語言(C# 或 VB)。
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 (C#) 或 MainWindow.xaml.vb 文件 (Visual Basic)。如果 用戶按住鼠標左鍵,_picture 成員將擁有當前跟蹤的圖片;否則,它將擁有空值 。_prevLocation 是 Mouse Move 事件報告的上一個位置,用於計算偏移量。
5.MainWindow 構造函數創建主窗口,注冊各種事件處理函數。
C#
public MainWindow()
{
InitializeComponent();
//Enable stylus events and load pictures
this.Loaded += (s, e) => { LoadPictures(); };
//Register for mouse events
MouseLeftButtonDown += ProcessDown;
MouseMove += ProcessMove;
MouseLeftButtonUp += ProcessUp;
MouseWheel += ProcessMouseWheel;
}
Visual Basic
Public Sub New()
InitializeComponent()
End Sub
注意: 在 Visual Basic 中,事件處理注冊在事件處理程序聲明中定義,使用 Handles 關鍵字。
6.LoadPictures() 函數從用戶的圖片文件夾加載圖片,並為所有圖片創 建一個 Picture 控件。它只在初始化畫布之後才執行此操作。下面看一下 LoadPictures() 代碼。
7.現在看一下如何處理鼠標事件。
C#
private void ProcessDown(object sender, MouseButtonEventArgs args)
{
_prevLocation = args.GetPosition(_canvas);
_picture = FindPicture (_prevMouseLocation);
BringPictureToFront(_picture);
}
Visual Basic
Private Sub ProcessDown (ByVal sender As Object, ByVal args As MouseButtonEventArgs) Handles Me.MouseLeftButtonDown
_prevLocation = args.GetPosition(_canvas)
_picture = FindPicture (_prevLocation)
BringPictureToFront(_picture)
End Sub
按下鼠標左鍵將啟動一個新的圖片拖動會話。首先我們必須獲 得相對於畫布的指針位置。我們將此信息保存在 _prevLocation 數據成員中。
8.下一步是在該位置找到一張圖片。FindPicture() 函數利用 WPF VisualTree 點擊測試功能來找到最頂層的圖片。如果鼠標所在位置沒有圖片,則 返回空值。
9.BringPictureToFront() 將所選圖片的 Z 軸次序設置在其 他圖片的最頂層。
此處理程序的處理結果是 _picture 數據成員“記住”了所選的圖 片,_prevLocation 獲取鼠標位置的代碼片段。我們看一下當鼠標移動時會發生 什麼情況:
C#
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;
}
Visual Basic
Private Sub ProcessMove(ByVal sender As Object, ByVal args As MouseEventArgs) Handles Me.MouseMove
If args.LeftButton = MouseButtonState.Released OrElse _picture Is Nothing Then Return
Dim newLocation = args.GetPosition (_canvas)
_picture.X += newLocation.X - _prevLocation.X
_picture.Y += newLocation.Y - _prevLocation.Y
_prevLocation = newLocation
End Sub
如果用戶未按下鼠標左鍵或者未選擇任何圖片,該函數將不執 行任何操作。否則,該函數將計算平移量並更新圖片的 X 和 Y 屬性。它還將更 新 _prevLocation。
10.我們需要注意的最後一個函數是 ProcessMouseWheel:
C#
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;
}
Visual Basic
Private Sub ProcessMouseWheel(ByVal sender As Object, ByVal args As MouseWheelEventArgs) Handles Me.MouseWheel
Dim location = args.GetPosition(_canvas)
Dim picture = FindPicture(location)
If picture Is Nothing Then Return
BringPictureToFront(picture)
Dim scalingFactor = 1 + args.Delta / 1000.0
picture.ScaleX *= scalingFactor
picture.ScaleY *= scalingFactor
End Sub
此函數獲取鼠標指針位置,找到該位置下的圖片,將其呈現在 最前面。然後它從鼠標滾輪偏移量中得到偏移系數。最後只需更新圖片縮放比例 。
任務 2 – 測試多點觸摸硬件是否存在及其就緒情況
在本 任務中,我們將開始編寫多點觸摸程序。盡管 WPF 3.5 不支持多點觸摸(多點觸 摸事件和控件將包含在 WPF 4.0 中),但可以通過某種方式來在當前版本中使用 多點觸摸。為此,我們必須使用 Windows 7 Integration Library 示例。此集成 庫是一個示例,演示了如何在 .NET 代碼中使用 Win32 本機 API。
注意 : 可以從網址 http://code.msdn.microsoft.com/Project/Download/FileDownload.aspx? ProjectName=WindowsTouch&DownloadId=5038 獲取 Windows 7 Integration Library 示例。為了簡單起見,這些庫在 % TrainingKitInstallDir%\MultiTouch\Assets\Win7LibSample 下以實驗資源的形 式提供,請選擇您想要使用的語言(C# 或 VB)。
1.添加對 Windows7.Multitouch.dll 和 Windows7.Multitouch.WPF.dll 的引用。
2.將以下代碼添加到 MainWindow 構造函數中:
(代碼片段 – MultiTouch – IsMultiTouchReady CSharp)
C#
if (! Windows7.Multitouch.TouchHandler.DigitizerCapabilities.IsMultiTouchRead y)
{
MessageBox.Show("Multitouch is not availible");
Environment.Exit(1);
}
(代 碼片段 – MultiTouch – IsMultiTouchReady VB)
Visual Basic
If Not Windows7.Multitouch.TouchHandler.DigitizerCapabilities.IsMultiTouchRead y Then
MsgBox("Multitouch is not availible")
Environment.Exit(1)
End If
3.查看 TouchHandler.DigitizerCapabilities 的其他屬性。
圖 2
查看 TouchHandler.DigitizerCapabilities 屬性
任 務 3 – 將鼠標事件替換為觸摸事件
在本練習中,我們將刪除鼠標 事件並將其替換為觸摸事件,以便使用我們的手指處理圖片。
1.將以下代 碼行添加到 MainWindow.xaml.cs 文件 (C#) 或 MainWindow.xaml.vb 文件 (Visual Basic) 開頭:
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 事件處理程序中添加對此函數的調用:
C#
public MainWindow()
{
...
//Enable stylus events and load pictures
this.Loaded += (s, e) => { Factory.EnableStylusEvents(this); LoadPictures(); };
...
Visual Basic
Private Sub Window_OnLoaded() Handles Me.Loaded
Factory.EnableStylusEvents(Me)
LoadPictures()
End Sub
3.刪除 ProcessMouseWheel 事件處理程序及相應的事件注冊 (我們將在稍後處理縮放)。
4.(僅適用於 C# 用戶)刪除 MouseLeftButtonDown、MouseMove 和 MouseLeftButtonUp 的事件注冊代碼。 MainWindow 構造函數應該類似於以下代碼:
C#
public MainWindow()
{
InitializeComponent();
if (! Windows7.Multitouch.TouchHandler.DigitizerCapabilities.IsMultiTouchRead y)
{
MessageBox.Show("Multitouch is not availible");
Environment.Exit(1);
}
this.Loaded += (s, e) => { Factory.EnableStylusEvents(this); LoadPictures(); };
}
5.更改以下事件處理程序的簽名和代碼:
注意:此事件處理程序的簽 名已經更改。我們使用StylusEventArgs 代替與鼠標相關的事件參數。
( 代碼片段 – MultiTouch – StylusEventHandlers CSharp)
C#
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;
}
(代碼 片段 – MultiTouch – StylusEventHandlers VB)
Visual Basic
Public Sub ProcessDown(ByVal sender As Object, ByVal args As StylusEventArgs)
_prevLocation = args.GetPosition(_canvas)
_picture = FindPicture (_prevLocation)
BringPictureToFront(_picture)
End Sub
Public Sub ProcessMove(ByVal sender As Object, ByVal args As StylusEventArgs)
If _picture Is Nothing Then Return
Dim newLocation = args.GetPosition(_canvas)
_picture.X += newLocation.X - _prevLocation.X
_picture.Y += newLocation.Y - _prevLocation.Y
_prevLocation = newLocation
End Sub
Public Sub ProcessUp(ByVal sender As Object, ByVal args As StylusEventArgs)
_picture = Nothing
End Sub
6.注冊觸筆事件。
C#
public MainWindow()
{
...
//Register for stylus (touch) events
StylusDown += ProcessDown;
StylusUp += ProcessUp;
StylusMove += ProcessMove;
}
Visual Basic
Public Sub ProcessDown(ByVal sender As Object, ByVal args As StylusEventArgs) Handles Me.StylusDown
...
End Sub
Public Sub ProcessMove(ByVal sender As Object, ByVal args As StylusEventArgs) Handles Me.StylusMove
...
End Sub
Public Sub ProcessUp(ByVal sender As Object, ByVal args As StylusEventArgs) Handles Me.StylusUp
...
End Sub
7.編譯並運行。使用手指代替鼠標!
注意: 如果嘗試使用多個手指會發生什麼情況?為什麼?
任務 4 – 同時處理多張圖片
在本任務中,我們將添加多點觸摸支持。觸摸 屏幕的每個手指都會獲得一個唯一的觸摸 ID。只要這根手指繼續觸摸屏幕,系統 就會將相同的觸摸 ID 與該手指關聯。當手指離開屏幕表面時,該觸摸 ID 將被 系統釋放並可被硬件再次使用。在我們的示例中,當一根手指觸摸圖片時,應該 將該手指的唯一觸摸 ID 與該圖片關聯,直到該手指離開屏幕。如果兩個或更多 手指同時觸摸屏幕,那麼每個手指都可以操作相關的圖片。
當使用 Stylus 事件作為觸摸事件時,可以從 Stylus 事件參數中提取出觸摸 ID:
C# | Visual Basic
args.StylusDevice.Id
WPF 將使用相關的 StylusDevice.Id(觸摸 ID)不斷為每個觸摸屏幕的手指 觸發事件。
1.我們需要同時跟蹤多張圖片。對於每張圖片,觸摸 ID、上 一個位置與圖片用戶控件之間必須保持關聯。我們將首先添加一個新的 PictureTracker 類:
注意:PictureTracker 類也在 % TrainingKitInstallDir%\MultiTouch\Assets\PictureHandling下以實驗資源的 形式提供,請選擇您想要使用的語言(C# 或 VB)。
(代碼片段 – MultiTouch – PictureTrackerClass CSharp)
C#
/// <summary>
/// Track a single picture
/// </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)
{
//Do Nothing, We might have another touch-id that is
//still down
}
}
(代碼片段 – MultiTouch – PictureTrackerClass VB)
Visual Basic
''' <summary>
''' Track a single picture.
''' </summary>
Imports System.Windows
Class PictureTracker
Private _prevLocation As Point
Private _picture As Picture
Public Property Picture() As Picture
Get
Return _picture
End Get
Set(ByVal value As Picture)
_picture = value
End Set
End Property
Public Sub ProcessDown(ByVal location As Point)
_prevLocation = location
End Sub
Public Sub ProcessMove(ByVal location As Point)
Picture.X += location.X - _prevLocation.X
Picture.Y += location.Y - _prevLocation.Y
_prevLocation = location
End Sub
Public Sub ProcessUp(ByVal location As Point)
' Do Nothing, We might have another touch-id that is.
' Still down.
End Sub
End Class
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
C#
private readonly Dictionary<int, PictureTracker> _pictureTrackerMap
Visual Basic
Private ReadOnly _pictureTrackerMap As Dictionary(Of Integer, PictureTracker)
b.它必須使用 VisualTree 點擊測試或通過在映射中查找來找到 PictureTracker
c.它必須將事件轉發給正確的 PictureTracker
4. 添加以下 PictureTrackerManager 類:
注意:PictureTrackerManager 類也以實驗資產的形式在 %TrainingKitInstallDir% \MultiTouch\Assets\PictureHandling 下提供,請選擇您想要使用的語言(C# 或 VB)。
(代碼片段 – MultiTouch – PictureTrackerManagerClass CSharp)
C#
class PictureTrackerManager
{
//Map between touch ids and picture trackers
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;
//See if we already track the picture with the touchId
if (_pictureTrackerMap.TryGetValue(touchId, out pictureTracker))
return pictureTracker;
//Get the picture under the touch location
Picture picture = FindPicture(location);
if (picture == null)
return null;
//See if we track the picture with other ID
pictureTracker = (from KeyValuePair<int, PictureTracker> entry in _pictureTrackerMap
where entry.Value.Picture == picture
select entry.Value).FirstOrDefault();
//First time
if (pictureTracker == null)
{
//create new
pictureTracker = new PictureTracker();
pictureTracker.Picture = picture;
BringPictureToFront(picture);
}
//remember the corelation between the touch id and the picture
_pictureTrackerMap[touchId] = pictureTracker;
return pictureTracker;
}
/// <summary>
/// Find the picture in the touch location
/// </summary>
/// <param name="pointF">touch location</param>
/// <returns>The picture or null if no picture exists in the touch
/// location</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);
}
}