Using Alternate Input Devices in Your Smart Client Applications

 

Bruce Thomas

May 2005

Applies to:
   Windows raw input API
   Human Interface Devices (HIDs)

Summary: Input devices such as touch screens and remote controls are referred to as Human Interface Devices (HIDs) and the Windows raw input API provides us with a robust method of accepting input from any combination of HIDs. (11 printed pages)

Download the sample that accompanies this article, RemoteControlSampleCode.msi.

Contents

Introduction
Traditional Input Mode
Why Is Raw Input Mode Different?
Getting Started Using the Raw Input API
Registering Devices for Raw Input Mode
Driving Your Smart-Client Application via Remote Control Device
Conclusion

Introduction

The growing popularity of consumer-oriented user interfaces like Windows XP Media Center Edition, which forego the traditional keyboard and mouse combination as the primary input devices, demonstrates how applications can leverage alternative input devices that enhance the overall user experience. By simply adding a new input method, the reach of an application can be extended to an entirely new customer base. As developers, we know how important it is to choose the right input scheme for our applications. The number of mouse clicks, keyboard shortcuts, and control tab order can have a huge impact on productivity and customer satisfaction. For most applications, the traditional keyboard and mouse user-input devices is an acceptable, if not required minimum level of user interaction for our applications. But for a growing number of applications this is not enough or no longer represents the most appropriate way to gather user input.

In addition to traditional devices like the keyboard and mouse, input devices like game pads, joysticks, global positioning system (GPS), microphones, touch screens, remote controls, and many others, can all be leveraged in a reliable and flexible way in your applications. Collectively, these devices are referred to as Human Interface Devices (HIDs) and the raw input API provides us with a robust method of accepting input from any combination of HIDs.

Traditional Input Mode

Before we get into the specifics of using the raw input APIs, let's briefly revisit the original Windows input model for the keyboard and mouse. Traditionally, applications received their input from devices in the form of messages that are posted to the application's message queue. For example, the keyboard generates device-specific scan codes data that is interpreted by the system and forwards this data to your application in the form of a WM_CHAR message. This allows your application to operate at a higher level of abstraction from the hardware, but it may also prevent some vital device information from ever reaching your application. While the original input model works well for standard keyboard- and mouse-based applications, its shortcoming become very obvious if you need to support other input-based devices within the same application.

Here's a list of major disadvantages to using the original input mode:

  • You may have to write device-specific code that detects and/or opens each HID.
  • Your application can't distinguish the particular source of input for devices that generate similar messages.
  • There is no way to manage the amount of data traffic to your application. It's all or nothing.
  • There is no support for future devices as they become available. You have to rely on OS updates or manufacturer SDKs.

The raw input model was specifically developed to address these issues and allow simple access to the raw input from all HIDs, including the keyboard and mouse.

Why Is Raw Input Mode Different?

In contrast, the raw input model requires your application to register the HIDs that it wants to receive input from. By default, your application will receive no raw input data unless it specifically registers for it, and all raw input data is received via the WM_INPUT message. As its name implies, the data received from registered devices is device-specific, low-level data and it is up to your application to interpret this data properly. Let's take a look at the list of raw input functions that you can use in your applications. For more specific information, please refer to the raw input API reference.

Function Description
DefRawInputProc This function calls the default raw input procedure to provide default processing for any raw input messages that an application does not process. This function ensures that every message is processed.
GetRawInputBuffer This function does a buffered read of the raw input data.
GetRawInputData This function gets the raw input from the specified device.
GetRawInputDeviceInfo This function gets information about the raw input device.
GetRawInputDeviceList This function enumerates the raw input devices attached to the system.
GetRegisteredRawInputDevices This function gets the information about the raw input devices for the current application.
RegisterRawInputDevices This function registers the devices that supply the raw input data.

Getting Started Using the Raw Input API

