共用方式為


深入 Windows 7

Windows 7 中的多點觸控功能

Yochay Kiriaty

本文討論重點如下:
  • 多點觸控程式設計模型
  • 筆勢
  • 原始觸控訊息
本文使用下列技術:
Windows 7

本文內容是以 Windows 7 搶鮮版為基礎,文中詳細資訊有可能變更。

內容

Windows 7 多點觸控簡介
Windows 7 多點觸控平台程式設計模型
處理筆勢
處理 Windows 原始觸控訊息
總結

本文是 Windows 7 系列文章的第三篇。這個系列專注於開發人員可納入應用程式而使得程式在 Windows 7 上更為搶眼的使用者體驗。第一篇討論程式庫。第二篇探討工作列 API。第三篇闡述 Windows 7 中的多點觸控功能。立即下載 Windows 7 發行候選版本可協助您深入了解本文的精義。

Windows 7 多點觸控簡介

在 Windows 7 中,我們運用觸控來輔助 Windows 的體驗,讓觸控成為滑鼠與鍵盤之外另一種與 PC 互動的方式。這些年來,各種多點觸控裝置的大量運用,造就了極獲好評的使用者體驗。因此,在 Windows 7 的核心功能中引進多點觸控支援也是順理成章之事。

採用 Windows 7 多點觸控平台後,您可以更進一步直接與您的電腦互動。例如,您可以直接伸手從 Windows 檔案總管緩緩地捲動圖片,或是以拂掠的方式快速檢視圖片。要特別強調的是,我們並未建立特殊的 Windows 7 多點觸控命令介面。並沒有專供多點觸控裝置使用的特殊 Windows 檔案總管。最簡單的例子就是 Windows 7 工作列捷徑清單。當您使用滑鼠右鍵按一下工作列上的任何圖示時,就可以看到對應的捷徑清單。例如,用滑鼠右鍵按一下 Windows Live Messenger 圖示便會顯示 Live Messenger 的捷徑清單。但是使用多點觸控要如何進行按滑鼠右鍵的功能呢?只需用手指輕觸 Live Messenger 圖示並且把它拖出來即可,如圖 1 所示。

fig01.gif

圖 1 在 Live Messenger 的捷徑清單上使用多點觸控

執行該拖曳筆勢便會顯示 Live Messenger 的捷徑清單。圖 2 中所示,觸控所觸發的捷徑清單會顯示與標準的滑鼠右鍵捷徑清單相同的內容。右邊的影像顯示的是使用觸控觸發的 Live Messenger 捷徑清單。啟用多點觸控的捷徑清單中的項目間距大於左圖 (預設滑鼠鍵捷徑清單) 中的項目間距。

fig01.gif

圖 2 捷徑清單的多點觸控和標準檢視

Windows Live Messenger 只是其中一個例子,說明 Windows 7 不為觸控情況建立新的 UI 組,而是讓它融入現有的基礎結構中。這個工作列只是 Windows 7 所附眾多多點觸控最佳化體驗 (例如 XPS 檢視器、Windows 相片檢視器和 IE8) 的一個例子。

Windows 7 多點觸控平台程式設計模型

為了對所有種類的應用程式提供面面俱到的 Windows 觸控解決方案,Windows 觸控平台提供了多種不同層級的支援。有一些您可以使用 Windows 觸控平台功能來增強應用程式的案例。但是在採用任何特定的方法之前,請先考量您希望您的應用程式執行何種功能。

舊版支援假設您現有的應用程式已擁有大量客戶群。您應該先問問自己,在 Windows 7 具備多點觸控功能的電腦上執行該應用程式時,使用者的多點觸控體驗應該是怎樣?值得慶幸的是,Windows 7 多點觸控平台為不感應觸控、以及非為支援多點觸控設計的應用程式提供了免費、現成的有應用程式體驗。更明確地講,它為幾種基本筆勢的提供免費的現成支援。換句話說,這幾種基本筆勢應該能在您的應用程式中運作並且發生預期的效果。這些基本筆勢包括在 Windows Vista 時間範圍引入的單指或雙指繪圖、雙指縮放及筆勢。

