多点触摸之WM_TOUCH - 本机(动手实验)
Windows 7 MultiTouch – WM_TOUCH
Windows 7 使用户无需使用中间设备,通过手指触摸方式就能够管理应用程序。这扩展了平板电脑基于触笔的功能。与其他指点设备不同,这种新功能支持在不同指点位置上同时发生多个输入事件,支持复杂的场景,比如通过十指或由多个并行用户管理应用程序。然而,要成功实现此功能,我们必须调整应用程序的用户界面和行为,以支持这种新的输入模式。
目标
在本动手实验中,我们将学习如何管理手势事件,包括:
• 理解使用手势事件操作对象的含义
• 检查多点触摸硬件是否存在及其就绪情况
• 从手势 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 与触摸输入相关联。
祝您实验愉快!