抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

在 Windows 提供很底层的方法接收硬件设备的裸数据,通过接收裸数据可以做到性能更高的全局键盘,还能支持多个鼠标。但是用这个方法需要自己解析裸数据,同时会因为接受到很多消息降低性能。

1、设计需求

由于鼠标键盘设计时候需要进行多次测试才能知道设备功能。而针对于这类高频率使用的设备,都需要进行成千上万的测试,所以就设计一款能够解决自动化测试鼠标键盘使用功能,并生成log文件记录自动化测试的结果,如下是软件界面图。

百度网盘分享的文件:1.rawinput_自动测试工具
链接:https://pan.baidu.com/s/1EIlPrt5c6l-ivQGavsl0dg
提取码:cu73

2、rawinput相关API

除了键盘和鼠标以外,还有很多其他的用户输入设备,比如手柄、触摸屏、麦克风等等,这些设备统称为人机接口设备HID(Human Interface Device),RawInput 提供API用于接受任何HID设备的原生输入,包括键盘和鼠标。

2.1、RawInput输入模型有以下几个优点

  • 应用程序不必检测或打开输入设备。
  • 应用程序直接从设备获取数据,并根据其需求处理数据。
  • 应用程序可以区分输入源,即使是同一类型的设备。例如,两个鼠标设备。
  • 应用程序可以通过从设备集合里的指定数据或者指定的设别类型来管理数据流
  • 向前兼容,HID设备可以保持可用,无需更新新的消息类型或者更新系统后在 WM_APPCOMMAND 里更新操作指令

2.2、注册Raw Input设备

一般情况下没有程序能够直接获取rawinput数据,在获取数据之前必须注册设备。

注册设备之前,应用程序首先要创建一个RAWINPUTDEVICE的结构体数组来指定顶层集合(TCL)里需要的设备。顶层集合(TCL)包括一个Usage Page(设备类别)和一个 Usage ID(设备类别中对应的设备类型)。例如,获取键盘的顶层集合,需要设置设备类别UsagePage = 0x01 即通用桌面控制器,设置设备类型为UsageID = 0x06即键盘类型。然后应用调用RegisterRawInputDevices去注册设备。

想要获取一个设备的注册状态,调用 GetRegisteredRawInputDevices

在 MainWindows 注册事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public MainWindow()
{
InitializeComponent();

SourceInitialized += MainWindow_SourceInitialized;
}

private void MainWindow_SourceInitialized(object sender, EventArgs e)
{
var windowInteropHelper = new WindowInteropHelper(this);
var hwnd = windowInteropHelper.Handle;

// Get the devices that can be handled with Raw Input.
var devices = RawInputDevice.GetDevices();

// register the keyboard device and you can register device which you need like mouse
RawInputDevice.RegisterDevice(HidUsageAndPage.Keyboard,
RawInputDeviceFlags.ExInputSink | RawInputDeviceFlags.NoLegacy, hwnd);

HwndSource source = HwndSource.FromHwnd(hwnd);
source.AddHook(Hook);
}

通过RawInputDevice.GetDevices 可以知道当前可以注册的设备有哪些,使用 RawInputDevice.RegisterDevice 可以注册事件,这里注册的是键盘事件,自己修改 HidUsageAndPage 的值可以注册不同的事件。

注册事件就可以在 Hook 函数接收到 WM_INPUT 消息,通过这个消息解析就可以拿到裸数据,对裸数据处理就可以收到输入,如果需要接入 WPF 可以使用WPF 模拟触摸设备将收到的消息模拟触摸。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
private IntPtr Hook(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam, ref bool handled)
{
const int WM_INPUT = 0x00FF;

// You can read inputs by processing the WM_INPUT message.
if (msg == WM_INPUT)
{
// Create an RawInputData from the handle stored in lParam.
var data = RawInputData.FromHandle(lparam);

// You can identify the source device using Header.DeviceHandle or just Device.
var sourceDeviceHandle = data.Header.DeviceHandle;
var sourceDevice = data.Device;

// The data will be an instance of either RawInputMouseData, RawInputKeyboardData, or RawInputHidData.
// They contain the raw input data in their properties.
switch (data)
{
case RawInputMouseData mouse:
Debug.WriteLine(mouse.Mouse);
break;
case RawInputKeyboardData keyboard:
Debug.WriteLine(keyboard.Keyboard);
break;
case RawInputHidData hid:
Debug.WriteLine(hid.Hid);
break;
}
}

return IntPtr.Zero;
}

RawInput 就是通过 RegisterRawInputDevices告诉系统当前进程需要支持裸数据,系统将会根据传入的参数将裸数据转发给应用。应用在消息解析数据拿到裸数据,然后按照业务解析裸数据。这个方法可以解决一些特殊设备支持,因为 HID 设备是独占设备,只能让系统独占,如果想要应用也接收硬件发过来的消息,就需要额外通道给应用。另外应用如果需要解决其他应用钩了消息,可以注册裸数据解决其他应用勾了键盘消息。

2.3、读取Raw Input输入数据

应用程序可以从任何顶层集合内匹配的已注册设备获取输入数据。当应用程序收到raw input数据的时候,他的消息队列获取到一个WM_INPUT消息并且队列状态标志设为QS_RAWINPUTQS_INPUT包含在这个标志里)应用程序在前台和后台的时候都能接受到输入数据。

3、程序示例

使用RAWINPUT,获取键盘原始输入,拦截键盘消息,键盘钩子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
#include "stdafx.h"
#include <windows.h>

//声明自定义消息处理函数
LRESULT CALLBACK WindowProc(HWND hwnd,UINT uMsg,WPARAM wParam,LPARAM lParam);
HWND m_hWnd;

