多点触控编程






4.91/5 (11投票s)
这是一篇关于 MSDN 文章“检测和跟踪多个触控点”的评论
观看此 YouTube 视频,了解此程序的功能...
介绍
我花了很长时间开发一个自定义控件——一个我很久以来就知道可以受益于多点触控界面的控件。然而,实验多点触控似乎是遥远的未来,直到大约一周前,我才拥有了我的第一台触摸屏笔记本电脑(一台 Toshiba C55T-10K - 以很大的折扣购入!)。
所以——我开始着手为我的自定义控件实现多点触控,并寻找实现它的方法。我取得的第一个真正的进展是基于 MSDN 文章...
http://msdn.microsoft.com/en-us/library/windows/desktop/dd744775%28v=vs.85%29.aspx
我认为,这些文章有些粗糙的地方也具有教育意义。像我这样的人会想找出它们是如何工作的。我们打开一个项目,然后复制代码并粘贴。代码可以运行,演示了它应该做的事情,但在可以用于另一个项目之前,还有一些事情需要完成。所以我们玩弄它,琢磨它,直到我们找出可以改变什么,使其更令人满意和/或可用。
对我来说,阅读这篇文章并应用 Tom1omT 和 duggulous 提供的修复方法非常困难,然后又发现一两件仍然需要修复的事情。因此,我写这篇评论(它相当紧密地基于 MSDN 文章)的目的,是希望为那些想开始使用多点触控的其他人提供一个更顺畅的开端。
一旦我让 MSDN 文章中的代码运行起来,我就对其进行了修改,通过消除内存泄漏和更改触摸点信息的存储方式来提高其稳定性。对我来说尤其感兴趣的是,因为我希望将来在自己的自定义控件中实现多点触控功能,所以找到一种清晰可靠的方法来实现触摸点的状态保持对我来说非常重要。在本文的第一个版本中,示例应用程序会根据其他触摸点发生的情况改变屏幕上圆圈的颜色。对于本文的第二个版本,我已更改代码,以便屏幕上按住的触摸点将保持其状态,而不管其他触摸点发生什么。
使用代码
要开始,请创建一个 Win 32 项目。MSDN 文章解释了如何使用 Visual Studio 项目向导工具来做到这一点。接下来的任务是向主 .cpp
文件添加一些全局变量和一个全局函数,并向由向导已生成的 InitInstance
和 WndProc
函数添加一些代码。
文章中描述的第一步是将一些行放入项目的 targetver.h
文件中。这些行会检查软件环境是否一切正常...
#ifndef WINVER // Specifies that the minimum required platform is Windows 7.
#define WINVER 0x0601 // Change this to the appropriate value to target other versions of Windows.
#endif
#ifndef _WIN32_WINNT // Specifies that the minimum required platform is Windows 7.
#define _WIN32_WINNT 0x0601 // Change this to the appropriate value to target other versions of Windows.
#endif
#ifndef _WIN32_WINDOWS // Specifies that the minimum required platform is Windows 98.
#define _WIN32_WINDOWS 0x0410 // Change this to the appropriate value to target Windows Me or later.
#endif
#ifndef _WIN32_IE // Specifies that the minimum required platform is Internet Explorer 7.0.
#define _WIN32_IE 0x0700 // Change this to the appropriate value to target other versions of IE.
#endif
我将这些包含和声明插入到主 .cpp
文件顶部附近...
#include <windows.h> // included for Windows Touch
#include <windowsx.h> // included for point conversion
#define MAXPOINTS 10
// You can make the touch points larger
// by changing this radius value
static int radius = 150;
//State information (ie colour of the circle)
// can be stored and retreived for the touch points.
struct circle{
COLORREF colour;
int sysID;
int pointX;
int pointY;
};
circle circlesArray[MAXPOINTS];
// For status reporting
int touchCount = 0;
int cycleCount = 0;
我添加了一些全局变量用于状态报告——touchCount
和 cycleCount
。
我的代码与 MSDN 代码的其他区别是:
- 我没有使用单独的数组来存储系统触摸点 ID
idLookup
和二维points
数组,而是将所有这些信息以及颜色(以英式拼写为例,以防您好奇)信息放入一个类型为circle
的数组(circlesArray
)中。我认为这样更整洁。 对于原始 MSDN 代码(或接近原始代码的版本),一旦在屏幕上放置了十个触摸点,状态保持就会停止。我稍后会提供有关如何保持状态的更多详细信息。 - 我没有通过一个包含十个固定值的数组来指定要使用的颜色,而是编写了一个函数来为触摸点提供随机颜色值。我将此函数放在
Multitouch.cpp
文件顶部附近。这样做的优点之一是,与 MSDN 代码不同,如果MAXPOINTS
的值增加,就不会有问题。这很容易做到,代码也简洁明了:
// This function makes a random colour value for the circle
COLORREF MakeColour(){
return RGB(rand()%(255),rand()%(255),rand()%(255));
}
原始 MSDN 代码有一个函数,用于返回屏幕上每个触摸点的系统标识符的可用索引。此函数(称为 GetContactIndex
)能够在标识符存储数组中找到系统标识符并返回相应的索引,或者在存在空成员且系统中不存在该标识符的情况下,将其分配给一个空成员。GetContactIndex
的问题在于,一旦分配了 MAXPOINTS
个触摸点,它就无法存储新的触摸点。此外,由于我最终希望将多点触控接口集成到我正在开发的自定义控件中,因此能够可靠地检索反复使用的触摸点相关的状态信息对我来说非常重要。
为了可靠地检索与触摸点相关的状态信息(在此示例中,该状态信息是屏幕上绘制的圆圈的颜色),我...
- 将原始代码中使用的数据结构替换为
circlesArray
(如上所述)。 - 将
GetContactIndex
函数替换为GetCircleIndex
。 - 通过编写一个释放
circlesArray
内存的函数——ReleaseCircleIndex
——启用了应用程序内存的重用,从而实现了无限的触摸点重用。
以下是新函数——GetCircleIndex
和 ReleaseCircleIndex
。同样,我将它们放在 .cpp
文件顶部附近...
// This function is used to return an index given an ID
int GetCircleIndex(int dwID){
for (int i=0; i < MAXPOINTS; i++){
if (circlesArray[i].sysID == dwID){
return i;
}
}
for (int i=0; i < MAXPOINTS; i++){
if (circlesArray[i].sysID == -1){
circlesArray[i].sysID = dwID;
circlesArray[i].colour = MakeColour();
return i;
}
}
// Out of contacts
return -1;
}
// This function is used to release an array member given an ID
void ReleaseCircleIndex(int dwID){
for (int i=0; i < MAXPOINTS; i++){
if (circlesArray[i].sysID == dwID){
circlesArray[i].sysID = -1;
circlesArray[i].pointX = -1;
circlesArray[i].pointY = -1;
}
}
//For aesthetics, these next lines will shuffle the vacant
//array slot to the end of the array. This means that, for
//overlapping circles on the screen, the last touch point
//made will cause a circle to be drawn on top of circles
//representing already existing touch points.
//It might be important to you to know the order in which
//your touch points were placed. If not, this block can be
//taken out.
for (int i=0; i < MAXPOINTS; i++){
if (i<MAXPOINTS-1 && circlesArray[i].sysID == -1){
circlesArray[i] = circlesArray[i+1];
circlesArray[i+1].sysID = -1;
circlesArray[i+1].pointX = -1;
circlesArray[i+1].pointY = -1;
}
}
}
初始化应用程序
我的代码中的 InitInstance 函数如下所示...
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd;
hInst = hInstance; // Store instance handle in our global variable
hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
if (!hWnd) {
return FALSE;
}
// register the window for touch instead of gestures
RegisterTouchWindow(hWnd, 0);
for (int i=0; i < MAXPOINTS; i++){
circlesArray[i].sysID = -1;
circlesArray[i].colour = RGB(0,0,0);
circlesArray[i].pointX = -1;
circlesArray[i].pointY = -1;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
关键部分是调用 RegisterTouchWindow
以及初始化 circlesArray
中所有值的代码块。
所有我们还需要添加到 WndProc
函数中的代码。这个回调函数会处理传递给它的各种窗口消息。我们将添加代码的部分是处理 WM_TOUCH
和 WM_PAINT
的部分。
准备 WndProc
函数
我在 WndProc
函数的开头放置了这些声明...
// For double buffering
static HDC memDC = 0;
static HBITMAP hMemBmp = 0;
HBITMAP hOldBmp = 0;
// For tracking dwId to points
int index;
// For dealing with WM_TOUCH in WndProc
int i, x, y;
UINT cInputs;
PTOUCHINPUT pInputs;
POINT ptInput;
添加 WM_TOUCH
的处理程序
您需要添加 WM_TOUCH
的部分。将此代码块添加到 switch (message) 块中...
case WM_TOUCH:
cInputs = LOWORD(wParam);
pInputs = new TOUCHINPUT[cInputs];
cycleCount++;
if (pInputs){
if (GetTouchInputInfo((HTOUCHINPUT)lParam, cInputs, pInputs, sizeof(TOUCHINPUT))){
for (int i=0; i < static_cast<int>(cInputs); i++){
TOUCHINPUT ti = pInputs[i];
if (ti.dwID != 0){
// Do something with your touch input handle
ptInput.x = TOUCH_COORD_TO_PIXEL(ti.x);
ptInput.y = TOUCH_COORD_TO_PIXEL(ti.y);
ScreenToClient(hWnd, &ptInput);
if (ti.dwFlags & TOUCHEVENTF_UP){
//ReleaseContactIndex(ti.dwID);
ReleaseCircleIndex(ti.dwID);
touchCount++;
}else{
//If the touch point already exists
// - ie. someone has held their finger
//on the screen or dragged it without
//lifting it off then GetCircleIndex
//will return a value by which we can
//retrieve information pertaining to
//that touch point from circlesArray.
//If the touch point doesn't exist then an
//array location will be allocated
//to it, a colour created and stored along
//with its position on the screen
//and that array location is returned.
index = GetCircleIndex(ti.dwID);
circlesArray[index].pointX = ptInput.x;
circlesArray[index].pointY = ptInput.y;
}
}
if (!CloseTouchInputHandle((HTOUCHINPUT)lParam))
{
/*
// error handling
MessageBox(
NULL,
(LPCWSTR)L"!CloseTouchInputHandle",
(LPCWSTR)L"Error",
MB_ICONWARNING | MB_DEFBUTTON2
);
*/
}
}
}
// If you handled the message and don't want anything else done with it, you can close it
CloseTouchInputHandle((HTOUCHINPUT)lParam);
delete [] pInputs;
}else{
// Handle the error here
}
InvalidateRect(hWnd, NULL, FALSE);
break;
注意倒数第二行的 InvalidateRect
调用。这似乎对于使绘图区域按照此应用程序所需的方式工作至关重要。原始文章中的代码缺少这一行。感谢 duggulous 在 MSDN 文章上的评论,他指出了这是必需的。
添加 WM_PAINT
的处理程序:
接下来,在 WndProc
中添加用于处理 WM_PAINT
的代码,以绘制圆圈。该部分最终看起来应如下所示:
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
// TODO: Add any drawing code here...
RECT client;
GetClientRect(hWnd, &client);
// start double buffering
if (!memDC){
memDC = CreateCompatibleDC(hdc);
}
hMemBmp = CreateCompatibleBitmap(hdc, client.right, client.bottom);
hOldBmp = (HBITMAP)SelectObject(memDC, hMemBmp);
//This conditional provides a convenient block
//within which backgroundBrush can be created and destroyed
if (memDC){
//A brush to create background is generated once
//and destroyed once every time this function is called
HBRUSH backgroundBrush = CreateSolidBrush(RGB(0,0,0));
FillRect(memDC, &client, backgroundBrush);
//Draw Touched Points
for (i=0; i < MAXPOINTS; i++){
//I added this block to monitor the touch point IDs on screen
TCHAR buffer[180];
_stprintf_s(buffer, 180, _T(
"cc %d tc %d idl: %d %d %d %d %d %d %d %d %d %d "),
cycleCount, touchCount,
circlesArray[0].sysID,
circlesArray[1].sysID,
circlesArray[2].sysID,
circlesArray[3].sysID,
circlesArray[4].sysID,
circlesArray[5].sysID,
circlesArray[6].sysID,
circlesArray[7].sysID,
circlesArray[8].sysID,
circlesArray[9].sysID
);
RECT rect = {0,0,800,20};
DrawText(memDC,buffer,100,(LPRECT)&rect,DT_TOP);
HBRUSH circleBrush = CreateSolidBrush(circlesArray[i].colour);
SelectObject( memDC, circleBrush);
x = circlesArray[i].pointX;
y = circlesArray[i].pointY;
if (x >0 && y>0){
Ellipse(memDC, x - radius, y - radius, x+ radius, y + radius);
}
ReleaseDC(hWnd, memDC);
DeleteObject(circleBrush);
}
BitBlt(hdc, 0,0, client.right, client.bottom, memDC, 0,0, SRCCOPY);
DeleteObject(backgroundBrush);
}
EndPaint(hWnd, &ps);
ReleaseDC(hWnd, hdc);
DeleteObject(hMemBmp);
DeleteObject(hOldBmp);
break;
关于内存处理的注意事项
注意 CreateSolidBrush
调用以及返回的 HBRUSH
被赋给 backgroundBrush
。在我的代码版本中,有两个地方我使用了此函数,并将返回的对象赋给了变量(backgroundBrush
和 circleBrush
)。 由于我将返回的对象赋给了变量,所以我能够在稍后的代码中删除这些对象,从而防止内存泄漏。 这就是原始 MSDN 代码中绘制背景的方式...
//This call to CreateSolidBrush causes a memory leak
//because there is no where where the created brush
//object can be deleted!
FillRect(memDC, &client, CreateSolidBrush(RGB(255,255,255)));
在 MSDN 文章的评论中,duggulous 展示了如何通过将返回的对象保存到变量中并在之后删除它来防止内存泄漏。也许有一种方法可以在直接将对象发送到 FillRect(如 MSDN 代码所示)后进行清理,但文章没有说明。Duggulous 的方法似乎有效,所以我采用了它。
如上所述,我通过将 CreateSolidBrush
创建的对象赋给变量位置来防止内存泄漏,从而可以在之后删除它们。circleBrush
在遍历 circlesArray
中每个项目的 for
循环的每个周期中创建和销毁,而 backgroundBrush
在每次处理 WM_PAINT
时创建和销毁一次。我的编译器对 backgroundBrush
在 switch
块的 case
段内的声明抱怨。为了绕过这个问题,我构造了一个舒适的 if(memDC){}
块让它在其中生灭,编译器对此很满意。
在阅读了 duggulous 和 Tom1omT 关于 MSDN 代码的评论并研究了 http://www.winprog.org/tutorial/bitmaps.html 上的文章后,包含以下几行似乎是明智的。
ReleaseDC(hWnd, hdc);
DeleteObject(hMemBmp);
DeleteObject(hOldBmp);
该文章描述了创建 HDC
的常见方法以及随后销毁它们的适当方法。显然,删除 memDC
的适当方法(再次,这是 MSDN 代码中未涉及的细节)是调用 DeleteDC
。我在各种地方尝试过,但代码无法与它正常工作,所以最终我只在处理 WM_DESTROY
时调用了一次。
case WM_DESTROY:
//next line gleaned from http://www.winprog.org/tutorial/bitmaps.html
DeleteDC(memDC);
PostQuitMessage(0);
break;
关注点
MSDN 代码是作为概念证明。也就是说,它纯粹是为了向您展示多点触控界面的工作原理,而不太关注内存处理或代码的长期稳定性。 让代码运行起来很有趣。多点触控界面将非常有用,并且由于这篇文章,我将能够充分利用它。感谢 Tom1omT 和 duggulous 的重要评论!
我尝试在 MSDN 文章上留下评论来解释我对代码所做的更改,但至今未成功。
历史
第一版 2013 年 10 月 14 日。
第二版 2013 年 10 月 28 日。
- 修复了一个内存泄漏(通过使用和删除
backgroundBrush
,而不是将CreateSolidBrush
的输出直接发送给FillRect
)。 - 将触摸点数据合并到
circle
结构和circlesArray
中。 - 确保与触摸点相关信息的长期状态保持(即,从
circlesArray
中正确返回系统触摸点 ID 的信息)。 - 为圆圈分配了随机颜色。