0%

触控板和触摸屏动作数据解析(1)

简介

在Windows系统中,预设了一些多点触控手势操作,提供用户进行操作。但有时候我们想要自己定义
一些手势,来实现其他的操作,这就需要进行触摸输入的开发

工作原理

1.触摸消息(Touch Message)

Windows系统提供了处理触摸屏手势和动作的 WM_GESTUREWM_TOUCH 消息,可以在MSDN帮助文档中
查看消息的参数内容,默认情况下,窗口只处理 WM_GESTURE 手势消息,如果注册了 WM_TOUCH 动作消息,
窗口就不会再收到手势消息,所有关于触摸屏的说明和示例,都可以在微软MSDN帮助中找到:
https://docs.microsoft.com/en-us/windows/win32/wintouch/windows-touch-portal
使用该方法有一定的限制,只能在消息窗口激活的状态才能获取到触摸动作,无法在后台状态获取消息

2.原始输入(Raw Input)

使用获取原始输入数据的方式,可以在后台工作,在微软MSDN帮助中有相关说明:
https://docs.microsoft.com/en-us/windows/win32/inputdev/raw-input
键盘和鼠标的输入都有详尽的解释,但是对于触摸设备的说明并不清晰,这里给出一个简单的示例

1.注册设备

要想获取到 WM_INPUT 原始输入消息,首先需要注册想要监控的输入设备类

1
2
3
4
BOOL RegisterRawInputDevices(
PCRAWINPUTDEVICE pRawInputDevices,
UINT uiNumDevices,
UINT cbSize);

其中 RAWINPUTDEVICE 结构体中指定 设备类型 和接收消息的 窗口句柄

1
2
3
4
5
6
typedef struct tagRAWINPUTDEVICE {
USHORT usUsagePage;
USHORT usUsage;
DWORD dwFlags;
HWND hwndTarget;
} RAWINPUTDEVICE, *PRAWINPUTDEVICE, *LPRAWINPUTDEVICE;

关于 UsagePageUsage 的值:(0x0D,0x05)表示触控板,(0x0D,0x04)表示触摸屏:
https://docs.microsoft.com/en-us/windows-hardware/drivers/hid/hid-architecture#hid-clients-supported-in-windows
Flags 指定 RIDEV_INPUTSINK 值,就可以让指定的窗口,在后台状态获取原始输入信息

1
2
3
4
5
6
7
8
9
10
11
12
13
BOOL RegistrerDevice(HWND hWnd)
{
RAWINPUTDEVICE Rid[2] = { 0 };
Rid[0].usUsagePage = 0x0D; // HID_USAGE_PAGE_DIGITIZER
Rid[0].usUsage = 0x05; // 符合HID标准的触控板
Rid[0].dwFlags = RIDEV_INPUTSINK;
Rid[0].hwndTarget = hWnd;
Rid[1].usUsagePage = 0x0D; // HID_USAGE_PAGE_DIGITIZER
Rid[1].usUsage = 0x04; // 符合HID标准的触摸屏
Rid[1].dwFlags = RIDEV_INPUTSINK;
Rid[1].hwndTarget = hWnd;
return RegisterRawInputDevices(Rid, 1, sizeof(RAWINPUTDEVICE));
}
2.解析消息

