捕获窗口消息
关于窗口消息,可以参考下面的文章
https://www.cnblogs.com/zhaotianff/p/11285312.html
https://www.cnblogs.com/zhaotianff/p/11297319.html
在WPF中,对于操作系统层面的原始输入 / 窗口消息,如 WM_LBUTTONDOWN、WM_MOUSEMOVE,都定义了对应的事件。
例如:WM_LBUTTONDOWN对应WPFMouseLeftButtonDown事件、WM_MOUSEMOVE对应WPF的MouseMove事件。
我们只需要添加事件处理函数,就可以对这些Win32消息作出响应,如下所示:
MainWindow.xaml
1 <Window MouseMove="Window_MouseMove" MouseLeftButtonDown="Window_MouseLeftButtonDown" >
2
3 </Window>
MainWindow.xaml.cs
1 private void Window_MouseMove(object sender, MouseEventArgs e)
2 {
3
4 }
5
6 private void Window_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
7 {
8
9 }
它的底层逻辑是通过接管所有Win32消息的模式来实现,WPF会把这些Win32消息转换为对应的事件。通过添加事件处理函数就可以对这些操作系统层面的Win32消息进行处理。
捕获窗口外的消息
有时候我们想捕获窗口外的消息,应该如何去操作呢。
例如,鼠标已经移出窗口外了,但是我还是想知道鼠标何时按下。
我们可以借助Win32 API RegisterRawInputDevices和 GetRawInputData函数来实现。
RegisterRawInputDevices函数注册提供原始输入数据的设备。
函数声明如下:
1 BOOL RegisterRawInputDevices(
2 [in] PCRAWINPUTDEVICE pRawInputDevices,
3 [in] UINT uiNumDevices,
4 [in] UINT cbSize
5 );
参数
pRawInputDevices
指向一组RAWINPUTDEVICE结构,代表提供原始输入的设备。
uiNumDevices
pRawInputDevices指向的RAWINPUTDEVICE结构的数量。
cbSize
指向RAWINPUTDEVICE结构的大小(以字节为单位)
返回值
如果函数成功,则返回值为TRUE;否则返回值为FALSE。
GetRawInputData可以从指定的设备中获取原始输入
GetRawInputData定义如下:
1 UINT GetRawInputData(
2 [in] HRAWINPUT hRawInput,
3 [in] UINT uiCommand,
4 [out, optional] LPVOID pData,
5 [in, out] PUINT pcbSize,
6 [in] UINT cbSizeHeader
7 );
参数
hRawInput
指向RAWINPUT结构的句柄。它来自于WM_INPUT中的lParam。
uiCommand
它是命令标志。此参数可以是以下值之一。
| 值 | 含义 |
|---|---|
| RID_HEADER 0x10000005 | 从 RAWINPUT 结构获取标头信息。 |
| RID_INPUT 0x10000003 | 从 RAWINPUT 结构获取原始数据。 |
pData
指向来自RAWINPUT结构的数据指针,这取决于uiCommand的值。
如果pData为NULL,则在* pcbSize中返回所需的缓冲区大小。
cbSizeHeader
指定RAWINPUTHEADER结构的大小(以字节为单位)。
返回值
如果pData为NULL且函数成功,则返回值为零。如果pData不为空且函数成功,则返回值为复制到pData中的字节数。如果有错误,则返回值为(UINT)-1。
这里还涉及了一个结构体RAWINPUTDEVICE,这个结构体定义原始输入设备的信息
RAWINPUTDEVICE定义如下:
1 typedef struct tagRAWINPUTDEVICE {
2 USHORT usUsagePage; //指向原始输入设备的顶级集合使用的页面。
3 USHORT usUsage; //指向原始输入设备的顶级集合的用法。
4 DWORD dwFlags; //指定如何解释由usUsagePage和usUsage提供的信息。它默认值为零,默认情况下,只要具有窗口焦点,操作系统就会将具有顶级集合(TLC)设备的原始输入发送到已注册的应用程序中。
5 HWND hwndTarget; //指向目标窗口的句柄。如果是NULL,则它会遵循键盘焦点。
6 } RAWINPUTDEVICE, *PRAWINPUTDEVICE, *LPRAWINPUTDEVICE;
WPF中的实现步骤如下:
1、引用Win32 api函数及定义相应结构体
1 namespace WPFGetRawInputData.Winapi
2 {
3 /// <summary>
4 /// 原始输入设备类型枚举
5 /// </summary>
6 public enum RawInputType : uint
7 {
8 RIM_TYPEKEYBOARD = 1, // 键盘
9 RIM_TYPEMOUSE = 0 // 鼠标
10 }
11
12 /// <summary>
13 /// 原始输入设备结构体
14 /// </summary>
15 [StructLayout(LayoutKind.Sequential)]
16 public struct RAWINPUTDEVICE
17 {
18 public ushort UsagePage; // 设备使用页(键盘/鼠标固定值)
19 public ushort Usage; // 设备使用ID(键盘/鼠标固定值)
20 public uint Flags; // 注册标志
21 public IntPtr WindowHandle; // 接收输入的窗口句柄
22 }
23
24 /// <summary>
25 /// 原始输入数据头部
26 /// </summary>
27 [StructLayout(LayoutKind.Sequential)]
28 public struct RAWINPUTHEADER
29 {
30 public RawInputType Type;
31 public uint Size;
32 public IntPtr Device;
33 public IntPtr WParam;
34 }
35
36 /// <summary>
37 /// 原始键盘输入结构体
38 /// </summary>
39 [StructLayout(LayoutKind.Sequential)]
40 public struct RAWKEYBOARD
41 {
42 public ushort MakeCode;
43 public ushort Flags;
44 public ushort Reserved;
45 public ushort VKey;
46 public uint Message;
47 public uint ExtraInformation;
48 }
49
50 /// <summary>
51 /// 原始鼠标输入结构体
52 /// </summary>
53 [StructLayout(LayoutKind.Explicit,Size = 4)]
54 public struct RAWMOUSE
55 {
56 [FieldOffset(0)]
57 public ushort Flags;
58 [FieldOffset(4)]
59 public uint Buttons;
60 [FieldOffset(4)]
61 public DUMMYSTRUCTNAME dUMMYSTRUCTNAME;
62 [FieldOffset(8)]
63 public uint RawButtons;
64 [FieldOffset(12)]
65 public int LastX;
66 [FieldOffset(16)]
67 public int LastY;
68 [FieldOffset(20)]
69 public uint ExtraInformation;
70 }
71
72 public struct DUMMYSTRUCTNAME
73 {
74 public ushort ButtonFlags;
75 public ushort ButtonData;
76 }
77
78 /// <summary>
79 /// 原始输入数据联合体(键盘/鼠标二选一)
80 /// </summary>
81 [StructLayout(LayoutKind.Explicit)]
82 public struct RAWINPUTDATA
83 {
84 [FieldOffset(0)]
85 public RAWMOUSE Mouse;
86 [FieldOffset(0)]
87 public RAWKEYBOARD Keyboard;
88 }
89
90 /// <summary>
91 /// 原始输入结构体
92 /// </summary>
93 [StructLayout(LayoutKind.Sequential)]
94 public struct RAWINPUT
95 {
96 public RAWINPUTHEADER Header;
97 public RAWINPUTDATA Data;
98 }
99
100 public static class User32
101 {
102 // 注册原始输入设备
103 [DllImport("user32.dll", SetLastError = true)]
104 public static extern bool RegisterRawInputDevices(
105 [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 0)]
106 RAWINPUTDEVICE[] pRawInputDevices,
107 uint uiNumDevices,
108 uint cbSize);
109
110 // 获取原始输入数据
111 [DllImport("user32.dll", SetLastError = true)]
112 public static extern uint GetRawInputData(
113 IntPtr hRawInput,
114 uint uiCommand,
115 IntPtr pData,
116 ref uint pcbSize,
117 uint cbSizeHeader);
118
119 // 窗口消息常量
120 public const uint WM_INPUT = 0x00FF;
121
122 // 键盘使用页/ID
123 public const ushort HID_USAGE_PAGE_GENERIC = 0x01;
124 public const ushort HID_USAGE_GENERIC_KEYBOARD = 0x06;
125
126 // 鼠标使用页/ID
127 public const ushort HID_USAGE_GENERIC_MOUSE = 0x02;
128
129 // 注册标志:输入数据发送到窗口消息队列
130 public const uint RIDEV_INPUTSINK = 0x00000100;
131 }
132 }
注意:这里有个非常大的坑,在定义RAWMOUSE时要注意联合体以及4字节对齐问题。
关于联合体在P/Invoke时的封送,可以参考:https://www.cnblogs.com/zhaotianff/p/13949849.html
说明:推荐使用nuget包Cswin32,可以参考我前面的文章:https://www.cnblogs.com/zhaotianff/p/18657903
2、注册原始输入设备
1 //注册
2 RAWINPUTDEVICE[] devices = new RAWINPUTDEVICE[2];
3
4 // 注册键盘
5 devices[0] = new RAWINPUTDEVICE
6 {
7 UsagePage = User32.HID_USAGE_PAGE_GENERIC,
8 Usage = User32.HID_USAGE_GENERIC_KEYBOARD,
9 Flags = User32.RIDEV_INPUTSINK,
10 WindowHandle = mainWindowHandle
11 };
12
13 // 注册鼠标
14 devices[1] = new RAWINPUTDEVICE
15 {
16 UsagePage = User32.HID_USAGE_PAGE_GENERIC,
17 Usage = User32.HID_USAGE_GENERIC_MOUSE,
18 Flags = User32.RIDEV_INPUTSINK,
19 WindowHandle = mainWindowHandle
20 };
21
22 // 调用API注册设备
23 var result = User32.RegisterRawInputDevices(
24 devices,
25 (uint)devices.Length,
26 (uint)Marshal.SizeOf(typeof(RAWINPUTDEVICE)));
27
28 if (result == false)
29 {
30 System.Windows.MessageBox.Show("注册失败");
31
32 //调用GetLastError查看原因
33 }
34 else
35 {
36 DisplayMessage("注册成功");
37 }
3、添加Win32消息捕获
1 protected override void OnSourceInitialized(EventArgs e)
2 {
3 base.OnSourceInitialized(e);
4
5 mainWindowHandle = new WindowInteropHelper(this).Handle;
6 HwndSource.FromHwnd(mainWindowHandle).AddHook(HwndProc);
7 }
8
9 public IntPtr HwndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
10 {
11 // 处理WM_INPUT消息
12 if (msg == User32.WM_INPUT)
13 {
14 //处理原始输入数据
15 ProcessRawInput(lParam);
16 handled = true;
17 }
18 return IntPtr.Zero;
19 }
4、获取原始数据
1 /// <summary>
2 /// 解析原始输入数据
3 /// </summary>
4 /// <param name="lParam"></param>
5 private void ProcessRawInput(IntPtr lParam)
6 {
7 uint dataSize = 0;
8 // 第一步:获取数据大小
9 User32.GetRawInputData(
10 lParam,
11 0x10000003, // RID_INPUT:获取原始输入数据
12 IntPtr.Zero,
13 ref dataSize,
14 (uint)Marshal.SizeOf(typeof(RAWINPUTHEADER)));
15
16 if (dataSize == 0) return;
17
18 // 第二步:分配内存并获取数据
19 IntPtr dataPtr = Marshal.AllocHGlobal((int)dataSize);
20 try
21 {
22 uint result = User32.GetRawInputData(
23 lParam,
24 0x10000003,
25 dataPtr,
26 ref dataSize,
27 (uint)Marshal.SizeOf(typeof(RAWINPUTHEADER)));
28
29 if (result != dataSize) return;
30
31 // 第三步:解析数据
32 RAWINPUT rawInput = Marshal.PtrToStructure<RAWINPUT>(dataPtr);
33 switch (rawInput.Header.Type)
34 {
35 //键盘
36 case RawInputType.RIM_TYPEKEYBOARD:
37 ProcessKeyboardInput(rawInput.Data.Keyboard);
38 break;
39 //鼠标
40 case RawInputType.RIM_TYPEMOUSE:
41 ProcessMouseInput(rawInput.Data.Mouse);
42 break;
43 }
44 }
45 finally
46 {
47 Marshal.FreeHGlobal(dataPtr);
48 }
49 }
50
51 /// <summary>
52 /// 处理键盘输入
53 /// </summary>
54 /// <param name="keyboard"></param>
55 private void ProcessKeyboardInput(RAWKEYBOARD keyboard)
56 {
57 // 判断按键按下(Flags=0)或释放(Flags=1)
58 bool isKeyDown = (keyboard.Flags & 0x01) == 0;
59
60 // 转换为键盘按键
61 System.Windows.Forms.Keys key = (System.Windows.Forms.Keys)keyboard.VKey;
62
63 // 输出调试信息(可替换为自定义逻辑)
64 string action = isKeyDown ? "按下" : "释放";
65
66 DisplayMessage($"键盘:{key} {action} (扫描码:{keyboard.MakeCode})");
67 }
68
69 // 处理鼠标输入
70 private void ProcessMouseInput(RAWMOUSE mouse)
71 {
72 // 鼠标按键状态
73 bool leftButtonDown = (mouse.dUMMYSTRUCTNAME.ButtonFlags & 0x0001) != 0;
74 bool leftButtonUp = (mouse.dUMMYSTRUCTNAME.ButtonFlags & 0x0002) != 0;
75 bool rightButtonDown = (mouse.dUMMYSTRUCTNAME.ButtonFlags & 0x0004) != 0;
76 bool rightButtonUp = (mouse.dUMMYSTRUCTNAME.ButtonFlags & 0x0008) != 0;
77
78 // 输出调试信息(可替换为自定义逻辑)
79 if (leftButtonDown)
80 {
81 DisplayMessage("鼠标左键按下");
82 }
83
84 if (leftButtonUp)
85 {
86 DisplayMessage("鼠标左键释放");
87 }
88
89 if (rightButtonDown)
90 {
91 DisplayMessage("鼠标右键按下");
92 }
93
94 if (rightButtonUp)
95 {
96 DisplayMessage("鼠标右键释放");
97 }
98 }
运行效果:
