{A}
{S0}
简介
曾经有一段时间,当你是幸运的,如果一台PC有这么多鼠标,但今天,它是常见的有多种游戏控制器到触摸屏人机接口设备(HID)的。特别是,用户可以连接多个键盘到他们的电脑。然而,通常的键盘。NET Framework的编程方法提供没有办法来区分不同的键盘输入。处理KeyPress事件的任何应用程序,将接收来自所有连接的键盘输入,好像他们是一个单一的设备。
的Windows XP及以上现在支持"原始输入API,它允许程序处理从任何连接的人机接口设备直接输入。拦截信息和过滤键盘,使应用程序,以确定哪些设备触发的消息。例如,这可以让两个不同的窗口,从不同的键盘输入响应。
本文所附的代码演示了如何处理原始输入,以处理击键,并确定他们来自哪个设备。在附加邮编InputDevice.cs文件中包含的原始输入API的包装,将此文件复制到自己的项目,并按照"使用的代码"的说明,如果你要运行示例应用程序的情况下使用类。{A6}背景
我最近发表的文章对实施跨我的文章{A7}来了,我们讨论了我的代码是否能够适应他的需求。事实上,它横空出世,它不能和原始输入API的解决方案。
不幸的是,有极少数的键盘相关的原始输入样本在线,所以当史蒂夫已经完成了他的代码的工作范例,我愿意写这篇文章,以便将来面对这个问题。NET开发人员就不必看远,以找到解决办法。虽然我对代码所做的轻微调整,主要是史蒂夫的工作,我感谢他分享。注:2007年3月,你也可以下载史蒂夫的WPF示例说明了Windows Vista中使用的的WndProc。然而,本文只介绍Windows XP的源代码。
请注意,这将只能工作在Windows XP或在非终端服务器环境后,附加的示例项目的Visual Studio 2005。{A8}支持不同的设备
附带的代码是一个通用的解决方案,主要是反映MSDN上给出的示例代码。不同的设备会以不同的方式,您可能需要修改代码以满足您使用的键盘。不幸的是,我们不会总是能够帮助设备特定查询,因为我们不会有你有相同的设备。史蒂夫梅塞尔已与不同的键盘测试代码,但是,并相信它将工作与大多数设备提供他们正确安装。{A9}使用代码
所有相关的原始输入处理的代码封装在输入设备类,并使用它,是实施三个简单的步骤问题:
1。实例化一个输入设备对象
输入设备类的构造函数接受一个参数,这是当前窗口的句柄。InputDevice id = new InputDevice( Handle );
的处理,以确保该窗口将继续侦听事件,即使它不具有焦点。
2。处理keyPressed事件
当按下一个键,输入设备类提出了一个自定义的含有一些KeyControlEventArgs keyPressed事件。这需要的类型DeviceEventHandler,可设置如下方法处理:{C}
处理事件的方法,就可以执行任何行动的基础上的KeyControlEventArgs参数的内容需要。本文所附的示例应用程序简单地使用这些值来填充对话框。
3。重写WndProc方法
在其目前的形式,输入设备类工程拦截消息窗口,以包含原始输入数据的过程中WM_INPUT消息。聆听原始输入的窗口,因此需要重写自己的Windows程序,并通过其所有邮件的实例化的输入设备对象。protected override void WndProc( ref Message message )
{
if( id != null )
{
id.ProcessMessage( message );
}
base.WndProc( ref message );
}
写在这篇文章中使用的代码后,史蒂夫决定输入设备类可以真正独立的应用程序使用它从{A10}继承。然而,这篇文章的目的主要是为了说明使用原始输入API,我们决定保持其原来的形式的代码。
本文的其余部分,介绍了如何处理从C#应用程序在示例应用程序的输入设备类所示,"原始输入"。{A11}实现一个Windows API原始输入处理程序
{A12}作为一个接口设备提供的原始数据。在键盘的情况下,这个数据通常是由Windows截获并翻译成。NET框架的关键事件提供的信息。例如,窗口管理器转换成虚拟键的左右按键的设备特定的数据。
然而,正常的Windows经理不提供任何信息,有哪些设备收到的击键,它只是捆绑所有的键盘事件为一类的行为就好像有一个键盘。
这是原始输入API是有用的。它允许应用程序直接从设备接收数据从Windows的最小干预。它提供的信息的一部分,是触发事件的设备的身份。
在Windows XP和Vista的user32.dll中包含用于处理原始输入下列方法:RegisterRawInputDevices允许申请登记要监视的输入设备。GetRawInputData从输入设备获取数据。GetRawInputDeviceList检索连接到系统的输入设备列表。GetRawInputDeviceInfo检索设备的信息。
下面的小节给出了这四种方法如何被用来处理原始数据从键盘的概述。注册原始输入设备
默认情况下,没有应用程序接收原始输入。因此,第一步是注册的输入设备,将提供所需的原始数据,他们将要处理这个数据与窗口关联。
要做到这一点,RegisterRawInputDevices方法是从user32.dll中导入:[DllImport("User32.dll")]
extern static bool RegisterRawInputDevices(
RAWINPUTDEVICE[] pRawInputDevice,
uint uiNumDevices, uint cbSize);
要确定哪些设备应当予以登记,该方法接受一个RAWINPUTDEVICE结构数组。其他两个参数是数组中的项数,并在RAWINPUTDEVICE结构的字节数。
RAWINPUTDEVICE结构定义在C项目WINDOWS.H,但这个文件是不是在C#中使用结构已重新定义为输入设备类的成员。[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;
}
每个RAWINPUTDEVICE结构添加到数组中包含利益的应用程序的设备类型的信息。例如,它可以注册键盘和电话设备。该结构采用了以下信息:用法页:顶级的HID"用法"页。对于大多数HIDS,包括键盘,这是0x01。使用编号:一个数字,指示应监测设备的精确类型。对于键盘,这是为0x06。 (使用页和用法ID值的列表可以发现{A14})标志:这些确定的数据应如何处理,是否应该被忽略某些类型。 {A15}如果你不已经有一个)。目标手柄:窗口,这将是从这个特定类型的设备监测数据的处理。
在这种情况下,我们只在键盘感兴趣,所以数组中只有一个成员,并设置如下:RAWINPUTDEVICE[] rid = new RAWINPUTDEVICE[1];
rid[0].usUsagePage = 0x01;
rid[0].usUsage = 0x06;
rid[0].dwFlags = RIDEV_INPUTSINK;
rid[0].hwndTarget = hwnd;
在这里,代码只定义RIDEV_INPUTSINK标志,这意味着该窗口将始终接收输入消息,即使它不再是重点。这将使两个窗口,以应对不同的键盘事件,至少即使其中一人将不会被激活。
就可以使用与阵列,该方法可以被称为登记窗口的任何设备,确定自己作为键盘的兴趣:RegisterRawInputDevices(rid, (uint)rid.Length,
(uint)Marshal.SizeOf(rid[0]))
一旦已注册的设备类型,这样,应用程序就可以开始使用GetRawInputData在下一节中描述的方法来处理数据。{A16}检索和处理原始输入
注册的设备类型是当,应用程序开始接收原始输入。每当注册设备使用,Windows生成一个WM_INPUT消息,其中包含从设备未处理的数据。
注册设备相关联,在上一节所述的每一个窗口的句柄,因此必须检查接收到的消息,并采取适当的行动时WM_INPUT之一是检测。在示例应用程序,输入设备类保健检查WM_INPUT消息,因此,所有的主窗口是覆盖其基地的WndProc方法获得的消息,并通过任何有效的输入设备对象:protected override void WndProc( ref Message message ) {
if( id != null ) {
id.ProcessMessage( message );
}
base.WndProc( ref message );
}
ProcessMessage方法在输入设备过滤器的消息,每当收到一个WM_INPUT调用ProcessInputCommand。任何其他类型的消息将通过调用基地的WndProc下降,因此应用程序会正常响应其他事件。public void ProcessMessage( Message message ) {
switch( message.Msg ) {
case WM_INPUT: {
ProcessInputCommand( message );
}
break;
}
}
ProcessInputCommand然后使用GetRawInputData方法来检索消息的内容,并转换成有意义的信息。从邮件中检索信息
,为了处理WM_INPUT消息数据,GetRawInputData方法是从user32.dll中导入:[DllImport("User32.dll")]
extern static uint GetRawInputData(IntPtr hRawInput, uint uiCommand,
IntPtr pData, ref uint pcbSize, uint cbSizeHeader);
该方法采用下列参数:hRawInput
RAWINPUT结构包含数据,在WM_INPUT消息的lParam提供的处理。uiCommand
一个标志设置是否要撷取的输入数据或从RAWINPUT结构的头信息。可能的值有RID_INPUT(0x10000003)或RID_HEADER(0x10000005)。PDATA:
根据期望的结果,这可能是两件事情之一:如果PDATA设置IntrPtr.Zero,需要包含的数据的缓冲区的大小是在pcbSize变量返回。否则,PDATA必须是一个分配的内存,可容纳RAWINPUT结构WM_INPUT消息的指针。在方法调用返回时,分配的内存的内容将是消息的标题信息或输入数据,根据uiCommand价值。pcbSize
返回或指定的数据大小的一个变量,指出由PDATA。cbSizeHeader
一个RAWINPUTHEADER结构的大小。
为了确保有足够的内存分配给存储所需要的信息,GetRawInputData方法首先应该被称为PDATA设置到IntPtr.Zero。uint dwSize = 0;
GetRawInputData( message.LParam, RID_INPUT,
IntPtr.Zero, ref dwSize,
(uint)Marshal.SizeOf( typeof( RAWINPUTHEADER )));
此调用的dwSize值将对应需要存储的原始输入数据(如使用的RID_INPUT标志表示)的字节数。
然后分配正确的内存量,在这种情况下,指针是存储在一个变量的缓冲区。IntPtr buffer = Marshal.AllocHGlobal( (int)dwSize );
缓冲点,以一个合适的位置,GetRawInputData可以再次调用RAWINPUT结构从目前的消息来填充分配的内存。如果成功,该方法返回的数据检索的大小,所以这是值得的检查,这相匹配的结果,然后再继续先前呼叫。if( GetRawInputData( message.LParam, RID_INPUT,
buffer, ref dwSize, (uint)Marshal.SizeOf( typeof( RAWINPUTHEADER )))
== dwSize )
//do something with the data
一旦这项工作已经完成,内容指出,由缓冲区可以封送处理成RAWINPUT结构,可以轻松访问各种数据的成员,如以下部分所示。
处理数据
如上所述,WM_INPUT消息包含在RAWINPUT结构封装的原始数据。由于在上一节中所述的RAWINPUTDEVICE结构,这种结构在输入设备类重新定义如下。[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;
}
之后的第二次调用GetRawInputData(见上一节),原料结构将包含以下信息:
所谓的头一个RAWINPUTHEADER结构,其中包含的消息,并触发它的设备的信息。
一个类型RAWKEYBOARD第二结构称为键盘。这也可能是一个RAWMOUSE或RAWHID结构被称为鼠标或HID,取决于设备类型。
RAWINPUTHEADER结构布局如下:[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;
}
其成员将返回以下信息:dwType
的消息表示原始输入的类型。的值可以RIM_TYPEHID(2),RIM_TYPEKEYBOARD(1),或RIM_TYPEMOUSE(0)。的dwSize
消息中的所有信息(包括头和输入数据)的大小。hDevice
处理设备,由此引发的消息。wParam参数
从WM_INPUT消息的wParam数据。
第二个结构将是一个RAWMOUSE,RAWKEYBOARD,或RAWHID类型。为了完整起见,输入设备类不包含RAWMOUSE和RAWHID的定义,虽然它只是设计过程中键盘信息。
键盘信息提供一个RAWKEYBOARD结构如下规定。
[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;
}
由于只关心键盘输入的输入设备类,ProcessInputCommand方法首先检查头,以确保这是一个键盘消息,然后再继续:if( raw.header.dwType == RIM_TYPEKEYBOARD )
下一步是过滤消息,看它是否是一个down事件的关键。这可以很容易地检查了事件的关键;这里的要点是使同一按键键并了事件的关键不处理的消息过滤器。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());
}
在这一点上,输入设备类检索有关消息,并触发它的设备的进一步信息,并提出了其自定义的keyPressed事件。以下各节描述如何在设备上的信息。{A17}检索输入设备清单
虽然这一步是不需要处理原始输入,输入设备清单可以是有用的。示例应用程序检索的设备清单,过滤器键盘,然后返回键盘。这是KeyControlEventArgs返回的信息输入设备类的keyPressed事件的一部分。
第一步是从user32.dll中导入必要的方法:
[DllImport("User32.dll")]
extern static uint GetRawInputDeviceList(IntPtr pRawInputDeviceList,
ref uint uiNumDevices, uint cbSize);
方法的参数如下:pRawInputDeviceList:根据期望的结果,这可以是一两件事情:如果目的IntPtr.Zero是只检索设备的数量。RAWINPUTDEVICELIST结构的阵列,如果方法调用的目的是为了获取设备的完整列表的指针。uiNumDevices:一个无符号整数,存储设备的数量的参考。如果pRawInputDeviceList说法是IntPtr.Zero,那么这个变量将返回设备的数量。如果pRawInputDeviceList参数是一个指向数组的指针,那么这个变量必须包含数组的大小。这允许适当的方法来分配内存。如果uiNumDevices小于在这种情况下数组的大小,该方法将返回数组的大小,但一个"缓冲区不足"的错误会发生,该方法将失败。cbSize:一个RAWINPUTDEVICELIST结构的大小。
为了确保第一和第二个参数是正确配置需要的设备清单时,该方法应设立三个阶段。
首先,它应该被称为pRawInputDeviceList设置到IntPtr.Zero。这将确保在第二个参数(deviceCount在这里)的变量是正确的设备填补。应检查此调用的结果,因为一个错误代码可以进行没有进一步。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
}
一旦deviceCount变量包含正确的价值,正确的存储器可以分配和指针关联:IntPtr pRawInputDeviceList =
Marshal.AllocHGlobal((int)(dwSize * deviceCount ));
和方法,可以再次调用,此时填充RAWINPUTDEVICELIST结构数组分配内存:GetRawInputDeviceList( pRawInputDeviceList, ref deviceCount, (uint)dwSize );
pRawInputDeviceList数据就可以被转换成单个的RAWINPUTDEVICELIST结构。在下面的例子中,for循环被用于遍历的设备,所以我代表数组中的当前设备的位置。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)
}
当任何后续处理完成后,内存被释放。
{A18}在特定设备上获取信息Marshal.FreeHGlobal( pRawInputDeviceList );
一旦GetRawInputDeviceList已用于检索RAWINPUTDEVICELIST结构的阵列,以及阵列中的项目数量,它是可以使用GetRawInputDeviceInfo的检索每个设备的具体信息。
首先,该方法是从user32.dll中导入:[DllImport("User32.dll")]
extern static uint GetRawInputDeviceInfo(IntPtr hDevice,
uint uiCommand, IntPtr pData, ref uint pcbSize);
它的参数如下:hDevice
设备句柄返回相应的RAWINPUTDEVICELIST结构。uiCommandA标志设置将在PDATA返回什么类型的数据。可能的值有RIDI_PREPARSEDDATA(0x20000005 - 返回以前分析过的数据),RIDI_DEVICENAME(0x20000007 - 一个字符串,其中包含的设备名称),或RIDI_DEVICEINFO(0x2000000b - RIDI_DEVICE_INFO结构)PDATA:根据期望的结果,这可以是一两件事情:如果PDATA设置IntrPtr.Zero,需要包含的数据的缓冲区的大小是在pcbSize变量返回。否则,PDATA必须分配的内存,可容纳uiCommand指定的数据类型的指针。
(注:如果uiCommand设置到RIDI_DEVICEINFO,然后RIDI_DEVICE_INFO结构的cbSize成员必须设置结构的大小)pcbSize
返回或指定的数据大小的一个变量,指出由PDATA。如果uiCommand是RIDI_DEVICENAME,pcbSize会显示字符串中的字符数。否则,它表示数据的字节数量。
示例代码使用一个for循环迭代通过提供设备deviceCount变量表示。在每个循环的开始,一个RAWINPUTDEVICELIST结构称为RID是与当前设备上的信息填写(见以上GetRawInputDeviceList节)。
为了确保有足够的内存分配给存储所需要的信息,GetRawInputDeviceInfo方法首先应该被称为PDATA设置到IntPtr.Zero。在hDevice参数的处理是由RID包含回路中的电流设备信息结构。uint pcbSize = 0;
GetRawInputDeviceInfo( rid.hDevice, RIDI_DEVICENAME, IntPtr.Zero,
ref pcbSize );
在这个例子中,目的是要找出设备的名称,将用于查找设备上的注册表中的信息。
此调用,pcbSize价值将对应所需的存储设备名称的字符数。一旦代码检查,pcbSize大于0,适量的内存可以分配。IntPtr pData = Marshal.AllocHGlobal( (int)pcbSize );
和方法,可以再次调用,此时填补分配的内存的设备名称。的数据可以被转换成一个C#字符串的易用性。string deviceName;
GetRawInputDeviceInfo( rid.hDevice, RIDI_DEVICENAME, pData, ref pcbSize );
deviceName = (string)Marshal.PtrToStringAnsi( pData );
该列表还包括"根"的键盘和鼠标设备,终端服务或远程桌面连接使用。由于这些不感兴趣,我们在这里,下面的代码将跳过这些,当他们在遇到循环。if (deviceName.ToUpper().Contains("ROOT"))
{
continue; //Drop into next iteration of the loop
}
下一个阶段是,以确定是否枚举设备是键盘。if( deviceType.Equals( "KEYBOARD" ) || deviceType.Equals( "HID" ))
{
//It's a keyboard 锟?or a USB device that could be a keyboard<
/span>
//Do something
}
代码的其余部分,然后检索设备的有关信息,并检查注册表,看设备是否是一个真正的键盘。{A19}从注册表中读取设备信息
上面的代码,DEVICENAME的值都将类似于以下内容:\\??\\ACPI#PNP0303#3&13c0b0c5&0#{884b96c3-56ef-11d1-bc8c-00a0c91405dd}
这个字符串反映设备的注册表项解析,因此它使我们能够找到相关的注册表项,其中包含设备的进一步信息。因此,第一步是要打破字符串的相关部分:// 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
类代码,子类的代码和"议定书"的检索,这种方式相对应的设备的路径HKEY_LOCAL_MACHINE \ SYSTEM \ CurrentControlSet下,使下一阶段的开放,关键:RegistryKey OurKey = Registry.LocalMachine;
string findme = string.Format(
@"System\CurrentControlSet\Enum\{0}\{1}\{2}",
id_01, id_02, id_03 );
我们感兴趣的是信息设备的友好描述,和它的类,因为后者会告诉我们,如果这是一个键盘:
string deviceDesc = (string)OurKey.GetValue( "DeviceDesc" );
string deviceClass = (string)OurKey.GetValue( "Class" );
if( deviceClass.ToUpper().Equals( "KEYBOARD" )){
isKeyboard = true;
}
else{
isKeyboard = false;
}
,然后是左是释放任何分配的内存,并与已检索到的数据的东西。{A20}结论
虽然。NET Framework中提供的最常见的用途的方法,原始输入API提供了一个更灵活的方式,设备的数据。包含的代码,在这篇文章中解释,将有望证明一个有用的出发点,在XP或Vista的应用程序来处理多个键盘的人。{A21}来源
本文给出了一个需要实现的原始输入API的不同步骤的概述。进一步处理原始输入信息:{A22}。如果你是在监测其他HIDS感兴趣,{A23}解释有关使用页。历史2007年3月 - 增加了WPF的样本响应用户的请求2007年1月 - 原始版本