WM_INPUT 消息中,LPARAM 存储着 HRAWINPUT 信息,我们可以用来查询原始输入信息,
解析的过程比较复杂,所有的数据都需要通过对应 UsagePageUsage 值来确认内容:
https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/windows-precision-touchpad-required-hid-top-level-collections#windows-precision-touchpad-input-reports

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
BOOL ParseInputInfo(HRAWINPUT lParam)
{
// 申请内存并获取RAWINPUT数据
// 其实在RAWINPUT中就已经包含全部有效数据,但是所有数据堆积在一起,
// 我们无法知道其表示的意义,所以才获取PREPARSEDDATA数据重新解析
UINT uSize = 0;
UINT uRet = GetRawInputData(
lParam, RID_INPUT, NULL, &uSize, sizeof(RAWINPUTHEADER));
if (uRet != 0) return FALSE;
PRAWINPUT pRawInput = (PRAWINPUT)malloc(uSize);
if (!pRawInput) return FALSE;
ZeroMemory(pRawInput, uSize);
uRet = GetRawInputData(
lParam, RID_INPUT, pRawInput, &uSize, sizeof(RAWINPUTHEADER));
if (uRet != uSize) return FALSE;
// 获取未解析PREPARSEDDATA数据
uSize = 0;
uRet = GetRawInputDeviceInfoW(
pRawInput->header.hDevice, RIDI_PREPARSEDDATA, NULL, &uSize);
if (uRet != 0) return FALSE;
PHIDP_PREPARSED_DATA pPreparsedData = (PHIDP_PREPARSED_DATA)malloc(uSize);
if (!pPreparsedData) return FALSE;
ZeroMemory(pPreparsedData, uSize);
uRet = GetRawInputDeviceInfoW(
pRawInput->header.hDevice, RIDI_PREPARSEDDATA, pPreparsedData, &uSize);
if (uRet != uSize) return FALSE;
// 查询PREPARSEDDATA数据中包含Button和Value信息的数量
HIDP_CAPS stCaps = { 0 };
NTSTATUS ntStatus = HidP_GetCaps(pPreparsedData, &stCaps);
if (ntStatus != HIDP_STATUS_SUCCESS) return FALSE;
// 申请数组空间用来存储Button和Value数据
ULONG uDataSize = stCaps.NumberInputDataIndices * sizeof(HIDP_DATA);
PHIDP_DATA pDataList = (PHIDP_DATA)malloc(uDataSize);
if (!pDataList) return FALSE;
ZeroMemory(pDataList, uDataSize);
ULONG uDataNum = stCaps.NumberInputDataIndices;
ntStatus = HidP_GetData(
HidP_Input, pDataList, &uDataNum, pPreparsedData,
(PCHAR)pRawInput->data.hid.bRawData, pRawInput->data.hid.dwSizeHid);
if (ntStatus != HIDP_STATUS_SUCCESS) return FALSE;
// 获取全部ButtonCaps信息
uSize = stCaps.NumberInputButtonCaps * sizeof(HIDP_BUTTON_CAPS);
PHIDP_BUTTON_CAPS pButtonCaps = (PHIDP_BUTTON_CAPS)malloc(uSize);
if (!pButtonCaps) return FALSE;
ZeroMemory(pButtonCaps, uSize);
ntStatus = HidP_GetButtonCaps(
HidP_Input, pButtonCaps, &stCaps.NumberInputButtonCaps, pPreparsedData);
if (ntStatus != HIDP_STATUS_SUCCESS) return FALSE;
// 在数组中查询对应Index
for (DWORD i = 0; i < stCaps.NumberInputButtonCaps; i++)
{
BOOL isBtnData = FALSE;
for (DWORD j = 0; j < uDataNum; j++)
{
if (pDataList[j].DataIndex == pButtonCaps[i].NotRange.DataIndex)
{
isBtnData = pDataList[j].On;
break;
}
}
// 只会获取On状态的Button数据,如果未查到就说明不是On状态
if (pButtonCaps[i].UsagePage == 0x0D &&
pButtonCaps[i].NotRange.Usage == 0x47)
{
// 这里获取的是Confidence信息,注意pButtonCaps[i].IsRange成员,
// 这里默认当成NotRange处理,但并不总是这样
}
}
// 获取全部ValueCaps信息
uSize = stCaps.NumberInputValueCaps * sizeof(HIDP_VALUE_CAPS);
PHIDP_VALUE_CAPS pValueCaps = (PHIDP_VALUE_CAPS)malloc(uSize);
if (!pValueCaps) return FALSE;
ZeroMemory(pValueCaps, uSize);
ntStatus = HidP_GetValueCaps(
HidP_Input, pValueCaps, &stCaps.NumberInputValueCaps, pPreparsedData);
if (ntStatus != HIDP_STATUS_SUCCESS) return FALSE;
// 在数组中查询对应Index
for (DWORD i = 0; i < stCaps.NumberInputValueCaps; i++)
{
ULONG uValueData = 0;
for (DWORD j = 0; j < uDataNum; j++)
{
if (pDataList[j].DataIndex == pValueCaps[i].NotRange.DataIndex)
{
uValueData = pDataList[j].RawValue;
break;
}
}
if (pValueCaps[i].UsagePage == 0x01 &&
pValueCaps[i].NotRange.Usage == 0x30)
{
// 这里获取的是坐标x信息,可以看微软相关规定的链接
LONG nXMax = pValueCaps[i].LogicalMax;
// 坐标x的最大值为pValueCaps[i].LogicalMax;
}
}
// 释放申请的内存空间
free(pDataList);
free(pButtonCaps);
free(pValueCaps);
free(pPreparsedData);
free(pRawInput);
return TRUE;
}
3.创建窗口

由于我们并不需要窗口操作界面,所以可以创建 Message-Only Window 来处理消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
LRESULT CALLBACK WndProc(
HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
if (uMsg == WM_INPUT)
{
ParseInputInfo((HRAWINPUT)lParam);
return S_OK;
}
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

int APIENTRY wWinMain(
HINSTANCE hInst, HINSTANCE hPrevInst, LPWSTR lpCmdLine, int nCmdShow)
{
// 注册窗口类
WNDCLASSEXW wcex = { 0 };
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.lpfnWndProc = WndProc;
wcex.hInstance = hInst;
wcex.lpszClassName = L"MsgOnlyClass";
ATOM ret = RegisterClassExW(&wcex);
if (!ret && (GetLastError() != ERROR_CLASS_ALREADY_EXISTS))
return 0;
// 创建窗口
HWND hWnd = CreateWindowExW(
0, L"MsgOnlyClass", L"MsgOnlyWnd", 0,
0, 0, 0, 0, HWND_MESSAGE, NULL, hInst, NULL);
if (!hWnd) return 0;
UpdateWindow(hWnd);
// 注册输入设备
if (!RegistrerDevice(hWnd))
{
DestroyWindow(hWnd);
return 0;
}
// 消息循环
MSG msg = { 0 };
while (GetMessageW(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
// 销毁窗口
DestroyWindow(hWnd);
return 0;
}