基於Visual C#2010開發Windows7應用 多點觸摸圖片處理應用程序(1)-同時處理多張圖片...

windows7的觸摸功能開闢了一個電腦的全新世紀。從此,您可以丟掉鼠標和鍵盤,直接用手在屏幕上玩遊戲、用筆來寫字編輯文檔,聊天。

windows7最重要特性之一就是:支持多點觸摸。比爾蓋茨說,不久,鼠標鍵盤會消失。
Windows 7 使用戶無需使用中間設備,通過手指觸摸方式就能夠管理應用程序。與其他指點設備不同,這種新功能支持在不同指點位置上同時發生多個輸入事件,支持複雜的場景,比如通過十指或由多個並行用戶管理應用程序。然而,要成功實現此功能,我們必須調整應用程序的用戶界面和行爲,以支持這種新的輸入模式。
下面我們來看下Windows7下多點觸摸圖片處理應用程序的具體酷炫操作界面:

畫布功能的操作界面

惠普TouchSmart 600評測
多點觸摸--將圖片自由縮放
當我們選擇好一張圖片後,通過紅外線觸控功能使用手指可以將圖片移動到屏幕內的任意位置。再通過多點觸摸功能,使用兩根手指就可以輕鬆的將圖片進行自由的旋轉、縮小和放大。

惠普TouchSmart 600評測
選擇多個圖片進行操作

惠普TouchSmart 600評測

多圖片爲一體時任意移動
還可以同時選擇多個圖片一起操作。在自己想操作的所有圖片範圍內,用手指以一個起點開始畫圈,最後終點要與起點重合,這樣就可以自由觸控被選中的所有圖片,不過此時這些圖片是一體化的,如果想取消全選只需要點擊另外任何一張圖片或者屏幕內的黑色範圍就可取消。

惠普TouchSmart 600評測

選擇並拖拽文件夾內的圖片

惠普TouchSmart 600評測
給圖片加以標籤說明

惠普TouchSmart 600評測
將標籤導入圖片上

在屏幕的下半部分,單擊左側的圖片文件夾,會彈出該文件夾內所有的圖片,通過手指上下移動,可以對所有的圖片進行瀏覽,如果選擇好其中的一張圖片,直接用手指拖拽到操作界面上即可。另外通過屏幕中間最下方右邊的標籤功能,再結合觸摸式鍵盤輸入信息,可以給每一張圖片添加標籤說明,而被添加上標籤的圖片會自動保存到標籤文件夾裏,這樣方便用戶對圖片的統一分配和整理。
好了上面介紹了這麼多關於Windows7的多點觸摸圖片應用程序,下面我們來打造自己的多點觸摸圖片處理應用程序,目標是將一個基於鼠標的簡單圖片操作應用程序升級爲支持多點觸摸的現代應用程序,類似於 Microsoft Surface 行爲。

開發多點觸摸圖片處理應用程序

要理解如何管理多點觸摸輸入,我們首先需要理解如何處理(基於鼠標的)單點輸入。爲此,我們準備了一個基於鼠標的圖片處理應用程序,就是多點觸摸動手實驗初始應用程序。

瞭解解決方案

1. 打開位於 %TrainingKitInstallDir%\MultiTouch\Ex1-PictureHandling\Begin 下的初始解決方案,選擇想要使用的語言C#。

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 。如果用戶按住鼠標左鍵,_picture 成員將擁有當前跟蹤的圖片;否則,它將擁有空值。_prevLocation 是 Mouse Move 事件報告的上一個位置,用於計算偏移量。

5.MainWindow 構造函數創建主窗口,註冊各種事件處理函數。

