原文:http://www.codeproject.com/KB/system/rawinput.aspx
- Download source files - 15.9 Kb
- Download WPF sample - 30 Kb
Introduction
Background
Support for different devices
Using the code
Implementing a Windows API Raw Input handler
Retrieving and processing raw input
Retrieving the list of input devices
Getting information on specific devices
Reading device information from the Registry
Conclusion
Sources
Introduction
There was a time when you were lucky if a PC had so much as a mouse, but today, it is common to have a wide variety of Human Interface Devices (HIDs) ranging from game controllers to touch screens. In particular, users can connect more than one keyboard to their PCs. However, the usual keyboard programming methods in the .NET Framework offer no way to differentiate the input from different keyboards. Any application handling KeyPress
events will receive the input from all connected keyboards as if they were a single device.
Windows XP and above now support a "raw input" API which allows programs to handle the input from any connected human interface devices directly. Intercepting this information and filtering it for keyboards enables an application to identify which device triggered the message. For example, this could allow two different windows to respond to input from different keyboards.
This article and the enclosed code demonstrate how to handle raw input in order to process keystrokes and identify which device they come from. The InputDevice.cs file in the attached zip contains the raw input API wrapper; copy this file to your own project and follow the instructions in "Using the code" if you want to use the class without running the sample application.
Background
I recently published an article on implementing a low-level keyboard hook in C#[^] using the SetWindowsHookEx
and related methods from user32.dll. While looking for a solution to handle multiple keyboards, Steve Messer[^] came across my article and we discussed whether my code could be adapted to his needs. In fact, it turned out that it couldn't, and that the Raw Input API was the solution.
Unfortunately, there are very few keyboard-related Raw Input samples online, so when Steve had finished a working sample of his code, I offered to write this article so that future .NET developers faced with this problem wouldn't have to look far to find the solution. While I have made minor adjustments to the code, it is primarily Steve's work and I thank him for sharing it. Note: as of March 2007, you can also download Steve's WPF sample illustrating the use of WndProc in Windows Vista. However, this article only describes the Windows XP source code.
Please note that this will only work on Windows XP or later in a non-Terminal Server environment, and the attached sample projects are for Visual Studio 2005.
Support for different devices
The attached code is a generic solution that mostly mirrors the sample code given on MSDN. Different devices will work in different ways, and you may need to amend the code to suit the keyboards you are using. Unfortunately, we won't always be able to help with device-specific queries, as we won't have the same devices you have. Steve Messer has tested the code with different keyboards, however, and is confident that it will work with most devices provided they are correctly installed.
Using the code
All the code related to raw input handling is encapsulated in the InputDevice
class, and using it is a matter of implementing three simple steps:
1. Instantiate an InputDevice
object
The InputDevice
class's constructor takes one argument, which is the handle to the current window.
InputDevice id = new InputDevice( Handle );
The handle is required to ensure that the window will continue to listen for events even when it doesn't have the focus.
2. Handle the KeyPressed
event
When a key is pressed, the InputDevice
class raises a custom KeyPressed
event containing some KeyControlEventArgs
. This needs to be handled by a method of the type DeviceEventHandler
, which can be set up as follows:
id.KeyPressed += new InputDevice.DeviceEventHandler( m_KeyPressed );
The method that handles the event can then perform whatever actions are required based on the contents of the KeyControlEventArgs
argument. The sample application attached to this article simply uses the values to populate a dialog box.
3. Override the WndProc
method
In its present form, the InputDevice
class works by intercepting messages to the window in order to process the WM_INPUT
messages containing raw input data. The window listening for raw input will therefore need to override its own Windows procedure and pass all its messages to the instantiated InputDevice
object.
protected override void WndProc( ref Message message ) { if( id != null ) { id.ProcessMessage( message ); } base.WndProc( ref message ); }
After writing the code used in this article, Steve decided that the InputDevice
class could be truly independent from the application using it by inheriting from NativeWindow[^]. However, as the purpose of this article is primarily to illustrate the use of the Raw Input API, we decided to keep the code in its original form.
The rest of this article describes how to handle "raw input" from a C# application, as illustrated by the InputDevice
class in the sample application.
Implementing a Windows API Raw Input handler
MSDN identifies "raw input" [^] as being the raw data supplied by an interface device. In the case of a keyboard, this data is normally intercepted by Windows and translated into the information provided by Key
events in the .NET Framework. For example, the Windows manager translates the device-specific data about keystrokes into virtual keys.
However, the normal Windows manager doesn't provide any information about which device received the keystroke; it just bundles events from all keyboards into one category and behaves as if there were just one keyboard.
This is where the Raw Input API is useful. It allows an application to receive data directly from the device, with minimal intervention from Windows. Part of the information it provides is the identity of the device that triggered the event.
The user32.dll in Windows XP and Vista contains the following methods for handling raw input:
RegisterRawInputDevices
allows the application to register the input devices it wants to monitor.GetRawInputData
retrieves the data from the input device.GetRawInputDeviceList
retrieves the list of input devices attached to the system.GetRawInputDeviceInfo
retrieves information on a device.
The following sections give an overview of how these four methods are used to process raw data from keyboards.
Registering raw input devices
By default, no application receives raw input. The first step is therefore to register the input devices that will be providing the desired raw data, and associate them with the window that will be handling this data.
To do this, the RegisterRawInputDevices
method is imported from user32.dll:
[DllImport("User32.dll")] extern static bool RegisterRawInputDevices( RAWINPUTDEVICE[] pRawInputDevice, uint uiNumDevices, uint cbSize);
To determine which devices should be registered, the method accepts an array of RAWINPUTDEVICE
structures. The other two arguments are the number of items in the array, and the number of bytes in a RAWINPUTDEVICE
structure.
The RAWINPUTDEVICE
structure is defined in Windows.h for C++ projects, but as this file isn't used in C#, the structure has been redefined as a member of the InputDevice
class.
[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; }
Each RAWINPUTDEVICE
structure added to the array contains information on a type of device which interests the application. For example, it is possible to register keyboards and telephony devices. The structure uses the following information:
- Usage Page: The top level HID "usage page". For most HIDs, including the keyboard, this is 0x01.
- Usage ID: A number indicating which precise type of device should be monitored. For the keyboard, this is 0x06. (A list of Usage Page and Usage ID values can be found in this MSDN article on HIDs[^])
- Flags: These determine how the data should be handled, and whether some types should be ignored. A list of possible values is given in the MSDN article[^], and the constants they represent are defined in Windows.h (there's a copy of it here[^] if you don't already have one).
- Target Handle: The handle of the window which will be monitoring data from this particular type of device.
In this case, we are only interested in keyboards, so the array only has one member and is set up as follows:
RAWINPUTDEVICE[] rid = new RAWINPUTDEVICE[1]; rid[0].usUsagePage = 0x01; rid[0].usUsage = 0x06; rid[0].dwFlags = RIDEV_INPUTSINK; rid[0].hwndTarget = hwnd;
Here, the code only defines the RIDEV_INPUTSINK
flag, which means that the window will always receive the input messages, even if it is no longer has the focus. This will enable two windows to respond to events from different keyboards, even though at least one of them won't be active.
With the array ready to be used, the method can be called to register the window's interest in any devices which identify themselves as keyboards:
RegisterRawInputDevices(rid, (uint)rid.Length, (uint)Marshal.SizeOf(rid[0]))
Once the type of device has been registered this way, the application can begin to process the data using the GetRawInputData
method described in the next section.
Retrieving and processing raw input
When the type of device is registered, the application begins to receive raw input. Whenever a registered device is used, Windows generates a WM_INPUT
message containing the unprocessed data from the device.
Each window whose handle is associated with a registered device as described in the previous section must therefore check the messages it receives and take appropriate action when a WM_INPUT
one is detected. In the sample application, the InputDevice
class takes care of checking for WM_INPUT
messages, so all the main window does is override its base WndProc
method to get access to the messages, and pass any valid ones to the InputDevice
object:
protected override void WndProc( ref Message message ) { if( id != null ) { id.ProcessMessage( message ); } base.WndProc( ref message ); }
The ProcessMessage
method in InputDevice
filters the messages, calling ProcessInputCommand
whenever a WM_INPUT
is received. Any other type of message will fall through to the call to the base WndProc
, so the application will respond to other events normally.
public void ProcessMessage( Message message ) { switch( message.Msg ) { case WM_INPUT: { ProcessInputCommand( message ); } break; } }
ProcessInputCommand
then uses the GetRawInputData
method to retrieve the contents of the message and translate it into meaningful information.
Retrieving the information from the message
In order to process the data in WM_INPUT
messages, the GetRawInputData
method is imported from user32.dll:
[DllImport("User32.dll")] extern static uint GetRawInputData(IntPtr hRawInput, uint uiCommand, IntPtr pData, ref uint pcbSize, uint cbSizeHeader);
The method uses the following parameters:
- hRawInput
The handle to theRAWINPUT
structure containing the data, as provided by thelParam
in aWM_INPUT
message. - uiCommand
A flag which sets whether to retrieve the input data or the header information from theRAWINPUT
structure. Possible values areRID_INPUT
(0x10000003) orRID_HEADER
(0x10000005) respectively. - pData:
Depending on the desired result, this can be one of two things:- If
pData
is set toIntrPtr.Zero
, the size of the buffer required to contain the data is returned in thepcbSize
variable. - Otherwise,
pData
must be a pointer to allocated memory that can hold theRAWINPUT
structure provided by theWM_INPUT
message. When the method call returns, the contents of the allocated memory will be either the message's header information or input data, depending on the value ofuiCommand
.
- If
- pcbSize
A variable that returns or specifies the size of the data pointed to bypData
. - cbSizeHeader
The size of aRAWINPUTHEADER
structure.
In order to ensure that enough memory is allocated to store the desired information, the GetRawInputData
method should first be called with pData
set to IntPtr.Zero
.
uint dwSize = 0; GetRawInputData( message.LParam, RID_INPUT, IntPtr.Zero, ref dwSize, (uint)Marshal.SizeOf( typeof( RAWINPUTHEADER )));
Following this call, the value of dwSize
will correspond to the number of bytes needed to store the raw input data (as indicated by the use of the RID_INPUT
flag).
It is then possible to allocate the right amount of memory; in this case, the pointer is stored in a variable called buffer
.
IntPtr buffer = Marshal.AllocHGlobal( (int)dwSize );
Now that buffer
points to a suitable location, GetRawInputData
can be called again to populate the allocated memory with the RAWINPUT
structure from the current message. If it succeeds, the method returns the size of the data it retrieved, so it is worth checking that this matches the result of the previous call before continuing.
if( GetRawInputData( message.LParam, RID_INPUT, buffer, ref dwSize, (uint)Marshal.SizeOf( typeof( RAWINPUTHEADER ))) == dwSize ) //do something with the data
Once this has been done, the contents pointed to by buffer
can be marshaled into a RAWINPUT
structure, which gives easy access to the data's various members, as illustrated in the following section.
RAWINPUT raw = (RAWINPUT)Marshal.PtrToStructure(
buffer, typeof( RAWINPUT ));
Processing the data
As mentioned above, the WM_INPUT
message contains raw data encapsulated in a RAWINPUT
structure. As with the RAWINPUTDEVICE
structure described in the previous section, this structure is redefined in the InputDevice
class as follows.
[StructLayout(LayoutKind.Explicit)] internal struct RAWINPUT { [FieldOffset(0)] public RAWINPUTHEADER header; [FieldOffset(16)] public RAWMOUSE mouse; [FieldOffset(16)] public RAWKEYBOARD keyboard; [FieldOffset(16)] public RAWHID hid; }
Following the second call to GetRawInputData
(see previous section), the raw
structure will contain the following information:
A RAWINPUTHEADER
structure called header
, which contains information on the message and the device that triggered it.
A second structure of type RAWKEYBOARD
called keyboard.
This could also be a RAWMOUSE
or RAWHID
structure called mouse
or hid
, depending on the type of device.
The RAWINPUTHEADER
structure is laid out as follows:
[StructLayout(LayoutKind.Sequential)] internal struct RAWINPUTHEADER { [MarshalAs(UnmanagedType.U4)] public int dwType; [MarshalAs(UnmanagedType.U4)] public int dwSize; public IntPtr hDevice; [MarshalAs(UnmanagedType.U4)] public int wParam; }
Its members return the following information:
- dwType
The type of raw input the message represents. The values can beRIM_TYPEHID
(2),RIM_TYPEKEYBOARD
(1), orRIM_TYPEMOUSE
(0). - dwSize
The size of all the information in the message (header and input data included). - hDevice
The handle of the device which triggered the message. - wParam
ThewParam
data from theWM_INPUT
message.
The second structure will be a RAWMOUSE
, a RAWKEYBOARD
, or a RAWHID
type. For the sake of completeness, the InputDevice
class does contain definitions for RAWMOUSE
and RAWHID
, though it is only designed to process keyboard information.
The keyboard information is provided by a RAWKEYBOARD
structure, laid out as follows.
[StructLayout(LayoutKind.Sequential)] internal struct RAWKEYBOARD { [MarshalAs(UnmanagedType.U2)] public ushort MakeCode; [MarshalAs(UnmanagedType.U2)] public ushort Flags; [MarshalAs(UnmanagedType.U2)] public ushort Reserved; [MarshalAs(UnmanagedType.U2)] public ushort VKey; [MarshalAs(UnmanagedType.U4)] public uint Message; [MarshalAs(UnmanagedType.U4)] public uint ExtraInformation; }
Since the InputDevice
class is only interested in keyboard input, the ProcessInputCommand
method starts by checking the header to make sure this is a keyboard message before proceeding:
if( raw.header.dwType == RIM_TYPEKEYBOARD )
The next step is to filter the message to see if it is a key down event. This could just as easily be a check for a key up event; the point here is to filter the messages so that the same keystroke isn't processed for both key down and key up events.
private const int WM_KEYDOWN = 0x0100; private const int WM_SYSKEYDOWN = 0x0104; ... if (raw.keyboard.Message == WM_KEYDOWN || raw.keyboard.Message == WM_SYSKEYDOWN) { //Do something like... int vkey = raw.keyboard.vkey; MessageBox.Show(vkey.ToString()); }
At this point, the InputDevice
class retrieves further information about the message and the device that triggered it, and raises its custom KeyPressed
event. The following sections describe how to get information on the devices.
Retrieving the list of input devices
Although this step isn't required to handle raw input, the list of input devices can be useful. The sample application retrieves a list of devices, filters it for keyboards, and then returns the number of keyboards. This is part of the information returned by the KeyControlEventArgs
in the InputDevice
class's KeyPressed
event.
The first step is to import the necessary method from user32.dll:
[DllImport("User32.dll")] extern static uint GetRawInputDeviceList(IntPtr pRawInputDeviceList, ref uint uiNumDevices, uint cbSize);
The method's arguments are as follows:
- pRawInputDeviceList: Depending on the desired result, this can be one of two things:
IntPtr.Zero
if the purpose is only to retrieve the number of devices.- A pointer to an array of
RAWINPUTDEVICELIST
structures if the purpose of the method call is to retrieve the complete list of devices.
- uiNumDevices: A reference to an unsigned integer to store the number of devices.
- If the
pRawInputDeviceList
argument isIntPtr.Zero
, then this variable will return the number of devices. - If the
pRawInputDeviceList
argument is a pointer to an array, then this variable must contain the size of the array. This allows the method to allocate memory appropriately. IfuiNumDevices
is less than the size of the array in this case, the method will return the size of the array, but an "insufficient buffer" error will occur and the method will fail.
- If the
- cbSize: The size of a
RAWINPUTDEVICELIST
structure.
In order to ensure that the first and second arguments are correctly configured when the list of devices is required, the method should be set up in three stages.
First, it should be called with pRawInputDeviceList
set to IntPtr.Zero
. This will ensure that the variable in the second argument (deviceCount
here) is filled with the correct number of devices. The result of this call should be checked, as an error means that the code can proceed no further.
uint deviceCount = 0; int dwSize = (Marshal.SizeOf( typeof( RAWINPUTDEVICELIST ))); if( GetRawInputDeviceList( IntPtr.Zero, ref deviceCount, (uint)dwSize ) == 0 ) { //continue retrieving the information (see below) } else { //handle the error or throw an exception }
Once the deviceCount
variable contains the right value, the correct amount of memory can be allocated and associated with a pointer:
IntPtr pRawInputDeviceList = Marshal.AllocHGlobal((int)(dwSize * deviceCount ));
And the method can be called again, this time to fill the allocated memory with an array of RAWINPUTDEVICELIST
structures:
GetRawInputDeviceList( pRawInputDeviceList, ref deviceCount, (uint)dwSize );
The pRawInputDeviceList
data can then be converted into individual RAWINPUTDEVICELIST
structures. In the example below, a for
loop has been used to iterate through the devices, so i
represents the position of the current device in the array.
for( int i = 0; i < deviceCount; i++ ) { RAWINPUTDEVICELIST rid = (RAWINPUTDEVICELIST)Marshal.PtrToStructure( new IntPtr(( pRawInputDeviceList.ToInt32() + ( dwSize * i ))), typeof( RAWINPUTDEVICELIST )); //do something with the information (see section on GetRawInputDeviceInfo) }
When any subsequent processing is completed, the memory should be deallocated.
Marshal.FreeHGlobal( pRawInputDeviceList );
Getting information on specific devices
Once GetRawInputDeviceList
has been used to retrieve an array of RAWINPUTDEVICELIST
structures as well as the number of items in the array, it is possible to use GetRawInputDeviceInfo
to retrieve specific information on each device.
First, the method is imported from user32.dll:
[DllImport("User32.dll")] extern static uint GetRawInputDeviceInfo(IntPtr hDevice, uint uiCommand, IntPtr pData, ref uint pcbSize);
Its arguments are as follows:
- hDevice
The device handle returned in the correspondingRAWINPUTDEVICELIST
structure. - uiCommandA flag to set what type of data will be returned in
pData
. Possible values areRIDI_PREPARSEDDATA
(0x20000005 - returns previously parsed data),RIDI_DEVICENAME
(0x20000007 - a string containing the device name), orRIDI_DEVICEINFO
(0x2000000b - anRIDI_DEVICE_INFO
structure) - pData: Depending on the desired result, this can be one of two things:
- If
pData
is set toIntrPtr.Zero
, the size of the buffer required to contain the data is returned in thepcbSize
variable. - Otherwise,
pData
must be a pointer to allocated memory that can hold the type of data specified byuiCommand
.
(Note: ifuiCommand
is set toRIDI_DEVICEINFO
, then thecbSize
member of theRIDI_DEVICE_INFO
structure must be set to the size of the structure)
- If
- pcbSize
A variable that returns or specifies the size of the data pointed to bypData
. IfuiCommand
isRIDI_DEVICENAME
,pcbSize
will indicate the number of characters in the string. Otherwise, it indicates the number of bytes in the data.
The example code uses a for
loop to iterate through the available devices as indicated by the deviceCount
variable. At the start of each loop, a RAWINPUTDEVICELIST
structure called rid
is filled with the information on the current device (see GetRawInputDeviceList
section above).
In order to ensure that enough memory is allocated to store the desired information, the GetRawInputDeviceInfo
method should first be called with pData
set to IntPtr.Zero
. The handle in the hDevice
parameter is provided by the rid
structure containing information on the current device in the loop.
uint pcbSize = 0; GetRawInputDeviceInfo( rid.hDevice, RIDI_DEVICENAME, IntPtr.Zero, ref pcbSize );
In this example, the purpose is to find out the device name, which will be used to look up information on the device in the Registry.
Following this call, the value of pcbSize
will correspond to the number of characters needed to store the device name. Once the code has checked that pcbSize
is greater than 0, the appropriate amount of memory can be allocated.
IntPtr pData = Marshal.AllocHGlobal( (int)pcbSize );
And the method can be called again, this time to fill the allocated memory with the device name. The data can then be converted into a C# string
for ease of use.
string deviceName; GetRawInputDeviceInfo( rid.hDevice, RIDI_DEVICENAME, pData, ref pcbSize ); deviceName = (string)Marshal.PtrToStringAnsi( pData );
The list will also include "root" keyboard and mouse devices that are used for Terminal Services or Remote Desktop connections. As these don't interest us here, the following code will skip those when they are encountered in the loop.
if (deviceName.ToUpper().Contains("ROOT")) { continue; //Drop into next iteration of the loop }
The next stage is to identify whether the enumerated device is a keyboard.
if( deviceType.Equals( "KEYBOARD" ) || deviceType.Equals( "HID" )) { //It's a keyboard – or a USB device that could be a keyboard //Do something }
The rest of the code then retrieves information about the device and checks the Registry to see whether the device is really a keyboard.
Reading device information from the Registry
Following the above code, deviceName
will have a value similar to the following:
\\??\\ACPI#PNP0303#3&13c0b0c5&0#{884b96c3-56ef-11d1-bc8c-00a0c91405dd}
This string mirrors the device's entry in the Registry; parsing it therefore allows us to find the relevant Registry key, which contains further information on the device. So the first step is to break down the relevant part of the string:
// remove the \??\ item = item.Substring( 4 ); string[] split = item.Split( '#' ); string id_01 = split[0]; // ACPI (Class code) string id_02 = split[1]; // PNP0303 (SubClass code) string id_03 = split[2]; // 3&13c0b0c5&0 (Protocol code) // The final part is the class GUID and is not needed here
The Class code, SubClass code and Protocol retrieved this way correspond to the device's path under HKEY_LOCAL_MACHINE\SYSTEM\
CurrentControlSet
, so the next stage is to open that key:
RegistryKey OurKey = Registry.LocalMachine; string findme = string.Format( @"System\CurrentControlSet\Enum\{0}\{1}\{2}", id_01, id_02, id_03 );
The information we are interested in is the device's friendly description, and its class, as this latter will tell us if it's a keyboard:
string deviceDesc = (string)OurKey.GetValue( "DeviceDesc" ); string deviceClass = (string)OurKey.GetValue( "Class" ); if( deviceClass.ToUpper().Equals( "KEYBOARD" )){ isKeyboard = true; } else{ isKeyboard = false; }
All that is left then is to deallocate any allocated memory and do something with the data that has been retrieved.
Conclusion
Although the .NET Framework offers methods for most common purposes, the Raw Input API offers a more flexible approach to device data. The enclosed code and the explanations in this article will hopefully prove a useful starting point for anyone looking to handle multiple keyboards in an XP or Vista based application.
Sources
This article gives an overview of the different steps required to implement the Raw Input API. For further information on handling raw input:
- MSDN's information on Raw Input[^].
- If you are interested in monitoring other HIDs, the MSDN article Hardware IDs on HIDs[^] explains more about Usage Pages.
History
- March 2007 - Added WPF sample in response to user request
- January 2007 - Original version