Introduction
Windows 7's multi-touch API is pretty impressive
and easy to use. However, there are a few occasions where the API is limited. For example, some touch-screens have features like higher precision that would otherwise
be unavailable to the developer, or if your application needs to support versions of Windows 7 not containing tablet support.
Likewise, the RAWINPUT
API is preferred for DirectX games due to lower latency; however, multi-touch devices using RAWINPUT
are completely unsupported out of the box.
Thus, this example demonstrates how to directly interpret the HID data to handle multi-touch devices.
The Solution
When communicating with USB devices on Windows, you immediately think of using the CreateFile
API. However, for devices such as keyboards and mice,
Windows has an exclusive lock on the file handle, preventing applications from sending and receiving data from these devices.
Unfortunately, Windows treats digitizers, including multi-touch devices, the same. But there is hope - Enter
the RAWINPUT
API, which allows parsing of raw HID data.
Unfortunately, raw data is exactly what you get. So before you write any code, you need to track down the specifications for how the HID data is structured.
These data arrangements are vendor and product specific, and some are even protected under NDAs, making it impossible to release end-all be-all solutions.
The good news is that Linux supports the most common multi-touch digitizers, and Linux is
Open Source. Thus, in most cases you can simply reverse engineer Linux's touchscreen drivers to generate the appropriate structures. In other cases,
you can usually search the vendor's website for product specifications or contact them directly to obtain the documentation needed to parse HID data.
Alternatively, you can sniff HID packets, but this is not always as easy as looking at the values in a hex editor. In order to reduce USB bandwidth,
HID data is encoded on a per-bit level. Thus, there is no guarantee that the data is byte aligned. As a result, reverse engineering is fairly time consuming,
but still possible for those hardcore enough to try. In fact, the vendor IDs and product IDs can be obtained as easily as opening up the device in Device Manager.
For the case of this example, we use a dummy layout similar (but changed enough to not violate any copyrights) to a common implementation:
#define VENDOR_ID 0x0001
#define PRODUCT_ID 0x0001
struct TouchData
{
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;
int X ()
{
return (((x_position_msbyte & 0x0F) << 8) + x_position_lsbyte);
}
int Y ()
{
return (((y_position_msbyte & 0x0F) << 4) + y_position_lsbyte);
}
};
struct DigitizerData
{
BYTE usb_report_id;
TouchData touch [8];
BYTE active_touch_count;
};
With the vendor specific data defined, we can begin digging into the RAWDATA
API. The first thing that is needed is to verify that the multi-touch
device we are trying to support is attached. We do this by iterating through each HID attached and matching both the vendor and product IDs:
bool supported_touchscreen_present (false);
UINT input_device_count (0);
if (GetRawInputDeviceList (NULL, &input_device_count, sizeof (RAWINPUTDEVICELIST)) != 0)
{
break;
}
vector<RAWINPUTDEVICELIST> input_devices;
input_devices.resize (input_device_count);
if (GetRawInputDeviceList (&input_devices [0], &input_device_count,
sizeof (RAWINPUTDEVICELIST)) == (UINT)-1)
{
break;
}
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)
{
if (device_info.dwType == RIM_TYPEHID)
{
if ((device_info.hid.dwVendorId == VENDOR_ID)
&& (device_info.hid.dwProductId == PRODUCT_ID))
{
supported_touchscreen_present = true;
break;
}
}
}
}
If the device is attached, we can then register for multi-touch HID messages to be received by our window's message put through
WM_INPUT
events:
RAWINPUTDEVICE raw_input_device [1];
raw_input_device [0].usUsagePage = 0x0D;
raw_input_device [0].dwFlags = RIDEV_INPUTSINK | RIDEV_PAGEONLY;
raw_input_device [0].usUsage = 0x00;
raw_input_device [0].hwndTarget = hwnd;
if (RegisterRawInputDevices (raw_input_device, 1, sizeof (raw_input_device [0])) == FALSE)
{
printError ();
}
And now the waiting game... When input from the multi-touch digitizer is received, the WM_INPUT
event will fire. Retrieving data through the RAWINPUT
API
is a two step process - first, you query the size of the data; then, after allocating the appropriate amount of memory, you actually receive a copy of the data.
The RAWDATA
API supports grouping packets together; thus, to ensure full compatibility and responsiveness, each packet indicated in the header should be parsed.
If the structures are defined correctly, the data in the packets themselves can just be reinterpreted casted to the supported HID structure.
While our example only registers for a single specific multi-touch device, the function
GetRawInputDeviceInfo
can be used
to determine which digitizer specifically generated the WM_INPUT
event.
case WM_INPUT:
{
do
{
UINT data_size (0);
GetRawInputData ((HRAWINPUT)l_param, RID_INPUT, NULL,
&data_size, sizeof (RAWINPUTHEADER));
vector<BYTE> data;
data.resize (data_size);
if (GetRawInputData ((HRAWINPUT)l_param, RID_INPUT, &data [0],
&data_size, sizeof(RAWINPUTHEADER)) != data_size)
{
printError ();
break;
}
RAWINPUT* raw = (RAWINPUT*)(&data [0]);
if (raw->header.dwType == RIM_TYPEHID)
{
for (DWORD index (0); index < raw->data.hid.dwCount; ++index)
{
DigitizerData* result ((DigitizerData*)&raw->data.hid.bRawData [raw->data.hid.dwSizeHid * index]);
for (BYTE touch_index (0); touch_index < result->active_touch_count; ++touch_index)
{
}
}
}
} while (0);
result = DefWindowProc (window_handle, message, w_param, l_param);
}
break;