public MainWindow() { InitializeComponent(); //啓用筆觸事件和加載圖片 this.Loaded += (s, e) => { LoadPictures(); }; //註冊鼠標事件 MouseLeftButtonDown += ProcessDown; MouseMove += ProcessMove; MouseLeftButtonUp += ProcessUp; MouseWheel += ProcessMouseWheel; }

6. LoadPictures() 函數從用戶的圖片文件夾加載圖片,併爲所有圖片創建一個 Picture 控件。它只在初始化畫布之後才執行此操作。下面看一下 LoadPictures() 代碼。

7.現在看一下如何處理鼠標事件。

private void ProcessDown(object sender, MouseButtonEventArgs args) { _prevLocation = args.GetPosition(_canvas); _picture = FindPicture(_prevMouseLocation); BringPictureToFront(_picture); }

按下鼠標左鍵將啓動一個新的圖片拖動會話。首先我們必須獲得相對於畫布的指針位置。我們將此信息保存在 _prevLocation 數據成員中。

8.下一步是在該位置找到一張圖片。FindPicture() 函數利用 WPF VisualTree 點擊測試功能來找到最頂層的圖片。如果鼠標所在位置沒有圖片,則返回空值。

9. BringPictureToFront() 將所選圖片的 Z 軸次序設置在其他圖片的最頂層。

此處理程序的處理結果是 _picture 數據成員「記住」了所選的圖片,_prevLocation 獲取鼠標位置的代碼片段。我們看一下當鼠標移動時會發生什麼情況:

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

如果用戶未按下鼠標左鍵或者未選擇任何圖片,該函數將不執行任何操作。否則,該函數將計算平移量並更新圖片的 X 和 Y 屬性。它還將更新 _prevLocation。

10.我們需要注意的最後一個函數是 ProcessMouseWheel:

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

將鼠標事件替換爲觸摸事件

我們將刪除鼠標事件並將其替換爲觸摸事件,以便使用我們的手指處理圖片。

1.將以下代碼行添加到 MainWindow.xaml.cs 文件 (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 事件處理程序中添加對此函數的調用:

public MainWindow() { ... //啓用筆觸事件和加載圖片 this.Loaded += (s, e) => { Factory.EnableStylusEvents(this); LoadPictures(); }; ...

3.刪除 ProcessMouseWheel 事件處理程序及相應的事件註冊(我們將在稍後處理縮放)。

4.(僅適用於 C# 用戶)刪除 MouseLeftButtonDown、MouseMove 和 MouseLeftButtonUp 的事件註冊代碼。MainWindow 構造函數應該類似於以下代碼:

public MainWindow() { InitializeComponent(); if (!Windows7.Multitouch.TouchHandler.DigitizerCapabilities.IsMultiTouchReady) { MessageBox.Show("Multitouch is not availible"); Environment.Exit(1); } this.Loaded += (s, e) => { Factory.EnableStylusEvents(this); LoadPictures(); }; }

5. 更改以下事件處理程序的簽名和代碼:

注意:此事件處理程序的簽名已經更改。我們使用StylusEventArgs 代替與鼠標相關的事件參數。

(代碼片段 – MultiTouch – StylusEventHandlers CSharp)

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

6. 註冊觸筆事件。

public MainWindow() { ... //註冊(觸摸)事件 StylusDown += ProcessDown; StylusUp += ProcessUp; StylusMove += ProcessMove; }

7. 編譯並運行。使用手指代替鼠標!

注意: 如果嘗試使用多個手指會發生什麼情況?爲什麼?

同時處理多張圖片

在本任務中,我們將添加多點觸摸支持。觸摸屏幕的每個手指都會獲得一個唯一的觸摸 ID。只要這根手指繼續觸摸屏幕,系統就會將相同的觸摸 ID 與該手指關聯。當手指離開屏幕表面時,該觸摸 ID 將被系統釋放並可被硬件再次使用。在我們的示例中,當一根手指觸摸圖片時,應該將該手指的唯一觸摸 ID 與該圖片關聯,直到該手指離開屏幕。如果兩個或更多手指同時觸摸屏幕,那麼每個手指都可以操作相關的圖片。

當使用 Stylus 事件作爲觸摸事件時,可以從 Stylus 事件參數中提取出觸摸 ID:args.StylusDevice.Id

WPF 將使用相關的 StylusDevice.Id(觸摸 ID)不斷爲每個觸摸屏幕的手指觸發事件。

1.我們需要同時跟蹤多張圖片。對於每張圖片,觸摸 ID、上一個位置與圖片用戶控件之間必須保持關聯。我們將首先添加一個新的 PictureTracker 類:

注意:PictureTracker 類也在 %TrainingKitInstallDir%\MultiTouch\Assets\PictureHandling下以實驗資源的形式提供,請選擇您想要使用的語言(C#)。

/// <summary> /// 跟蹤單個圖片 /// </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) { //什麼都不做,可能有另一個觸摸ID仍下跌 } }

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

private readonly Dictionary<int, PictureTracker> _pictureTrackerMap

4.添加以下 PictureTrackerManager 類:

注意:PictureTrackerManager 類也以實驗資產的形式在 %TrainingKitInstallDir%\MultiTouch\Assets\PictureHandling 下提供,

class PictureTrackerManager { //圖片之間的接觸和ID跟蹤 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; //我們已經根據筆觸ID追蹤到了圖片 if (_pictureTrackerMap.TryGetValue(touchId, out pictureTracker)) return pictureTracker; //獲取圖片下的觸摸位置 Picture picture = FindPicture(location); if (picture == null) return null; //我們根據其他ID來追蹤圖片 pictureTracker = (from KeyValuePair<int, PictureTracker> entry in _pictureTrackerMap where entry.Value.Picture == picture select entry.Value).FirstOrDefault(); //第一次 if (pictureTracker == null) { //創建 pictureTracker = new PictureTracker(); pictureTracker.Picture = picture; BringPictureToFront(picture); } //記得接觸ID和圖片之間的相關性實證分析 _pictureTrackerMap[touchId] = pictureTracker; return pictureTracker; } /// <summary> /// 在觸摸位置中找到圖片 /// </summary> /// <param name="pointF">觸摸位置</param> /// <returns>空的圖片或照片,如果沒有在觸摸位置存在</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); } }

5.將以下字段聲明添加到 MainWindow 類的開頭:

private readonly PictureTrackerManager _pictureTrackerManager;

6. 修改 MainWindow 構造函數:

a.在調用 InitializeComponent() 之後,添加管理器初始化:

_pictureTrackerManager = new PictureTrackerManager(_canvas);

b. 更改觸筆事件註冊代碼

//Register for stylus (touch) events StylusDown += _pictureTrackerManager.ProcessDown; StylusUp += _pictureTrackerManager.ProcessUp; StylusMove += _pictureTrackerManager.ProcessMove;

7. 從 MainWindow 類刪除 ProcessDown、ProcessMove 和 ProcessUp 事件處理程序。這裏將不再需要它們,因爲它們現在已包含在 PictureTrackerManager 類中。

8. 編譯並運行。嘗試同時抓取多張圖片。嘗試使用多個手指抓取一張圖片。發生了什麼情況?爲什麼?

<本內容未完待續...>