To get a feel for using the raw input API in your application, let's begin by getting a list of existing input devices that are currently available on the system. The GetRawInputDeviceList function is use to request the list of devices and we can gather the name of each device using the GetRawInputDeviceInfo function. Each device is classified as one of three types; RIM_TYPEMOUSE, RIM_TYPEKEYBOARD and RIM_TYPEHID. RIM_TYPEHID represents every device that is not a mouse or keyboard. Note that some devices can represent multiple types, as we will see later using a remote control device.

private const int RIDI_DEVICENAME = 0x20000007;

[StructLayout(LayoutKind.Sequential)]
internal struct RAWINPUTDEVICELIST
{
   public IntPtr hDevice;
   [MarshalAs(UnmanagedType.U4)]
   public int   dwType;
}

[DllImport("User32.dll")]
extern static uint GetRawInputDeviceList(IntPtr pRawInputDeviceList, 
ref uint uiNumDevices, uint cbSize);
[DllImport("User32.dll")]
extern static uint GetRawInputDeviceInfo(IntPtr hDevice, uint 
uiCommand, IntPtr pData, ref uint pcbSize);

We start with the typical structure and method declarations for the raw input API. But, because many of the raw input API structures and methods use variable arrays, getting the results is not that trivial.

uint deviceCount = 0;
int dwSize = (Marshal.SizeOf(typeof(RAWINPUTDEVICELIST)));

if (GetRawInputDeviceList(IntPtr.Zero, ref deviceCount, (uint) dwSize) != 0)
   throw new ApplicationException("Error!");

IntPtr pRawInputDeviceList = Marshal.AllocHGlobal((int) (dwSize * deviceCount));
GetRawInputDeviceList(pRawInputDeviceList, ref deviceCount, (uint) dwSize);

for (int i = 0; i < deviceCount; i++)
{
   string deviceName = string.Empty;
   uint pcbSize = 0;

   RAWINPUTDEVICELIST rid = (RAWINPUTDEVICELIST) Marshal.PtrToStructure(
      new IntPtr((pRawInputDeviceList.ToInt32() + (dwSize * i))),
      typeof(RAWINPUTDEVICELIST)
      );

   GetRawInputDeviceInfo(rid.hDevice, RIDI_DEVICENAME, IntPtr.Zero, ref pcbSize);
   if (pcbSize > 0)
   {
      IntPtr pData = Marshal.AllocHGlobal((int) pcbSize);
      GetRawInputDeviceInfo(rid.hDevice, RIDI_DEVICENAME, pData, ref pcbSize);
      deviceName = (string) Marshal.PtrToStringAnsi(pData);
      Marshal.FreeHGlobal(pData);
   }
   Console.WriteLine("hDevice: {0}, dwType: {1}, Name: {2}", 
      rid.hDevice, rid.dwType, deviceName);
}
Marshal.FreeHGlobal(pRawInputDeviceList);Device, uint uiCommand, IntPtr 
pData, ref uint pcbSize);

In the sample code above, we first call the GetRawInputDeviceList function to get a count of the devices currently on the system. Next, we allocate some unmanaged memory to hold the list based on the number of devices and a second call is made to GetRawInputDeviceList passing in a pointer to memory. Then we enumerate the memory, copying each to a RAWINPUTDEVICELIST structure. Last, we repeat this process using the device handle from the RAWINPUTDEVICELIST structure to call the GetRawInputDeviceInfo function to get the name of the device.

Registering Devices for Raw Input Mode

Now that we have a feel for how the raw input API works, let get our application registered to receive input from some of those devices. As previously mentioned, by default, your application will not receive any raw input from any device until it has registered the device. To register one or more devices, we pass an array of the RAWINPUTDEVICE structure to the RegisterRawInputDevice function. The RAWINPUTDEVICE structure contains the usUsagePage and usUsage properties, which determine the class of device and the device within the class to register. Collectively, the Usage Page and Usage specify the top-level collection (TLC) for the device. We can specify the dwFlags property to determine how the TLC is interpreted.

