实现多键盘输入选择性控制
系统连接多个键盘设备时,可能需要使用某个键盘作为标准输入,另一个键盘用作其他特殊任务。比如,当打开记事本后,按下键盘1上的字母a键,你希望其写入记事本,而敲击键盘2上的a键时,记事本上并没有记录字母a,取而代之的是通知后台执行某一特殊任务。如何控制键盘输入并根据键盘的输入信息决定下一步的动作或者任务,可能很多读者知道可以采用键盘过滤钩子技术实现,通过Windows自带的APIs函数或者通过自定义驱动的方式均可实现键盘过滤,然而如何判断键盘消息来自哪个键盘设备,键盘过滤技术是无法做到的,系统是否有方法可以甄别来自两个不同设备的输入,RawInput技术即可实现在不查找和打开输入设备的情况下,甄别来自两个不同设备的输入并准确获取输入数据。本文将结合原始输入及键盘过滤技术实现多键盘输入的选择性控制。
RawInput技术获取特定设备的原始输入应用程序想获取原始数据,则必须注册其想要获取原始输入的那些设备。为了注册这个设备,应用程序首先必须创建一个指明其所希望接受设备类别的(toplevelcollection—TLC)RAWINPUTDEVICE结构。TLC被定义成为UsagePage(设备类)和Usage(设备类内的具体设备)。例如,为了从键盘获取原始输入,设置UsagePage=1andUsage=6,应用程序调用RegisterRawInputDevice完成设备注册。
//注册原始输入设备 RAWINPUTDEVICErawInputDevice[1]; rawInputDevice[0].usUsagePage=1; rawInputDevice[0].usUsage=6; rawInputDevice[0].dwFlags=RIDEV_INPUTSINK; rawInputDevice[0].hwndTarget=hWnd; RegisterRawInputDevices(rawInputDevice,1,sizeof(rawInputDevice[0]));
rawInputDevice[0].dwFlags=RIDEV_INPUTSINK意味着即使某窗口失去焦点位置,仍然会一直接收输入消息,这使得多个窗口可分别响应来自不同键盘的事件。当设备注册成功后,应用程序的消息队列就会得到一个WM_INPUT消息,然后再调用GetRawInputData函数处理原始输入数据。
GetRawInputData函数共包含5个参数:
lhRawInput:WM_INPUT消息的lParam参数地址
lcommand:获取原始输入数据或者RAWINPUT结构体头信息标志,为了获取原始信息,此标记设置为RID_INPUT
lpData:若设置为NULL,则表示原始数据缓冲区大小,不然则是一个指定大小的缓冲区地址,用以存放原始数据信息
lsize:返回即数据缓冲区大小
lsizeHeader:RAWINPUTHEADER结构体大小
下面给出的部分代码即通过上述函数读取原始数据信息并输出。
GetRawInputData((HRAWINPUT)lParam,RID_INPUT,NULL,bufferSize,sizeof (RAWINPUTHEADER)); LPBYTEdataBuffer=newBYTE[bufferSize];//分配指定的缓冲区大小 //获取原始输入数据,读入dataBuffer GetRawInputData((HRAWINPUT)lParam,RID_INPUT,dataBuffer,bufferSize,sizeof (RAWINPUTHEADER)); RAWINPUT*raw=(RAWINPUT*)dataBuffer; //Getthevirtualkeycodeofthekeyandreportit USHORTvirtualKeyCode=raw-data.keyboard.VKey; USHORTkeyPressed=raw-data.keyboard.FlagsRI_KEY_BREAK?0:1; WCHARtext[128]; swprintf_s(text,128,LRawInput:%X(%d)\n,virtualKeyCode,keyPressed);
通过上面的代码,我们已经获取到了设备输出的原始信息。下面我们需要通过函数
GetRawInputDeviceInfo来判断输出的原始信息来自哪个键盘设备。现假设按下的键为数字键盘值7且来自设备A时,拦截此信息即不写入记事本,具体代码如下:
//为键盘设备名准备缓冲区大小 GetRawInputDeviceInfo (raw-header.hDevice, RIDI_DEVICENAME, NULL, bufferSize); WCHAR*stringBuffer=newWCHAR[bufferSize]; //将设备名读入缓冲区stringBuffer GetRawInputDeviceInfo(raw-header.hDevice,RIDI_DEVICENAME,stringBuffer, bufferSize); //判断读入的值是否符合下述规则 if(virtualKeyCode==0x67wcscmp(stringBuffer,LA)==0) blockNextHook=TRUE; else blockNextHook=FALSE;
键盘钩子APIs实现消息拦截
借助SetWindowsHookEx函数实现消息的拦截,SetWindowsHookEx函数带4个参数,第一个参数指定钩子的类型,本文设置为WH_KEYBOARD;第二参数为钩子函数的入口地址;第三个参数为函数所在模块的句柄;第四个参数指定hook目的线程,这里设置为0,即拦截整个系统消息。
通过钩子函数KeyboardProc(intcode,WPARAMwParam,LPARAMlParam)捕获键盘消息,再调用SendMessage(hwndServer,WM_HOOK,wParam,lParam)函数将键盘消息送到主窗口程序,其中返回的code值若是一个大于或等于零得值,则表示发生了正常的键盘事件,wParam和lParam分别存键盘虚拟码及键被按下还是松开,以及是否按下的键是一个系统键。
caseWM_HOOK: { if(blockNextHook){ swprintf_s(text,128,LKeyboardevent:%X(%d)isbeingblocked!\n, virtualKeyCode,keyPressed); OutputDebugString(text); return1;} }
RawInput及键盘过滤组合技术
由上述分析可知,在窗口类中注册原始输入设备并启动键盘钩子函数,使其分别向窗口类的过程处理函数WndProc()发送WM_INPUT、WM_HOOK消息,函数WndProc()根据消息类别分别进行各自处理,WM_IPUT决定是否将此按键消息写入记事本,而WM_HOOK则根据WM_INPUT的处理结果决定是否进行消息过滤。显然,上述代码是在比较理想化的情况下才能很好地完成多键盘选择性控制。因为当短时间内出现大量的击键消息时,WM_INPUT、WM_HOOK消息很可能不是成对出现的。假设出现如下场景:
RawIput:43(1)Hook:43(1) RawIput:4B(1)RawIput:55(1) Hook:4B(1)Hook:55(1)
很显然,若运行前面编写的代码则会出现“4B”消息未拦截的情况。这里我们可以采用FIFO思想,通过C++提供的容器方式解决上述问题,每次出现WM_INPUT消息时,系统暂不将结果赋予全局变量,而是将其push到队列中,具体代码如下所示:
caseWM_INPUT: { if (virtualKeyCode == 0x67 wcscmp (stringBuffer, numericKeyboardDeviceName)==0) decisionBuffer.push_back(TRUE); else decisionBuffer.push_back(FALSE); } 在WM_HOOK消息中,取出队列中的处理结果再进行后续处理。 caseWM_HOOK: { BOOLblockThisHook=FALSE; if(!decisionBuffer.empty()){ blockThisHook=decisionBuffer.front(); decisionBuffer.pop_front(); }if(blockThisHook){ //... return1; } }
至此,我们已经分析了两种情形,分别为WM_INPUT、WM_HOOK成对出现和WM_INPUT连续出现后再出现WM_HOOK消息,两种方式均为WM_INPUT早于WM_HOOK出现,虽然可能WM_INPUT晚于WM_HOOK出现的概率极低,但也可能会出现偶然现象,此时我们如果仍旧采用上述代码实现,就无法按照WM_INPUT消息给出的控制指令执行下步任务,导致出现异常。为了准确执行控制指令,我们可以使用PeekMessage函数等待WM_INPUT消息,然后再判断消息code是否一致,若一致再根据指令判断是否hook。这种方式看似能够实现上述问题,但若出现消息丢失情形的话,PeekMessage方法可能会出现死等待。如我们按下一个“AltGr”键时,hook消息认为其是一个Ctrl+Alt组合键,Ctrl+Alt组合键,hook消息会产生两个消息,分别为Ctrl(11)+Alt(12),然而RawIput消息却仅仅发出一个AltGr
(12),此时即出现消息不对称。为了解决此问题,我们需要设定一个超时时间的阀值,超出阀值时间即放弃等待。具体实现方式请参照源代码附件,本文不再给出。
小结
本文主要介绍了RawInput输入及键盘钩子技术的组合用法,实现多键盘输入信号的选择性控制,通过RawInput技术判断消息来源,匹配事先设定的规则后,将控制指令保存到一个全局迭代容器内,然后利用hook技术捕获WM_HOOK,并通过SendMessage方式将WM_HOOK消息发送给主窗口,再在主窗口过程处理函数解析WM_INPUT和WM_HOOK消息,后续的具体逻辑处理读者可以参考附件源代码,也可以从stevemesser-“UsingRawInputfromC#tohandlemultiplekeyboards”和AntoineAubry-“Usingmultiplekeyboardswithdifferentlayoutsonthesamemachine”文章中了解更多关于RawInput技术。