目標
在本動手實驗中,我們將學習如何管理手勢事件,包括:
• 理解使用手勢事件操作對象的含義
• 檢查多點觸摸 硬件是否存在及其就緒情況
• 從手勢 Windows Message 中提取信息
系統要求
要完成本實驗,必須擁有以下工具:
• Microsoft Visual Studio 2008 SP1
• Windows 7
• Windows 7 SDK
• 一台多點觸摸硬件設備
引言
要創建多點觸摸驅動的應用程序,有 3 種方法可供 選擇:“好”、“出色”或“最佳”方法。
“好”方法是這些方法中最簡單的。設計應用程序用戶界面時 應該將觸摸能力考慮在內。可以使用大量基於 Win32 的簡單工具構建一種自然的 界面,以提供更出色的用戶體驗。滾動等觸摸能力來自於 Win32 控件,無需額外 的工作。例如,現在嘗試使用手指滾動您正在閱讀的文檔!這就是“好 ”方法。“出色”方法讓系統接收各種低級觸摸事件,並獲取系 統對這些事件的探索結果。例如,如果用戶在屏幕上做出一個旋轉動作,系統將 發出一個帶有旋轉角度的旋轉手勢。盡管“出色”方法易於使用,但 它具有自身的局限性。使用手勢無法同時實現旋轉、平移和縮放。您也無法同時 處理多個基於不同觸摸方式的操作。例如,兩個用戶操作窗口的不同區域。
“最佳”方法是讀取低級觸摸事件,將其作為應用程序的輸入。 “Piano”等應用程序或可供用戶同時操作的多個滑塊等復雜控件都是 不錯的例子。運行 MS Paint,從工具箱中選擇一個繪制工具,然後使用您的 4 根手指進行繪制(如果硬件支持):
本動手實驗將模仿新的 MS Paint 多點觸摸繪畫特性。我們將使用“最 佳”方法,即讀取並使用原始的觸摸事件,進行低級 WM_TOUCH Multi- touch 消息解碼。
關於 Multi-touch Scratchpad 應用程序
Multi-touch Scratchpad 應用程序提供了一個簡單的窗口,支持使用多 個手指同時繪制連續的線條。在 HOL 文件夾中可以找到本實驗的每項任務所對應 的項目。Starter 文件夾包含實驗需要的文件。本實驗的完成版本位於 Final 文 件夾中。
練習 #1 – 開發多點觸摸應用程序
任務 1 – 創建 Win32 應用程序
1.啟動 Visual Studio 2008 SP1
2.選擇一個新的 C++ Win32 應用程序項目:
3.編譯並運行!
4.我們將使用屬於 Windows 7 的 API 和宏。在 targetver.h 頭文件中將 WINVER 和 _WIN32_WINNT 定義更改為 0x0601
C++
#ifndef WINVER //Specifies that the minimum required platform is Windows 7
#define WINVER 0x0601
#endif
#ifndef _WIN32_WINNT //Specifies that the minimum required platform is Win 7
#define _WIN32_WINNT 0x0601
#endif
5.編譯並 運行!
任務 2 – 檢查多點觸摸硬件是否存在及其就緒情況
1.我們將開發的應用程序需要一個啟用了觸摸功能的設備。在調用 _tWinMain() 中的 InitInstance() 之前,添加以下代碼來檢查硬件的觸摸功能 及就緒情況:
C++
BYTE digitizerStatus = (BYTE) GetSystemMetrics(SM_DIGITIZER);
if ((digitizerStatus & (0x80 + 0x40)) == 0) //Stack Ready + MultiTouch
{
MessageBox(0, L"No touch support is currently availible", L"Error", MB_OK);
return 1;
}
BYTE nInputs = (BYTE)GetSystemMetrics(SM_MAXIMUMTOUCHES);
wsprintf(szTitle, L"%s - %d touch inputs", szTitle, nInputs);
2.可以 看到,除了檢查觸摸功能可用性和就緒情況,我們還會找到硬件支持的觸摸輸入 數量。
3.編譯並運行!
任務 3 – 將筆畫源和頭文件添加到項目中,然後使用手指繪制線條
我們想要使用手指作為多點鼠標設備。我們希望觸摸屏幕的每個手指都能 繪制一條線。為此,我們將使用兩個筆畫集合。一個集合保存完成的筆畫(線) ,另一個集合保存正在繪制的線。觸摸屏幕的每個手指向 g_StrkColDrawing 集 合中的一個筆畫添加點。當手指離開屏幕時,我們將該手指的筆畫從 g_StrkColDrawing 移動到 g_StrkColFinished 集合。在 WM_PAINT 上繪制兩個 集合。
1.在 Starter 文件夾中可以找到兩個文件:Stroke.h 和 Stroke.cpp。將它 們復制到項目文件夾,使用“Add Existing item…” 將它們添加到項目中。
2.在 MTScratchpadWMTouch.cpp 文件開頭添加一 行:#include "Stroke.h"
C++
#include "Stroke.h"
3.在 mtGesture.cpp 文件開頭的 //Global Variables: 部分添加一個全局變量定義:
C++
CStrokeCollection g_StrkColFinished; // The user finished entering strokes.
// The user lifted his or her finger.
CStrokeCollection g_StrkColDrawing; // The Strokes collection the user is
// currently drawing.
4.將以下行添加到 WndProc() 中,注意 WM_already PAINT 已經由應用程序向導創建:
C++
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
// Full redraw: draw complete collection of finished strokes and
// also all the strokes that are currently in drawing.
g_StrkColFinished.Draw(hdc);
g_StrkColDrawing.Draw (hdc);
EndPaint(hWnd, &ps);
break;
5.現 在可以啟用 WM_TOUCH 消息了。默認情況下,窗口會接收 WM_GESTURE 消息。要 切換到低級 WM_TOUCH 消息,需要調用 RegisterTouchWindow() API。將以下代 碼添加到 InitInstance() 函數中 ShowWindow() 調用之前:
C++
// Register the application window for receiving multi-touch input.
if (!RegisterTouchWindow(hWnd, 0))
{
MessageBox(hWnd,
L"Cannot register application window for touch input", L"Error", MB_OK);
return FALSE;
}
6.我們要求 Windows 發送 WM_TOUCH 消息。WM_TOUCH 消息比較特殊。除非要求系統不收集消息中的多 點觸摸事件(參見 TWF_FINETOUCH 參數),否則您將得到消息中的所有觸摸點。 這是合理的,因為用戶在同時通過多個觸摸點觸摸屏幕。將以下行添加到 WndProc() 函數中:
C++
case WM_TOUCH:
{
// A WM_TOUCH message can contain several messages from different contacts
// packed together.
unsigned int numInputs = (int) wParam; //Number of actual contact messages
TOUCHINPUT* ti = new TOUCHINPUT[numInputs]; // Allocate the storage for
//the parameters of the per-
//contact messages
// Unpack message parameters into the array of TOUCHINPUT structures, each
// representing a message for one single contact.
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;
7.wParam 保存 WM_TOUCH 消息中包含的觸摸輸入數量。GetTouchInputInfo() API 使用每個觸摸點的觸摸信息填充 TOUCHINPUT 數組。從 TOUCHINPUT 數組提 取完數據之後,您需要調用 CloseTouchInputHandle() 來釋放系統資源。下面是 TOUCHINPUT 結構的定義:
C++ (Extracted from WinUser.h)
typedef struct tagTOUCHINPUT {
LONG x;
LONG y;
HANDLE hSource;
DWORD dwID;
DWORD dwFlags;
DWORD dwMask;
DWORD dwTime;
ULONG_PTR dwExtraInfo;
DWORD cxContact;
DWORD cyContact;
} TOUCHINPUT, *PTOUCHINPUT;
typedef TOUCHINPUT const * PCTOUCHINPUT;
/*
* Conversion of touch input coordinates to pixels
*/
#define TOUCH_COORD_TO_PIXEL(l) ((l) / 100)
/*
* Touch input flag values (TOUCHINPUT.dwFlags)
*/
#define TOUCHEVENTF_MOVE 0x0001
#define TOUCHEVENTF_DOWN 0x0002
#define TOUCHEVENTF_UP 0x0004
我們主要 關注 TOUCHINPUT 結構中的 4 個參數:x 和 y 是觸摸位置的屏幕乘以 100 後的 值。這意味著我們需要將每個坐標值除以 100(或使用 TOUCH_COORD_TO_PIXEL() 宏)並調用 ScreenToClient() 來轉換為窗口坐標體系。請注意,如果屏幕設 置為高 DPI(大於 96 DPI),您可能還需要將坐標值除以 96 並乘以當前 DPI。 為了簡單起見,我們在應用程序中跳過這一步。其他兩個參數是 dwID 和 dwFlags。dwFlags 告訴我們觸摸輸入的類型:向下、移動或向上。在我們的應用 程序中,TOUCHEVENTF_DOWN 開始一個新筆畫,TOUCHEVENTF_MOVE 將另一個點添 加到現有筆畫中,而 TOUCHEVENTF_UP 完成一個筆畫並將其移動到 g_StrkColFinished 集合。最後一個參數是 dwID,這是觸摸輸入標識符。但手指 首次觸摸屏幕時,會生成一個與該手指相關聯的唯一觸摸 ID。以後來自該手指的 所有觸摸輸入都具有相同的唯一 ID,直到最後的 TOUCHEVENTF_UP 輸入。當手指 離開屏幕時,該 ID 被釋放並可重用為後來觸摸屏幕的其他手指的唯一 ID。現在 我們來處理觸摸輸入,將以下函數添加到 WndProc() 函數之前:
C++
// Returns color for the newly started stroke.
// in:
// bPrimaryContact flag, whether the contact is the primary contact
// returns:
// COLORREF, color of the stroke
COLORREF GetTouchColor(bool bPrimaryContact)
{
static int s_iCurrColor = 0; // Rotating secondary color index
static COLORREF s_arrColor[] = // Secondary colors array
{
RGB(255, 0, 0), // Red
RGB(0, 255, 0), // Green
RGB(0, 0, 255), // Blue
RGB(0, 255, 255), // Cyan
RGB(255, 0, 255), // Magenta
RGB(255, 255, 0) // Yellow
};
COLORREF color;
if (bPrimaryContact)
{
// The application renders the primary contact in black.
color = RGB(0,0,0); // Black
}
else
{
// Take the current secondary color.
color = s_arrColor[s_iCurrColor];
// Move to the next color in the array.
s_iCurrColor = (s_iCurrColor + 1) %
(sizeof(s_arrColor)/sizeof (s_arrColor[0]));
}
return color;
}
// Extracts contact point in client area coordinates (pixels) from a
// TOUCHINPUT structure.
// in:
// hWnd window handle
// ti TOUCHINPUT structure (info about contact)
// returns:
// POINT with contact coordinates
POINT GetTouchPoint(HWND hWnd, const TOUCHINPUT& ti)
{
POINT pt;
pt.x = TOUCH_COORD_TO_PIXEL (ti.x);
pt.y = TOUCH_COORD_TO_PIXEL(ti.y);
ScreenToClient(hWnd, &pt);
return pt;
}
// Handler for touch-down input.
// in:
// hWnd window handle
// ti TOUCHINPUT structure (info about contact)
void OnTouchDownHandler(HWND hWnd, const TOUCHINPUT& ti)
{
// Create a new stroke, add a point, and assign a color to it.
CStroke strkNew;
POINT p = GetTouchPoint(hWnd, ti);
strkNew.AddPoint(p);
strkNew.SetColor(GetTouchColor((ti.dwFlags & TOUCHEVENTF_PRIMARY) != 0));
strkNew.SetId(ti.dwID);
// Add the new stroke to the collection of strokes being drawn.
g_StrkColDrawing.AddStroke(strkNew);
}
// Handler for touch-move input.
// in:
// hWnd window handle
// ti TOUCHINPUT structure (info about contact)
void OnTouchMoveHandler(HWND hWnd, const TOUCHINPUT& ti)
{
// Find the stroke in the collection of the strokes being drawn.
int iStrk = g_StrkColDrawing.FindStrokeById(ti.dwID);
POINT p = GetTouchPoint(hWnd, ti);
// Add the contact point to the stroke.
g_StrkColDrawing[iStrk].AddPoint (p);
// Partial redraw: redraw only the last line segment.
HDC hDC = GetDC(hWnd);
g_StrkColDrawing[iStrk].DrawLast(hDC);
ReleaseDC(hWnd, hDC);
}
// Handler for touch-up message.
// in:
// hWnd window handle
// ti TOUCHINPUT structure (info about contact)
void OnTouchUpHandler(HWND hWnd, const TOUCHINPUT& ti)
{
// Find the stroke in the collection of the strokes being drawn.
int iStrk = g_StrkColDrawing.FindStrokeById(ti.dwID);
// Add the finished stroke to the collection of finished strokes.
g_StrkColFinished.AddStroke(g_StrkColDrawing[iStrk]);
// Remove finished stroke from the collection of strokes being drawn.
g_StrkColDrawing.RemoveStroke (iStrk);
// Redraw the window.
InvalidateRect(hWnd, NULL, FALSE);
}
8.為了使繪 制的筆畫顯得更有意思,我們為每個唯一 ID 挑選了一種不同的顏色。主觸摸手 指是第一個觸摸屏幕的手指。它是一種特殊的輸入,因為它充當著鼠標指針。我 們為這個手指的 ID 選擇了黑色。編譯應用程序並運行!現在您可以觸摸屏幕了 !
小結
在本實驗中,我們學習了如何使用低級 WM_TOUCH 消息。 您了解了如何檢查是否存在多點觸摸設備;學習了如何配置一個窗口來獲得 WM_TOUCH 消息,如何從消息提取輸入,以及系統如何將觸摸 ID 與觸摸輸入相關 聯。