[DllImport("User32.dll")]
extern static bool RegisterRawInputDevices(RAWINPUTDEVICE[] 
pRawInputDevice, uint uiNumDevices, uint cbSize);

[StructLayout(LayoutKind.Sequential)]
internal struct RAWINPUTDEVICE
{
   [MarshalAs(UnmanagedType.U2)]
   public ushort usUsagePage;
   [MarshalAs(UnmanagedType.U2)]
   public ushort usUsage;
   [MarshalAs(UnmanagedType.U4)]
   public int    dwFlags;
   public IntPtr hwndTarget;
}
 
private void Form1_Load(object sender, System.EventArgs e)
{ 
   RAWINPUTDEVICE[] rid = new RAWINPUTDEVICE[5];

   rid[0].usUsagePage = 0xFFBC;      // adds HID remote control
   rid[0].usUsage = 0x88;
   rid[0].dwFlags = RIDEV_INPUTSINK;
   rid[0].hwndTarget = this.Handle;

   rid[1].usUsagePage = 0x0C;      // adds HID remote control
   rid[1].usUsage = 0x01;
   rid[0].dwFlags = RIDEV_INPUTSINK;
   rid[0].hwndTarget = this.Handle;

   rid[2].usUsagePage = 0x0C;      // adds HID remote control
   rid[2].usUsage = 0x80;
   rid[0].dwFlags = RIDEV_INPUTSINK;
   rid[0].hwndTarget = this.Handle;
   
   rid[3].usUsagePage = 0x01;      // adds HID mouse with no legacy messages
   rid[3].usUsage = 0x02;
   rid[3].dwFlags = RIDEV_NOLEGACY;

   rid[4].usUsagePage = 0x01;      // adds HID keyboard with no legacy message
   rid[4].usUsage = 0x06;
   rid[4].dwFlags = RIDEV_NOLEGACY;
   
   if (!RegisterRawInputDevices(rid, (uint) rid.Length, (uint) Marshal.SizeOf(rid[0])))
      throw new ApplicationException("Failed to register raw input devices.");
}

In this scenario, our application is registering to receive raw input from three different devices: a remote control, the mouse, and the keyboard. Further, this sample code demonstrates the amount of control your application can exercise to determine what type of input it wants to receive. This isn't possible without using the raw input mode. Our RAWINPUTDEVICE array is declared with an element size of five because the remote control device requires three TLCs to be registered for a different set of buttons. The RIDEV_INPUTSINK flag is set for the remote control's TLCs so that we can receive input from that device even if our application is not in the foreground. For the mouse and keyboard, we can eliminate legacy messages like WM_LBUTTONDOWN and WM_KEYDOWN and deal with these events only via raw input. Once we have successfully called the RegisterRawInputDevices function our application will begin to receive the raw input data via the WM_INPUT message. One important concept here is that you can register devices even if they aren't currently connected to the system. Once the device is connected, your application will begin receiving input from that device.

Driving Your Smart-Client Application via Remote Control Device

We began this discussion by talking about the growing popularity of consumer-oriented applications, so let's take a look at how to gather data from one of these devices to drive our application. For this demonstration, we are using a Microsoft Windows XP Media Center Remote Control device connected through a USB port. Support for input from infrared remote control devices was added beginning with Windows XP Service Pack 1.

What's interesting about this device is that it combines both the traditional and the raw input model. Some buttons like the digits 1-9, ENTER, and CHAN/PAGE UP generate traditional messages like WM_KEYDOWN and WM_APPCOMMAND. However, other buttons like GUIDE, RECORDED TV, and DVD MENU are available only using the raw input model. We want our application to be able to receive input from all of the remote's buttons. We start by creating the RemoteControlDevice class to represent the functionality of the remote control device in our application.

// some form in our app...
RemoteControlDevice _remote

protected override void WndProc(ref Message message)
{
   _remote.ProcessMessage(message);         
   base.WndProc(ref message);
}

