多点触摸之手势 - 本机(动手实验)
Windows 7 多点触摸 – 手势
Windows 7 使用户无需中间设备,通过手指触摸方式就能够管理应用程序。这扩展了平板电脑基于触笔的功能。与其他指点设备不同,这种新功能支持在不同指点位置上同时发生多个输入事件,支持复杂的场景,比如通过十指或由多个并行用户管理应用程序。然而,要成功实现此功能,我们必须调整应用程序的用户界面和行为,以支持这种新的输入模式。
目标
在本动手实验中,我们将学习如何管理手势事件,包括:
• 理解使用手势事件操作对象的含义
• 检查多点触摸硬件是否存在及其就绪情况
• 从手势 Windows Message 中提取信息
系统要求
要完成本实验,必须拥有以下工具:
• Microsoft Visual Studio 2008 SP1
• Windows 7
• Windows 7 SDK
• 一台多点触摸硬件设备
引言
要创建多点触摸驱动的应用程序,有 3 种方法可供选择:“好”、“出色”或“最佳”方法。
“好”方法是这些方法中最简单的。设计应用程序用户界面时应该将触摸能力考虑在内。可以使用大量基于 Win32 的简单工具构建一种自然的界面,以提供更出色的用户体验。滚动等触摸能力来自于 Win32 控件,无需额外的工作。例如,现在尝试使用手指滚动您正在阅读的文档!这就是“好”方法。
“最佳”方法是读取低级触摸事件,将其作为应用程序的输入。“Piano”等应用程序或可供用户同时操作的多个滑块等复杂控件都是不错的例子。运行 MS Paint,从工具箱中选择一个绘制工具,然后使用您的 4 根手指进行绘制(如果硬件支持):
在本动手实验中,我们将使用“出色”方法。“出色”方法是为应用程序获取触摸事件的最简单方式,可用于自定义缩放、旋转和平移等操作,无需读取和操作原始的触摸事件。让我们来立即体验多点触摸手势!
关于 Multitouch Gesture 应用程序
Multitouch Gesture 应用程序提供了一个简单的窗口,其中包含一个矩形。在 HOL 文件夹中,可以找到本实验的每项任务所对应的项目。Starter 文件夹包含实验需要的文件。本实验的完成版本位于 Final 文件夹中。
此应用程序通过与一个着色的矩形交互来响应多点触摸手势输入。该矩形能够响应以下手势:
平移
要平移图像,将两个手指放在应用程序窗口中并朝着希望移动的方向拖动。确保在两个手指之间保留少量空间,以便触摸界面能够将它们看作独立的接触点。
旋转
使用两个手指触摸矩形,然后旋转手指形成一个圆。
缩放
使用两个手指触摸矩形,然后让两个手指相互靠近或远离。
双指点击
使用两个手指同时点击一次,可以在红色矩形中显示或隐藏对角线。
双指交替点击
首先使用一个手指按住矩形,然后使用另一个手指点击矩形,再移开第一个手指,这样可以更改矩形的颜色。
练习 #1 :开发多点触摸应用程序
任务 1 – 创建 Win32 应用程序
1. 启动 Visual Studio 2008 SP1
2. 选择一个新的 C++ Win32 应用程序项目:
3. 编译并运行
4. 我们将使用属于 Windows 7 的 API 和宏。
a. 在 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 available", L"Error", MB_OK);
return 1;
}
BYTE nInputs = (BYTE)GetSystemMetrics(SM_MAXIMUMTOUCHES);
wsprintf(szTitle, L"%s - %d touch inputs", szTitle, nInputs);
2. 可以看到,除了检查触摸功能可用性和就绪情况,我们还会找到硬件支持的触摸输入数量。
3. 编译并运行
任务 3 – 将绘制对象源和头文件添加到项目,然后绘制矩形
在 Starter 文件夹中可以找到两个文件:DrawingObject.h 和 DrawingObject.cpp。将它们复制到项目文件夹,使用“Add Existing item…”将它们添加到项目中。
1. 在 mtGesture.cpp 文件开头添加一行:#include DrawingObject.h
C++
#include "DrawingObject.h"
2. 在 mtGesture.cpp 文件开头的 //Global Variables: 部分添加一个全局变量定义:
C++
CDrawingObject g_drawingObject;
3. 将以下行添加到 WndProc 中,注意 WM_already PAINT 已经由应用程序向导创建:
C++
case WM_SIZE:
g_drawingObject.ResetObject(LOWORD(lParam), HIWORD(lParam));
break;
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
g_drawingObject.Paint(hdc);
EndPaint(hWnd, &ps);
break;
4. 编译并运行
5. 更改 Windows 的大小以触发发送给应用程序的 WM-Paint 消息。窗口中央应该出现一个红色矩形:
任务 4 – 立即开始触摸!
可以开始使用应用程序了。默认情况下,启用了触摸功能的系统会向目标窗口提供 WM_GESTURE 消息。这有点类似于鼠标和键盘消息。系统使用低级触摸输入事件并计算生成的手势。使用 lParam 参数作为句柄来提取手势信息。GetGestureInfo() API 获取 lParam 手势句柄和 GESTUREINFO 结构变量的一个地址:
C++(摘自 WinUser.h)
typedef struct tagGESTUREINFO {
UINT cbSize; // size, in bytes, (including variable length Args field)
DWORD dwFlags; // see GF_* flags
DWORD dwID; // gesture ID, see GID_* defines
HWND hwndTarget; // handle to window targeted by this gesture
POINTS ptsLocation; // current location of this gesture
DWORD dwInstanceID; // internally used
DWORD dwSequenceID; // internally used
ULONGLONG ullArguments; // arguments for gestures (8 BYTES)
UINT cbExtraArgs; // size, of extra arguments, that accompany this gesture
} GESTUREINFO, *PGESTUREINFO;
typedef GESTUREINFO const * PCGESTUREINFO;
/*
* Gesture flags - GESTUREINFO.dwFlags
*/
#define GF_BEGIN 0x00000001
#define GF_INERTIA 0x00000002
#define GF_END 0x00000004
/*
* Gesture IDs
*/
#define GID_BEGIN 1
#define GID_END 2
#define GID_ZOOM 3
#define GID_PAN 4
#define GID_ROTATE 5
#define GID_TWOFINGERTAP 6
#define GID_PRESSANDTAP 7
#define GID_ROLLOVER GID_PRESSANDTAP
使用了通过调用 GetGestureInfo() 获得的信息之后,我们必须调用 CloseGestureInfoHandle() 来释放系统分配的内存块。
当响应手势消息时,dwFlags 和 dwID 两个字段非常重要。dwID 告诉我们用户执行了何种手势:缩放、平移、旋转等。dwFlags 告诉我们这是它第一次通知我们该手势,还是最后一次,或者是否用户已从屏幕移开了手指,但惯性引擎仍在发出手势消息。有许多简单手势(比如“双指点击”),应用程序对它们只需响应一次;其他手势稍微复杂一些,因为它们在用户操作期间发送了许多消息。对于这类手势(缩放、旋转和平移),我们需要根据不同的条件采取不同的响应方式。对于平移和缩放,我们不会对一系列手势中的第一个采取任何操作。表示旋转开始的消息附带一个旋转角度,我们需要根据该角度来旋转绘制对象。只要一个手势不是一系列手势中的第一个,我们就要计算上一个参数与新参数之间的差值,并提取缩放系数、平移范围或相对旋转角度。通过这种方式,我们可以在用户使用手指触摸屏幕并移动手指时更新应用程序。
1. 让我们来移动对象!将以下代码添加到 WndProc 中:
C++
case WM_GESTURE:
return ProcessGestureMessage(hWnd, wParam, lParam);
2. 将以下函数添加到 WndProc 之前:
C++
LRESULT ProcessGestureMessage(HWND hWnd, WPARAM wParam, LPARAM lParam)
{
static POINT lastPoint;
static ULONGLONG lastArguments;
GESTUREINFO gi;
gi.cbSize = sizeof(GESTUREINFO);
gi.dwFlags = 0;
gi.ptsLocation.x = 0;
gi.ptsLocation.y = 0;
gi.dwID = 0;
gi.dwInstanceID = 0;
gi.dwSequenceID = 0;
BOOL bResult = GetGestureInfo((HGESTUREINFO)lParam, &gi);
switch(gi.dwID)
{
case GID_PAN:
if ((gi.dwFlags & GF_BEGIN) == 0) //not the first message
{
g_drawingObject.Move(gi.ptsLocation.x - lastPoint.x,
gi.ptsLocation.y - lastPoint.y);
InvalidateRect(hWnd, NULL, TRUE);
}
}
//Remember last values for delta calculations
lastPoint.x = gi.ptsLocation.x;
lastPoint.y = gi.ptsLocation.y;
lastArguments = gi.ullArguments;
CloseGestureInfoHandle((HGESTUREINFO)lParam);
return 0;
}
3. 编译并运行
4. 尝试使用两个手指移动对象,可以看到,对象在跟随手指移动。您应该了解以下事实:
a. 触摸位置由屏幕坐标确定。我们感兴趣的是偏移量,所以我们并不关心精确的位置坐标,如果我们需要了解精确位置来进行调整,就要调用 ScreenToClient()。
b. 尝试在不触摸对象的情况下移动它,触摸窗口的空白区域。它移动了!我们没有执行“点击测试”来检查触摸位置是否在对象内部。执行点击测试需要窗口中的位置坐标。
5. 完成应用程序并响应所有手势 ID:
C++
LRESULT ProcessGestureMessage(HWND hWnd, WPARAM wParam, LPARAM lParam)
{
static POINT lastPoint;
static ULONGLONG lastArguments;
GESTUREINFO gi;
gi.cbSize = sizeof(GESTUREINFO);
gi.dwFlags = 0;
gi.ptsLocation.x = 0;
gi.ptsLocation.y = 0;
gi.dwID = 0;
gi.dwInstanceID = 0;
gi.dwSequenceID = 0;
BOOL bResult = GetGestureInfo((HGESTUREINFO)lParam, &gi);
switch(gi.dwID)
{
case GID_PAN:
if ((gi.dwFlags & GF_BEGIN) == 0) //not the first message
{
g_drawingObject.Move(gi.ptsLocation.x - lastPoint.x,
gi.ptsLocation.y - lastPoint.y);
// repaint the rect
InvalidateRect(hWnd, NULL, TRUE);
}
break;
case GID_ZOOM:
//not the first message
if ((gi.dwFlags & GF_BEGIN) == 0 && lastArguments != 0)
{
double zoomFactor = (double)gi.ullArguments/lastArguments;
// Now we process zooming in/out of object
g_drawingObject.Zoom(zoomFactor, gi.ptsLocation.x, gi.ptsLocation.y);
InvalidateRect(hWnd,NULL,TRUE);
}
break;
case GID_ROTATE:
{
//The angle is the rotation delta from the last message,
//or from 0 if it is a new message
ULONGLONG lastAngle = ((gi.dwFlags & GF_BEGIN) != 0) ? 0 : lastArguments
int angle = (int)(gi.ullArguments – lastAngle);
g_drawingObject.Rotate(
GID_ROTATE_ANGLE_FROM_ARGUMENT(angle),
gi.ptsLocation.x, gi.ptsLocation.y);
InvalidateRect (hWnd, NULL, TRUE);
}
break;
case GID_PRESSANDTAP:
g_drawingObject.ShiftColor();
InvalidateRect(hWnd, NULL, TRUE);
break;
case GID_TWOFINGERTAP:
g_drawingObject.TogleDrawDiagonals();
InvalidateRect(hWnd, NULL, TRUE);
break;
}
//Remember last values for delta calculations
lastPoint.x = gi.ptsLocation.x;
lastPoint.y = gi.ptsLocation.y;
lastArguments = gi.ullArguments;
CloseGestureInfoHandle((HGESTUREINFO)lParam);
return 0;
}
6. 编译并运行
7. 测试所有手势。
任务 5 – 存在一个 Bug !
1. 尝试旋转对象。结果如何?根据定义,窗口将接收除旋转之外的所有手势。我们可以配置触摸引擎来仅提供可用的手势或所有手势。
2. 将以下代码添加到 InitInstance() 函数中的 ShowWindow() 调用之前,以启用包括 GID_ROTATE 在内的所有手势类型:
C++
//Enable all gesture types
GESTURECONFIG gestureConfig;
gestureConfig.dwID = 0;
gestureConfig.dwBlock = 0;
gestureConfig.dwWant = GC_ALLGESTURES;
BOOL result = SetGestureConfig(hWnd, 0, 1, &gestureConfig,
sizeof(gestureConfig));
3. 编译并运行
4. 尝试旋转对象。大功告成!
小结
在本实验中,我们学习了如何使用触摸手势消息来操作屏幕上的对象。您了解了如何检查是否存在多点触摸硬件;如何配置一个窗口来获得想要的手势事件;以及如何提取和操作手势参数。
祝您实验愉快!