使用 RawInput API 处理多点触控数字化仪






4.58/5 (8投票s)
演示如何使用 Windows 的 RawInput API 手动处理多点触摸数据。
引言
Windows 7 的 多点触摸 API 相当令人印象深刻,并且易于使用。但是,在某些情况下,API 会受到限制。例如,某些触摸屏具有更高的精度等功能,而开发人员无法使用这些功能,或者如果您的应用程序需要支持不包含平板电脑支持的 Windows 7 版本。同样,由于较低的延迟,RAWINPUT
API 优先用于 DirectX 游戏;但是,使用 RAWINPUT
的多点触摸设备在开箱即用时完全不受支持。因此,本示例演示了如何直接解释 HID 数据来处理多点触摸设备。
解决方案
当与 Windows 上的 USB 设备通信时,您会立即想到使用 CreateFile
API。但是,对于键盘和鼠标等设备,Windows 会独占锁定文件句柄,阻止应用程序从这些设备发送和接收数据。不幸的是,Windows 对数字化仪(包括多点触摸设备)的处理方式相同。但还有希望 - 输入 RAWINPUT
API,它允许解析原始 HID 数据。
不幸的是,您得到的正是原始数据。因此,在编写任何代码之前,您需要查找 HID 数据结构的规范。这些数据排列是特定于供应商和产品的,有些甚至受到 NDA 的保护,这使得发布终极解决方案成为不可能。好消息是 Linux 支持最常见的多点触摸数字化仪,而 Linux 是开源的。因此,在大多数情况下,您可以简单地逆向工程 Linux 的 触摸屏驱动程序以生成适当的结构。在其他情况下,您通常可以在供应商的网站上搜索产品规格,或直接联系他们以获取解析 HID 数据所需的文件。或者,您可以嗅探 HID 数据包,但这并不总是像查看十六进制编辑器中的值那么容易。为了减少 USB 带宽,HID 数据是在每位级别编码的。因此,不能保证数据是字节对齐的。因此,逆向工程非常耗时,但对于那些足够硬核的人来说,仍然是可能的。事实上,可以像在设备管理器中打开设备一样轻松地获取供应商 ID 和产品 ID。对于本例,我们使用类似于(但已更改足够多以不侵犯任何版权)常见实现的虚拟布局
// the vendor ID for our supported touchscreen; this and the product ID can be found
// when selecting the touchscreen in Device Manager
#define VENDOR_ID 0x0001
// the product ID for our supported touchscreen
#define PRODUCT_ID 0x0001
// a sample structure containing interpreted HID data for a touch instance;
// as stated earlier, you'll have to roll your own and then use the device's
// vendor and product IDs for each device supported
struct TouchData
{
// the good news is that bit packing makes reinterpreting the data fairly simple,
// even in crazy cases where the data is half-aligned such as our interleaved
// X and Y position
BYTE status : 1;
BYTE in_range : 1;
BYTE padding : 1;
BYTE touch_id : 5;
BYTE x_position_lsbyte;
BYTE x_position_msbyte : 4;
BYTE y_position_lsbyte : 4;
BYTE y_position_msbyte;
// the exception is when data is stored across bytes, especially with
// partial bits involed. in these cases, helper functions make things
// easy to work with
// helper function to get the X value from the multi-byte parameter
int X ()
{
return (((x_position_msbyte & 0x0F) << 8) + x_position_lsbyte);
}
// helper function to get the Y value from the multi-byte parameter
int Y ()
{
return (((y_position_msbyte & 0x0F) << 4) + y_position_lsbyte);
}
};
// a sample structure mapping the entire HID data for a touch event;
// again, these are vendor specific, so you will need to either acquire the data sheets
// for the device or reverse engineer the Linux drivers
struct DigitizerData
{
BYTE usb_report_id;
TouchData touch [8];
BYTE active_touch_count;
};
定义了特定于供应商的数据后,我们可以开始深入研究 RAWDATA
API。首先需要验证我们要支持的多点触摸设备是否已连接。我们通过迭代每个连接的 HID 并匹配供应商和产品 ID 来实现这一点
bool supported_touchscreen_present (false);
// get the number of HID input devices
UINT input_device_count (0);
if (GetRawInputDeviceList (NULL, &input_device_count, sizeof (RAWINPUTDEVICELIST)) != 0)
{
break;
}
// allocate memory for their structures
vector<RAWINPUTDEVICELIST> input_devices;
input_devices.resize (input_device_count);
// then populate the device list
if (GetRawInputDeviceList (&input_devices [0], &input_device_count,
sizeof (RAWINPUTDEVICELIST)) == (UINT)-1)
{
break;
}
// for each device...
RID_DEVICE_INFO device_info;
for (vector<RAWINPUTDEVICELIST>::iterator device_iterator (input_devices.begin ());
device_iterator != input_devices.end ();
++device_iterator)
{
UINT info_size (sizeof (RID_DEVICE_INFO));
if (GetRawInputDeviceInfo (device_iterator->hDevice,
RIDI_DEVICEINFO, (LPVOID)&device_info, &info_size) == info_size)
{
// non-keyboard, non-mouse HID device?
if (device_info.dwType == RIM_TYPEHID)
{
if ((device_info.hid.dwVendorId == VENDOR_ID)
&& (device_info.hid.dwProductId == PRODUCT_ID))
{
supported_touchscreen_present = true;
break;
}
}
}
}
如果设备已连接,我们就可以注册多点触摸 HID 消息,以通过我们的窗口的 WM_INPUT
事件接收
RAWINPUTDEVICE raw_input_device [1];
// Starting with Windows 7, multitouch digitizers appear as HID touch digitizers (page 0x0D, usage 0x04),
// but they also contain the contact ID usage in their report descriptor (page 0x0D, usage 0x51).
raw_input_device [0].usUsagePage = 0x0D;
// RIDEV_PAGEONLY specifies all devices whose top level collection is from the specified usUsagePage.
// Note that usUsage must be zero.
raw_input_device [0].dwFlags = RIDEV_INPUTSINK | RIDEV_PAGEONLY;
raw_input_device [0].usUsage = 0x00;
// route the RAWINPUT messages to our window; this is required for the RIDEV_INPUTSINK option
raw_input_device [0].hwndTarget = hwnd;
// listen to digitizer events
if (RegisterRawInputDevices (raw_input_device, 1, sizeof (raw_input_device [0])) == FALSE)
{
// handle the error gracefully
printError ();
}
现在是等待游戏... 当从多点触摸数字化仪接收到输入时,将触发 WM_INPUT
事件。通过 RAWINPUT
API 检索数据是一个两步过程 - 首先,您查询数据的大小;然后,在分配了适当的内存量后,您实际上会收到数据的副本。RAWDATA
API 支持将数据包分组在一起;因此,为确保完全兼容性和响应性,应解析标头中指示的每个数据包。如果结构定义正确,数据包中的数据本身可以重新解释为支持的 HID 结构。虽然我们的示例仅注册单个特定的多点触摸设备,但函数 GetRawInputDeviceInfo
可用于确定哪个数字化仪专门生成了 WM_INPUT
事件。
case WM_INPUT:
{
do
{
// determine the size of the input data
UINT data_size (0);
GetRawInputData ((HRAWINPUT)l_param, RID_INPUT, NULL,
&data_size, sizeof (RAWINPUTHEADER));
// preallocate our buffer
vector<BYTE> data;
data.resize (data_size);
// and then read the input data in
if (GetRawInputData ((HRAWINPUT)l_param, RID_INPUT, &data [0],
&data_size, sizeof(RAWINPUTHEADER)) != data_size)
{
// handle the error gracefully
printError ();
break;
}
// the RAWINPUT structure starts at the beginning of our data array
RAWINPUT* raw = (RAWINPUT*)(&data [0]);
// make sure keyboard/mouse HID data didn't somehow sneak its way in here
if (raw->header.dwType == RIM_TYPEHID)
{
// for each packet received..
for (DWORD index (0); index < raw->data.hid.dwCount; ++index)
{
// reinterpret the data as our nicely formatted digitizer-specific structure
DigitizerData* result ((DigitizerData*)&raw->data.hid.bRawData [raw->data.hid.dwSizeHid * index]);
// for each touch registered...
for (BYTE touch_index (0); touch_index < result->active_touch_count; ++touch_index)
{
// insert touch handler code here
}
}
}
} while (0);
// the application must call DefWindowProc so the system can perform the cleanup
result = DefWindowProc (window_handle, message, w_param, l_param);
}
break;