public sealed class RemoteControlDevice
{
   public delegate void RemoteControlDeviceEventHandler(object 
sender, RemoteControlEventArgs e);
   public event RemoteControlDeviceEventHandler ButtonPressed;

   public RemoteControlDevice()
   { 
      // register device here...
    }

   public void ProcessMessage(Message message)
   {
      int param;

      switch (message.Msg)
      {
         case WM_KEYDOWN:
            param = message.WParam.ToInt32();
            ProcessKeyDown(param);
            break;
         case WM_APPCOMMAND:
            param = message.LParam.ToInt32();
            ProcessAppCommand(param);
            break;
         case WM_INPUT:
            ProcessInputCommand(ref message);
            break;
      }
   }
   ...
}

In the sample code above, we begin by overriding WndProc in our main form in order to process the messages. The messages are passed on to an instance of our RemoteControlDevice class for filtering and processing. In this case, we are interested in the three messages produced by the remote when a button is pressed. This could be reduced to one using the previously demonstrated techniques for registering devices with no legacy support.

private void ProcessInputCommand(ref Message message)
{
   RemoteControlButton rcb = RemoteControlButton.Unknown;
   uint dwSize = 0;

   GetRawInputData(message.LParam, RID_INPUT, IntPtr.Zero, ref 
dwSize, (uint) Marshal.SizeOf(typeof(RAWINPUTHEADER)));

   IntPtr buffer = Marshal.AllocHGlobal((int) dwSize);
   try
   {
      if (GetRawInputData(message.LParam,  RID_INPUT, buffer, ref 
dwSize, (uint) Marshal.SizeOf(typeof(RAWINPUTHEADER))) != dwSize)
         return;
      
      RAWINPUT raw = (RAWINPUT) Marshal.PtrToStructure(buffer, typeof(RAWINPUT));
      
      byte[] bRawData = new byte[raw.hid.dwSizHid];
      int pRawData = buffer.ToInt32() + Marshal.SizeOf(typeof(RAWINPUT)) + 1;

      Marshal.Copy(new IntPtr(pRawData), bRawData, 0, raw.hid.dwSizHid - 1);
      int rawData = bRawData[0] | bRawData[1] << 8;   // do a 
little bit shifting to assign the button value

      switch (rawData)
      {
         case RAWINPUT_DETAILS:
            rcb = RemoteControlButton.Details;
            break;
         case RAWINPUT_GUIDE:
            rcb = RemoteControlButton.Guide;
            break;            
         case RAWINPUT_DVDMENU:
            rcb = RemoteControlButton.DVDMenu;
            break;
         ...
      }

      if (rcb != RemoteControlButton.Unknown && 
this.ButtonPressed != null)
         this.ButtonPressed(this, new 
RemoteControlEventArgs(rcb, GetDevice(message.LParam.ToInt32())));
   }
   finally
   {
      Marshal.FreeHGlobal(buffer);
   }
}

As you can see, the code used to retrieve the actual raw input data using the GetRawInputData function follows the same pattern we have used to gather other information about the HIDs. At this point, it's really up to our application to decide what to do with this information. In this case, the raw data contains a value for the button pressed on the remote, but for a GPS input device this data could contain the coordinates for the current position. However, this information has to be interpreted for each HID, so you will need detailed information about the raw data to do this interpretation correctly.

Conclusion

As developers, we have much to gain by writing applications that support a wider range of input devices. In addition to introducing new ways to accomplish tasks, alternative device support can help us find new audiences for our application, reduce training obstacles, and allow end-users to interact with simpler, more familiar devices. By leveraging the raw input API, opening our applications to support new devices in a robust way is not as difficult as you may think.

 

About the author

Bruce Thomas is the founder and Chief Software Architect of Simplify (www.simplifi.com), a development, consulting, and integration firm dedicated to using its expertise in the Microsoft .NET Framework and Windows to help companies build better software solutions. Bruce specializes in the design and development of enterprise-level applications. He can be reached at brucet@simplifi.com.