加入多點觸控支援我們在這裡將專門探討加入直接筆勢支援 (和一些其他行為) 及使用者介面變更,讓應用程式比簡單筆勢支援更便於觸控的操作。

在本文一開始介紹過的一個例子,就是觸控最佳化的工作列捷徑清單。這個工作列使用 getMessageExtraInfo 方法來追蹤輸入訊息的來源,並且判斷此訊息是否為觸控訊息,然後再以相對的方式回應。

此外,您可以使用筆勢來增強應用程式並且提供更佳的多點觸控支援。直接回應筆勢的應用程式將完全掌控它在使用者觸碰啟用觸控的裝置時的行為方式。例如,Windows 7 所附的 Windows 相片檢視器。在相片檢視器應用程式中,檢視器會接收有關縮放筆勢來自何處的特定資訊。換言之,縮放筆勢包含了縮放筆勢中心點 (特定 X 和 Y 座標) 的資訊,所以相片檢視器才能將焦點置於這個筆勢的中心。Windows 相片檢視器也會使用移動和旋轉筆勢提供非常棒的影像檢視體驗,而且不用花太多力氣。

您也可以運用筆勢來取代預設的移動行為。例如,預設的觸控捲動原先是針對以垂直捲動為主的文字視窗設計的 (例如網頁或文件),所以水平拖曳執行的是文字選取,而非捲動。在大部分應用程式中,這種運作方式都沒麼問題。但是,萬一您的應用程式需要求援水平捲動,那又該怎麼辦呢?同時,對某些應用程式而言,預設捲動可能會顯得太厚重,移動太快或太慢。您可以利用筆勢支援取代預設的移動行為,並且針對您應用程式的需求進行最佳化。

針對多點觸控最佳化的體驗最理想的情況是應用程式一開始就是以支援多點觸控而設計的。這些應用程式會以 Windows 訊息觸控訊息 (WM_TOUCH) 為基礎建置。這些訊息會提供原始觸控資料給應用程式,然後您就可以自訂這些訊息並且處理多個觸控點。前面所提的大部分筆勢都屬於雙指筆勢,這些筆勢只要配合 WM_TOUCH 訊息就可以接收多個同時觸控點,其數目將視基礎觸控感應硬體支援的數目而定。

Windows 7 多點觸控平台也提供操作和慣性處理器,協助您解譯這些觸控訊息。您可以將此操作想像成一個黑箱,接收的輸入就是所碰觸的物件及所有相關觸控訊息。而結果則是一個 2D 仿射轉換矩陣 (對照表),表示手指移動所產生的轉換。例如,假設您正在撰寫相片編輯應用程式,您可以用手指 (隨你高興要使用幾支手指) 同時抓取兩張相片進行旋轉、調整大小及轉譯相片,而操作處理序會提供您需要的機會來反映在該物件上。

慣性會提供一種非常基本的應用程式物理模型,並且提供繼續物件平順轉換 (即使手指已離開觸控感應裝置) 的簡便方法來建立簡單的轉換效果,而不是讓物件驟然停止在那個點上。

處理筆勢

根據預設,只要使用者碰觸具備觸控感應能力的 Windows 7 裝置,Windows 7 多點觸控平台便會傳送筆勢訊息 (WM_GESTURE) 到您的應用程式。這是免費的既有行為,如果您想停止接數這類訊息,您必須選擇不採用。

筆勢可視為單指或雙指觸控輸入,可轉譯為使用者執行的某種預先定義的動作 (筆勢)。偵測到筆勢之後 (作業系統會為您執行所有的工作),作業系統便會傳送筆勢訊息到您的應用程式。這項訊息包含解碼筆勢和讓它運作的所有資訊。Windows 7 支援下列筆勢:

  • 縮放
  • 單指和雙指移動
  • 旋轉
  • 雙指輕點
  • 按住並輕點

