如何在WPF中捕获窗口外的事件

捕获窗口消息

关于窗口消息,可以参考下面的文章

https://www.cnblogs.com/zhaotianff/p/11285312.html

https://www.cnblogs.com/zhaotianff/p/11297319.html

在WPF中,对于操作系统层面的原始输入 / 窗口消息,如 WM_LBUTTONDOWNWM_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 RegisterRawInputDevicesGetRawInputData函数来实现。

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  }

运行效果:

相关推荐
爱吃烤鸡翅的酸菜鱼1 天前
Java 事件发布-订阅机制全解析:从原生实现到主流中间件
java·中间件·wpf·事件·发布订阅
武藤一雄2 天前
WPF中ViewModel之间的5种通讯方式
开发语言·前端·microsoft·c#·wpf
CSharp精选营2 天前
都是微软亲儿子,WPF凭啥干不掉WinForm?这3个场景说明白了
c#·wpf·跨平台·winform
baivfhpwxf20232 天前
wpf TextBlock 控件如何根据内容换行?
wpf
亘元有量-流量变现2 天前
鸿蒙、安卓、苹果音频设备技术深度解析与开发实践
android·wpf·harmonyos·亘元有量·积分墙
软泡芙2 天前
【Bug】ReactiveUI WPF绑定中依赖属性不更新的问题分析与解决方案
java·bug·wpf
浪扼飞舟2 天前
WPF输入验证(ValidationRule)
java·javascript·wpf
IOFsmLtzR4 天前
Flink Agents 源码解读 --- (5) --- ActionExecutionOperator
microsoft·flink·wpf
廋到被风吹走5 天前
【AI】Codex 复杂任务拆解:从“一气呵成“到“步步为营“
人工智能·wpf