Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C++

Using the RawInput API to Process MultiTouch Digitizers

4.58/5 (8 votes)
9 May 2012CPOL3 min read 54.8K   3K  
A demonstration of how to use Windows' RawInput API to manually process MultiTouch data.

RawInput API to Process MultiTouch Digitizers

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:

C++
// 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;
};

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:

C++
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;
       }
    }
 }
}

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:

C++
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 ();
}

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.

C++
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;

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)