//注册原始输入设备
void RegKeyboardRawInput(HWND hWnd)
{
RAWINPUTDEVICE rawInputDevice[1];
rawInputDevice[0].usUsagePage = 0x01;//设备类
rawInputDevice[0].usUsage = 0x06;//设备类内的具体设备
rawInputDevice[0].dwFlags = RIDEV_INPUTSINK;//意味着即使窗口失去焦点位置,仍然会一直接收输入消息
rawInputDevice[0].hwndTarget = hWnd;
if (RegisterRawInputDevices(rawInputDevice, 1, sizeof(rawInputDevice[0])) == FALSE)
{
printf("RegisterRawInputDevices failed");
}
}

int _tmain(int argc, _TCHAR* argv[])
{
//创建一个隐藏窗口
HINSTANCE hInst;
hInst = GetModuleHandle(NULL); //获取一个应用程序或动态链接库的模块句柄
WNDCLASSEX wcx;
wcx.cbSize = sizeof(wcx);
wcx.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC | CS_NOCLOSE;//窗口的风格
wcx.lpfnWndProc = WindowProc; //采用自定义消息处理函数
wcx.cbClsExtra = 0; // no extra class memory
wcx.cbWndExtra = 0; // no extra window memory
wcx.hInstance = hInst; //当前应用程序的实例句柄
wcx.hIcon = LoadIcon(NULL,IDI_APPLICATION); //图标风格
wcx.hCursor = LoadCursor(NULL,IDC_ARROW); //鼠标风格
wcx.hbrBackground = (HBRUSH)WHITE_BRUSH; //背景色
wcx.lpszMenuName =NULL; //菜单名
wcx.lpszClassName = _T("ITSMYOSDCLASS"); //窗口类的名称
wcx.hIconSm = NULL;

if (!RegisterClassEx(&wcx))
{
printf("RegisterClassEx failed");
return 1;
}

//窗口在屏幕上的显示位置
int OSDleft = GetSystemMetrics(SM_CXSCREEN) / 2 - 300;
int OSDTop = GetSystemMetrics(SM_CYSCREEN) / 2;

m_hWnd = CreateWindowEx(
WS_EX_LAYERED | WS_EX_TOOLWINDOW | WS_EX_TOPMOST | WS_EX_TRANSPARENT | WS_EX_NOACTIVATE,//窗口的扩展风格
wcx.lpszClassName,//上面注册的类名lpszClassName,要完全一致
NULL,
WS_VISIBLE | WS_POPUP,//窗口的风格
OSDleft,//窗口相对于父级的X坐标
OSDTop,//窗口相对于父级的Y坐标
300,//窗口的宽度
300,//窗口的高度
(HWND)NULL,//没有父窗口,为(HWND)NULL//GetDesktopWindow(),
(HMENU)NULL,//没有菜单,为NULL
hInst,//当前应用程序的实例句柄
(LPVOID)NULL); //没有附加数据,为NULL

if (!m_hWnd)
{
printf("CreateWindowEx failed");
return 1;
}
RegKeyboardRawInput(m_hWnd);
//消息循环
MSG msg;
while (GetMessage(&msg, (HWND)NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

return 0;
}
//LRESULT CALLBACK MainWndProc(HWND, UINT, WPARAM, LPARAM);
// 自定义消息处理函数的实现
LRESULT CALLBACK WindowProc(_In_ HWND hwnd,_In_ UINT uMsg,_In_ WPARAM wParam,_In_ LPARAM lParam)
{
switch (uMsg)
{
case WM_COMMAND:
break;
case WM_PAINT:
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
case WM_INPUT:
{
UINT dwSize = 0;
GetRawInputData((HRAWINPUT)lParam, (UINT)RID_INPUT, NULL, &dwSize, sizeof(RAWINPUTHEADER));//取数据,第一次调用函数将获取需要的字节大小
LPBYTE lpbBuffer = new BYTE[dwSize];//分配指定的缓冲区大小
GetRawInputData((HRAWINPUT)lParam, (UINT)RID_INPUT, (LPVOID)lpbBuffer, (PUINT)&dwSize, (UINT)sizeof(RAWINPUTHEADER));//第二次调用获取原始输入数据,读入lpbBuffer

RAWINPUT * raw = (RAWINPUT *)lpbBuffer;
if (raw->header.dwType == RIM_TYPEKEYBOARD)//这里可以截获所有键盘信息,如需区分不同的键盘输入信息,可以通过设备句柄判断。
{
if (raw->data.keyboard.Message == WM_KEYUP)
{
//读取键盘名称
UINT bufferSize;//为键盘设备名准备缓冲区大小
GetRawInputDeviceInfo(raw->header.hDevice, RIDI_DEVICENAME, NULL, &bufferSize);
WCHAR* RawDevName = new WCHAR[bufferSize];
GetRawInputDeviceInfo(raw->header.hDevice, RIDI_DEVICENAME, RawDevName, &bufferSize);//将设备名读入缓冲区RawDevName
printf("dev=%ls\n", RawDevName);
delete[] RawDevName;

//获取键盘输入
char keytext[10] = { 0 };
BYTE state[256] = { 0 };
ToAscii(raw->data.keyboard.VKey, raw->data.keyboard.MakeCode, state, (LPWORD)keytext, 0);//通过虚拟键盘码得到名字
printf("key=%s\n", keytext);

printf("Kbd: make = % 04x Flags : % 04x Reserved : % 04x ExtraInformation : % 08x, msg = % 04x VK = % 04x \n",
raw->data.keyboard.MakeCode,
raw->data.keyboard.Flags,
raw->data.keyboard.Reserved,
raw->data.keyboard.ExtraInformation,
raw->data.keyboard.Message,
raw->data.keyboard.VKey);
}
}
delete[] lpbBuffer;
return 0;
}
default:
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
return 0;
}

运行效果图:

评论