|
|||||||||||||||||||||
|
|||||||||||||||||||||
|
Announcements
Chapters
Services
Feature Zones
|
Introduction
Retrieving and processing raw input Retrieving the list of input devices Getting information on specific devices Reading device information from the Registry Sources IntroductionThere 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 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. BackgroundI recently published an article on implementing a low-level keyboard hook in C#[^] using the 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 devicesThe 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 codeAll the code related to raw input handling is encapsulated in the 1. Instantiate an The 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 When a key is pressed, the 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 3. Override the In its present form, the 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 The rest of this article describes how to handle "raw input" from a C# application, as illustrated by the Implementing a Windows API Raw Input handlerMSDN 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 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:
The following sections give an overview of how these four methods are used to process raw data from keyboards. Registering raw input devicesBy 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 [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 The [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
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 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 Retrieving and processing raw inputWhen the type of device is registered, the application begins to receive raw input. Whenever a registered device is used, Windows generates a 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 protected override void WndProc( ref Message message ) {
if( id != null ) {
id.ProcessMessage( message );
}
base.WndProc( ref message );
}
The public void ProcessMessage( Message message ) {
switch( message.Msg ) {
case WM_INPUT: {
ProcessInputCommand( message );
}
break;
}
}
Retrieving the information from the messageIn order to process the data in [DllImport("User32.dll")]
extern static uint GetRawInputData(IntPtr hRawInput, uint uiCommand,
IntPtr pData, ref uint pcbSize, uint cbSizeHeader);
The method uses the following parameters:
In order to ensure that enough memory is allocated to store the desired information, the uint dwSize = 0;
GetRawInputData( message.LParam, RID_INPUT,
IntPtr.Zero, ref dwSize,
(uint)Marshal.SizeOf( typeof( RAWINPUTHEADER )));
Following this call, the value of It is then possible to allocate the right amount of memory; in this case, the pointer is stored in a variable called IntPtr buffer = Marshal.AllocHGlobal( (int)dwSize );
Now that 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 RAWINPUT raw = (RAWINPUT)Marshal.PtrToStructure(
buffer, typeof( RAWINPUT ));
Processing the dataAs mentioned above, the [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 A A second structure of type The [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:
The second structure will be a The keyboard information is provided by a [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 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 Retrieving the list of input devicesAlthough 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 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:
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 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 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 GetRawInputDeviceList( pRawInputDeviceList, ref deviceCount, (uint)dwSize );
The 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 devicesOnce 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:
The example code uses a In order to ensure that enough memory is allocated to store the desired information, the 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 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 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 RegistryFollowing the above code, \\??\\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 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. ConclusionAlthough 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. SourcesThis article gives an overview of the different steps required to implement the Raw Input API. For further information on handling raw input:
History
| ||||||||||||||||||||