處理 WM_Gesture 訊息若要處理筆勢,您必須處理傳送到應用程式的 WM_GESTURE 訊息。如果您是 Win32 程式設計人員,您可以在應用程式的 WndProc 函式中檢查 WM_GESTURE 訊息。

WM_GESTURE 是使用於所有筆勢的一般訊息。因此,要判斷您需要處理哪種筆勢,就必須先將筆勢訊息解碼。您可以在 lParam 參數中找到筆勢的相關資訊,然後您需要使用一個特殊函式 GetGestureInfo 來解碼筆勢訊息,如下列程式碼片段所示:

GESTUREINFO gi;
ZeroMemory(&gi, sizeof(GESTUREINFO));
gi.cbSize = sizeof(gi);
BOOL bResult = GetGestureInfo((HGESTUREINFO)lParam, &gi);

取得 GESTUREINFO 結構之後,您可以檢查 dwID 來判斷執行的是哪種筆勢。GESTUREINFO 結構包含一些其他的重要成員:

  • cbSize - 此結構的大小,以位元組為單位
  • ptsLocation - 包含筆勢相關座標的 POINTS 結構。這些座標永遠是相對於螢幕的原點
  • dwFlags - 筆勢的狀態,例如開始、慣性和結束
  • ullArguments - 64 位元不帶正負號的整數,包含以 8 個位元組容納的筆勢引數。這是對每種筆勢類型都不相同的額外資訊

有了這些資訊之後,我們就可以繼續撰寫完整的 Switch-Case 方法來處理所有筆勢,如圖 3 所示。

圖 3 Switch-Case 方法

void CMTTestDlg::DecodeGesture(WPARAM wParam, LPARAM lParam)
{
    GESTUREINFO gi; 
    ZeroMemory(&gi, sizeof(GESTUREINFO));
    GetGestureInfo((HGESTUREINFO)lParam, &gi);
    switch (gi.dwID){
        case GID_ZOOM:
            // Code for zooming goes here
            break;
        case GID_PAN:
            break;
        case GID_ROTATE:
            break;
        case GID_TWOFINGERTAP:
            break;
        case GID_PRESSANDTAP:
            break;
        default:
            // You have encountered an unknown gesture
            break;
    CloseGestureInfoHandle((HGESTUREINFO)lParam);
}

請留意,在此函式的結尾我們會呼叫 CloseGestureInfoHandle 函式來關閉與筆勢資訊處理常式關聯的資源。如果您要處理 WM_GESTURE 訊息,就必須使用這個函式來結束處理,否則可能會造成記憶體遺漏。

處理筆勢訊息有固定的流程,包括組態、解碼筆勢訊息,以及依據應用程式的需求處理特定的筆勢。從前面的程式碼可以看出,這種方式並不困難。

現在,讓我們仔細審視一下縮放筆勢,這可以讓您大致了解所有其他筆勢可能會是什麼樣子。

使用縮放筆勢來伸縮物件

縮放筆勢通常被使用者認為是兩個觸控點之間的「夾捏」動作,將兩個手指彼此靠攏就會拉遠,而將兩個手指彼此分開就會拉近並且放大內容。縮放筆勢可以讓您伸縮物件的大小。圖 4 說明縮放筆勢的運作方式。

fig01.gif

圖 4 縮放筆勢

現在,讓我們看看在 GID_ZOOM switch 中要實作怎樣的程式碼才能達成想要的縮放效果。

筆勢資訊架構包含用來判斷筆勢狀態的 dwFlags 成員,而且可以包任下列任何值:

  • GF_BEGIN – 表示筆勢正在開始,在第一個 WM_Gesture 訊息中收到
  • GF_INERTIA – 表示筆勢已觸發慣性
  • GF_END – 表示筆勢已完成
  • Switch 中的 default – 表示其餘的筆勢訊息,通常被稱為 delta

我們將使用 GF_BEGIN 旗標將觸控點的慣性開始座標儲存到變數中當做後續步驟的參考。我們將 ptsLocation 儲存到 _ptFirst 變數中。就縮放筆勢而言,ptsLocation 是表示縮放的中心。

所收到的下列縮放訊息會由 default case 處理。我們將這些座標儲存在 _ptSecond 變數中。接下來,我們要計算縮放中心點、縮放比,最後我們還要更新矩形 (我們的圖形物件) 來反映縮放中心點和縮放比。圖 5 示範這些引數。

圖 5 GID_ZOOM Switch

case GID_ZOOM:
switch(gi.dwFlags)
{
case GF_BEGIN:
    _dwArguments = LODWORD(gi.ullArguments);
    _ptFirst.x = gi.ptsLocation.x;
    _ptFirst.y = gi.ptsLocation.y;
    ScreenToClient(hWnd,&_ptFirst);
    break;
default:
    // We read here the second point of the gesture. This is middle point between fingers. 
    _ptSecond.x = gi.ptsLocation.x;
    _ptSecond.y = gi.ptsLocation.y;
    ScreenToClient(hWnd,&_ptSecond);
    // We have to calculate zoom center point 
    ptZoomCenter.x = (_ptFirst.x + _ptSecond.x)/2;
    ptZoomCenter.y = (_ptFirst.y + _ptSecond.y)/2;           
    
    // The zoom factor is the ratio between the new and the old distance. 
    k = (double)(LODWORD(gi.ullArguments))/(double)(_dwArguments);
    // Now we process zooming in/out of the object
    ProcessZoom(k,ptZoomCenter.x,ptZoomCenter.y);
    InvalidateRect(hWnd,NULL,TRUE);

    // Now we have to store new information as a starting information for the next step
    _ptFirst = _ptSecond;
    _dwArguments = LODWORD(gi.ullArguments);
    break;
}
break;

在 default case 的處理常式中,我們要儲存筆勢的位置,並從兩組點 (代表目前的觸控點和先前的觸控點) 計算縮放中心位置,並且儲存在 ptZoomCenter 中。我們也要藉由計算兩個點之間的比率來計算縮放因素。呼叫 ProcessZoom Helper 函式會更新這個新座標來反映縮放因素和中心點。

處理其餘的 Windows 7 預設筆勢將非常類似上述的特定縮放筆勢處理。所有筆勢都遵循相同的流程,只有內部邏輯實作會依筆勢、依 use-case 的情況而異。接下來,我們要探討最佳化模型,並且深入探索可以讓我們接收和處理原始觸控事件的 API。

處理 Windows 原始觸控訊息

要接收原始觸控訊息 WM_TOUCH,首先必須要求作業系統開始傳送觸控訊息到您的應用程式,並且停止傳送預設筆勢訊息。執行的方式就是呼叫 RegisterTouchWindow(HWND hWnd, ULONG uFlags) 函式。呼叫這個函式會將單一 hWnd 項目 (通常是視窗) 登錄為具備觸控功能。

就如同處理筆勢一樣,您必須在應用程式的 WndProc 函式中處理 WM_TOUCH 訊息。單一 WM_TOUCH 訊息可能包含幾個不同的「觸控點訊息」,必須拆解成觸控輸入結構的陣列。標準的方法是將 WM_TOUCH 訊息拆解成 TOUCHINPUT 結構的陣列,而該陣列中的每一結構就表示來自某個單一觸控點的資料。要執行拆解,必須呼叫 GetTouchInputInfo(HTOUCHINPUT hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) 函式,並且將 WM_TOUCH 訊息的 lParam 及剛建立的觸控點陣列傳遞給它,如圖 6 所示。

圖 6 拆解 WM_TOUCH

case WM_TOUCH:
{
    unsigned int numInputs = (unsigned int) wParam; 
    TOUCHINPUT* ti = new TOUCHINPUT[numInputs]; 
    if(GetTouchInputInfo((HTOUCHINPUT)lParam, numInputs, ti, sizeof(TOUCHINPUT)))
    {
        // Handle each contact point
        for(unsigned int i=0; i< numInputs; ++i)
        {
           /* handle ti[i]  */
        }
    }
    CloseTouchInputHandle((HTOUCHINPUT)lParam);
    delete [] ti;
}
break;
default:
    return DefWindowProc(hWnd, message, wParam, lParam);


 

您可以在這裡看到我們如何使用來自每個觸控點的資料填入 TOUCHPOINT ti 陣列。接下來我要反覆處理這個觸控點陣列,把我們的邏輯套用到每個觸控點 (處理 ti[i] 註解)。最後,我們必須呼叫 CloseTouchInputHandle(HTOUCHINPUT hTouchInput),傳遞原來 WinProc 的 lParam 來清除觸控處理。不這麼做的話,可能會造成記憶體遺漏。

前面的程式碼表示處理 WM_TOUCH 訊息的第一個步驟。單一觸控輸入結構 TOUCHINPUT 包含了您需要處理的單一觸控點的所有必要資訊:

  • dwID – 是觸控點識別項,用來區別特定的觸控點
  • dwFlags – 是一組位元旗標,用來指定觸控點的狀態
  • 觸控點的 XY 座標 (基本上就是每個觸控點的位置)
  • dwTime – 是事件的時間步驟,以毫秒為單位
  • dwMask – 一組位元旗標,用來指定結構中哪個選擇性欄位包含有效的值

請注意,X 和 Y 座標是以實體螢幕座標像素的百分之幾為單位 (即 centa-pixel)。這種格外精細的解析度提升了高度精確性和準確的硬體辨識,可使用於其他可能需要這類精細解析度的應用程式。但是對於大部分的情況而言,請記得將觸控點 X 和 Y 座標除以一百,將觸控點座標轉譯為可使用的螢幕座標,然後才能開始使用這些座標。

現在,您已經了解如何處理觸控訊息,而且已有足夠資訊為上述 WM_TOUCH 處理常式加上實際的邏輯。現在就讓我們運用這些知識建置一個多點觸控應用程式,也稱為「便條簿」(Scratch Pad)。

追蹤觸控點 ID 若要建立這個「便條簿」應用程式,您必須追蹤每個觸控點的動作和它執行的路徑,然後沿著該路徑繪製一段線條。為了區別不同的觸控點並確保您確實正確處理了每個觸控點,我們要對每個觸控點指定不同的顏色。

將觸控訊息拆解成觸控點輸入結構的陣列 ti 之後,您必須檢查每個觸控點的狀態並且對每一種觸控狀態套用不同的邏輯。在這個「便條簿」範例中,新觸控點是以向下狀態 TOUCHEVENTF_DOWN 識別。您必須登錄這個新觸控點 ID 並且為它指定色彩。一旦觸控點被移除後 (TOUCHEVENTF_UP),您必須完成最後的繪製並且解除登錄這個觸控點 ID。在向下和向上事件之間,您可能會取得許多移動訊息 TOUCHEVENTF_MOVE。對於每一個移動訊息,您必須在現有的線條上加入一個新點,然後繪製此線條的新區段。圖 7 所示為「便條簿」應用程式支援多點觸控所需的整個 WM_TOUCH 處理常式。

圖 7 WM_TOUCH 處理常式

case WM_TOUCH:
{
    unsigned int numInputs = (unsigned int) wParam; 
    TOUCHINPUT* ti = new TOUCHINPUT[numInputs]; 
    if(GetTouchInputInfo((HTOUCHINPUT)lParam, numInputs, ti, sizeof(TOUCHINPUT)))
    {
        // For each contact, dispatch the message to the appropriate message handler.
        for(unsigned int i=0; i< numInputs; ++i)
        {
            if(ti[i].dwFlags & TOUCHEVENTF_DOWN)
            {
               OnTouchDownHandler(hWnd, ti[i]);
            }
            else if(ti[i].dwFlags & TOUCHEVENTF_MOVE)
            {
               OnTouchMoveHandler(hWnd, ti[i]);
            }
            else if(ti[i].dwFlags & TOUCHEVENTF_UP)
            {
               OnTouchUpHandler(hWnd, ti[i]);
            }
        }
    }
    CloseTouchInputHandle((HTOUCHINPUT)lParam);
    delete [] ti;
}
break;
 

追蹤個別觸控點的關鍵就在於使用 dwID,它會在特定觸控筆劃的持續時間內保持不變。在 OnTouchDownHandler 函式中,您必須將這個 ID 指定給 CStroke 物件,此物件基本上就是表示線條的點所構成的陣列。這個線條就是您在觸控感應裝置上拖曳手指路徑形式。我們不打算探討支援此應用程式並實際在螢幕上繪製線條的整個程式碼範例。基本上,支援多點觸控需要做的動作都可以在前面的範例中找到。

您可以在圖 8 中檢視「便條簿」應用程式的輸出。

fig01.gif

圖 8 便條簿應用程式的輸出

在 default case 的處理常式中,我們要儲存筆勢的位置,並從兩組點 (代表目前的觸控點和先前的觸控點) 計算縮放中心位置,並且儲存在 ptZoomCenter 中。我們也要藉由計算兩個點之間的比率來計算縮放因素。呼叫 ProcessZoom Helper 函式會更新這個新座標來反映縮放因素和中心點。

處理其餘的 Windows 7 預設筆勢將非常類似上述的特定縮放筆勢處理。所有筆勢都遵循相同的流程,只有內部邏輯實作會依筆勢、依 use-case 的情況而異。接下來,我們要探討最佳化模型,並且深入探索可以讓我們接收和處理原始觸控事件的 API。

處理 Windows 原始觸控訊息

要接收原始觸控訊息 WM_TOUCH,首先必須要求作業系統開始傳送觸控訊息到您的應用程式,並且停止傳送預設筆勢訊息。執行的方式就是呼叫 RegisterTouchWindow(HWND hWnd, ULONG uFlags) 函式。呼叫這個函式會將單一 hWnd 項目 (通常是視窗) 登錄為具備觸控功能。

就如同處理筆勢一樣,您必須在應用程式的 WndProc 函式中處理 WM_TOUCH 訊息。單一 WM_TOUCH 訊息可能包含幾個不同的「觸控點訊息」,必須拆解成觸控輸入結構的陣列。標準的方法是將 WM_TOUCH 訊息拆解成 TOUCHINPUT 結構的陣列,而該陣列中的每一結構就表示來自某個單一觸控點的資料。要執行拆解,必須呼叫 GetTouchInputInfo(HTOUCHINPUT hTouchInput, UINT cInputs, PTOUCHINPUT pInputs, int cbSize) 函式,並且將 WM_TOUCH 訊息的 lParam 及剛建立的觸控點陣列傳遞給它,如圖 6 所示。

摘要

Windows 7 多點觸控平台是非常強大的開發平台。從實作預設的筆勢支援到進階的原始觸控訊息,它能夠讓您以相對簡單的實作取得許多強大的能力。

此平台也包括操作和慣性處理器。操作在許多方面都很類似筆勢,但是功能更為強大。操作可用來簡化任何指定數目的物件上的轉換作業。您可以在同一時間在特定的物件上執行特定元件筆勢 (例如旋轉、縮放和伸縮) 的組合。操作處理器會產生二維「轉換矩陣」,表示一段時間後觸控點所執行動作在物件上發生的轉換、伸縮變更及旋轉 (以 X 和 Y 座標為單位)。一旦放開最後一個觸控點後,您應該將簡單的物理學應用在此物件,讓它平順地停止,而不是驟然停在那個點上。為了支援這種平順的動作,Windows 7 多點觸控平台提供了慣性 API。

這些 API 將是我們下一篇 MSDN 文章的主題。

Yochay Kiriaty 是 Microsoft 的技術推廣經理,專門負責 Windows 7 方面的事務。他在軟體開發方面已經有十多年的經驗。他曾經撰寫及教授電腦科學的課程,而且目前是 Windows 部落格的活躍參與者。