.NET 8 BackgroundService中HID读卡器ProcessRawInput未触发问题
解决HID读卡器RawInput未触发ProcessRawInput的问题
核心问题分析及修复步骤
1. 窗口过程委托被GC回收
CreateMessageOnlyWindow中创建的WndProcDelegate是局部变量,垃圾回收会自动回收该委托,导致窗口过程失效,无法接收WM_INPUT消息。
修复:
将WndProcDelegate改为类级成员变量,避免被GC回收:
// 原局部变量改为类成员 private WndProcDelegate _wndProcDelegate; private IntPtr CreateMessageOnlyWindow() { // 赋值给类成员 _wndProcDelegate = new WndProcDelegate(WindowProc); var wndClass = new WNDCLASS { lpfnWndProc = Marshal.GetFunctionPointerForDelegate(_wndProcDelegate), // ...其他代码不变 }; // ...其他代码不变 }
2. 重复注册RawInput设备
ExecuteAsync和CreateMessageOnlyWindow中都调用了RegisterHIDDevice,重复注册可能导致消息路由异常。
修复:
删除CreateMessageOnlyWindow末尾的RegisterHIDDevice(hWnd);调用,只保留ExecuteAsync中的注册逻辑。
3. 消息循环优化
GetMessage传入IntPtr.Zero会接收系统所有窗口消息,改为传入消息窗口句柄,确保只处理目标窗口的消息:
private void MessageLoop() { MSG msg; // 传入消息窗口句柄,只处理该窗口的消息 while (GetMessage(out msg, _messageOnlyWindowHandle, 0, 0)) { TranslateMessage(ref msg); DispatchMessage(ref msg); } }
4. 过滤目标设备的VID/PID
当前代码接收所有键盘设备的输入,需要在ProcessRawInput中验证设备的VID/PID,只处理目标读卡器:
private void ProcessRawInput(IntPtr lParam) { uint dwSize = 0; GetRawInputData(lParam, RID_INPUT, IntPtr.Zero, ref dwSize, (uint)Marshal.SizeOf(typeof(RAWINPUTHEADER))); IntPtr buffer = Marshal.AllocHGlobal((int)dwSize); try { if (GetRawInputData(lParam, RID_INPUT, buffer, ref dwSize, (uint)Marshal.SizeOf(typeof(RAWINPUTHEADER))) == dwSize) { var raw = (RAWINPUT)Marshal.PtrToStructure(buffer, typeof(RAWINPUT)); if (raw.header.dwType == RIM_TYPEKEYBOARD) { // 获取设备VID/PID GetDeviceInfo(raw.header.hDevice, out ushort vid, out ushort pid); if (vid == VENDOR_ID && pid == PRODUCT_ID) { _logger.LogInformation($"RawInput Keyboard Event from target device: VKey={raw.keyboard.VKey}, Message={raw.keyboard.Message}"); // 处理刷卡逻辑 if (raw.keyboard.Message == WM_KEYDOWN) { // 收集按键字符(需实现字符转换逻辑) } } } } } finally { Marshal.FreeHGlobal(buffer); } }
5. 修正RAWINPUT结构体布局
RAWINPUT使用FieldOffset(16)可能因系统结构体对齐差异导致错误,改为LayoutKind.Sequential更可靠:
[StructLayout(LayoutKind.Sequential)] private struct RAWINPUT { public RAWINPUTHEADER header; public RAWKEYBOARD keyboard; }
修改后的完整代码
namespace HIDReaderService { public class Worker : BackgroundService { private readonly ILogger<Worker> _logger; private IntPtr _messageOnlyWindowHandle = IntPtr.Zero; private const ushort VENDOR_ID = 0xFFFF; // 替换为实际VID private const ushort PRODUCT_ID = 0xFF; // 替换为实际PID private delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam); private WndProcDelegate _wndProcDelegate; // 类成员委托,避免GC回收 public Worker(ILogger<Worker> logger) { _logger = logger; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("Starting HID reader service..."); _messageOnlyWindowHandle = CreateMessageOnlyWindow(); if (_messageOnlyWindowHandle == IntPtr.Zero) { _logger.LogError("Failed to create message-only window."); return; } // 仅注册一次设备 if (!RegisterHIDDevice(_messageOnlyWindowHandle)) { _logger.LogError("Failed to register HID device."); return; } _logger.LogInformation("Listening for HID device input..."); var messageLoopThread = new Thread(MessageLoop); messageLoopThread.Start(); while (!stoppingToken.IsCancellationRequested) { await Task.Delay(100); } } private IntPtr CreateMessageOnlyWindow() { _wndProcDelegate = new WndProcDelegate(WindowProc); var wndClass = new WNDCLASS { lpfnWndProc = Marshal.GetFunctionPointerForDelegate(_wndProcDelegate), hInstance = Marshal.GetHINSTANCE(typeof(Worker).Module), lpszClassName = "MessageOnlyWindow" }; ushort classAtom = RegisterClass(ref wndClass); if (classAtom == 0) { int error = Marshal.GetLastWin32Error(); _logger.LogError($"Failed to register window class. Error: {error}"); return IntPtr.Zero; } IntPtr hWnd = CreateWindowEx(0, "MessageOnlyWindow", "Message Only Window", 0, 0, 0, 0, 0, HWND_MESSAGE, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero); if (hWnd == IntPtr.Zero) { int error = Marshal.GetLastWin32Error(); _logger.LogError($"Failed to create message-only window. Error: {error}"); return IntPtr.Zero; } _logger.LogInformation("Message-only window created successfully."); return hWnd; } private IntPtr WindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { if (msg == WM_INPUT) { _logger.LogInformation("WM_INPUT message received."); ProcessRawInput(lParam); } return DefWindowProc(hWnd, msg, wParam, lParam); } private bool RegisterHIDDevice(IntPtr hWnd) { var rid = new RAWINPUTDEVICE[1]; rid[0].UsagePage = 0x01; // Generic Desktop Controls rid[0].Usage = 0x06; // Keyboard rid[0].Flags = RIDEV_INPUTSINK; rid[0].Target = hWnd; bool result = RegisterRawInputDevices(rid, (uint)rid.Length, (uint)Marshal.SizeOf(typeof(RAWINPUTDEVICE))); if (!result) { int errorCode = Marshal.GetLastWin32Error(); _logger.LogError($"RegisterRawInputDevices failed. Error Code: {errorCode}"); } else { _logger.LogInformation("HID device registered successfully."); } return result; } private void MessageLoop() { MSG msg; // 仅处理消息窗口的消息 while (GetMessage(out msg, _messageOnlyWindowHandle, 0, 0)) { TranslateMessage(ref msg); DispatchMessage(ref msg); } } private void ProcessRawInput(IntPtr lParam) { uint dwSize = 0; GetRawInputData(lParam, RID_INPUT, IntPtr.Zero, ref dwSize, (uint)Marshal.SizeOf(typeof(RAWINPUTHEADER))); IntPtr buffer = Marshal.AllocHGlobal((int)dwSize); try { if (GetRawInputData(lParam, RID_INPUT, buffer, ref dwSize, (uint)Marshal.SizeOf(typeof(RAWINPUTHEADER))) == dwSize) { var raw = (RAWINPUT)Marshal.PtrToStructure(buffer, typeof(RAWINPUT)); if (raw.header.dwType == RIM_TYPEKEYBOARD) { GetDeviceInfo(raw.header.hDevice, out ushort vid, out ushort pid); if (vid == VENDOR_ID && pid == PRODUCT_ID) { _logger.LogInformation($"Target Device Input: VKey={raw.keyboard.VKey}, Message={raw.keyboard.Message}"); // 这里添加刷卡数据收集逻辑 } } } } finally { Marshal.FreeHGlobal(buffer); } } private void GetDeviceInfo(IntPtr hDevice, out ushort vendorId, out ushort productId) { uint size = 0; GetRawInputDeviceInfo(hDevice, RIDI_DEVICENAME, IntPtr.Zero, ref size); if (size == 0) { vendorId = productId = 0; return; } var nameBuffer = Marshal.AllocHGlobal((int)size); try { if (GetRawInputDeviceInfo(hDevice, RIDI_DEVICENAME, nameBuffer, ref size) > 0) { var deviceName = Marshal.PtrToStringAnsi(nameBuffer); if (deviceName != null) { var vidIndex = deviceName.IndexOf("VID_") + 4; var pidIndex = deviceName.IndexOf("PID_") + 4; if (vidIndex >=4 && pidIndex >=4 && vidIndex +4 <= deviceName.Length && pidIndex +4 <= deviceName.Length) { vendorId = Convert.ToUInt16(deviceName.Substring(vidIndex, 4), 16); productId = Convert.ToUInt16(deviceName.Substring(pidIndex, 4), 16); return; } } } } finally { Marshal.FreeHGlobal(nameBuffer); } vendorId = productId = 0; } private const int WM_KEYDOWN = 0x100; private const int WM_INPUT = 0x00FF; private const uint RID_INPUT = 0x10000003; private const uint RIM_TYPEKEYBOARD = 1; private const uint VK_RETURN = 0x0D; private const int RIDEV_INPUTSINK = 0x00000100; private const uint RIDI_DEVICENAME = 0x20000007; private static readonly IntPtr HWND_MESSAGE = new IntPtr(-3); [DllImport("user32.dll")] private static extern bool RegisterRawInputDevices(RAWINPUTDEVICE[] pRawInputDevices, uint uiNumDevices, uint cbSize); [DllImport("user32.dll")] private static extern uint GetRawInputData(IntPtr hRawInput, uint uiCommand, IntPtr pData, ref uint pcbSize, uint cbSizeHeader); [DllImport("user32.dll")] private static extern uint GetRawInputDeviceInfo(IntPtr hDevice, uint uiCommand, IntPtr pData, ref uint pcbSize); [DllImport("user32.dll")] private static extern IntPtr CreateWindowEx(int exStyle, string lpClassName, string lpWindowName, int dwStyle, int x, int y, int nWidth, int nHeight, IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam); [DllImport("user32.dll", SetLastError = true)] private static extern ushort RegisterClass(ref WNDCLASS lpWndClass); [DllImport("user32.dll")] private static extern bool GetMessage(out MSG lpMsg, IntPtr hWnd, uint wMsgFilterMin, uint wMsgFilterMax); [DllImport("user32.dll")] private static extern bool TranslateMessage(ref MSG lpMsg); [DllImport("user32.dll")] private static extern IntPtr DispatchMessage(ref MSG lpMsg); [DllImport("user32.dll", SetLastError = true)] private static extern IntPtr DefWindowProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam); [StructLayout(LayoutKind.Sequential)] private struct WNDCLASS { public int style; public IntPtr lpfnWndProc; public int cbClsExtra; public int cbWndExtra; public IntPtr hInstance; public IntPtr hIcon; public IntPtr hCursor; public IntPtr hbrBackground; public string lpszMenuName; public string lpszClassName; } [StructLayout(LayoutKind.Sequential)] private struct MSG { public IntPtr hWnd; public uint message; public IntPtr wParam; public IntPtr lParam; public uint time; public POINT pt; } [StructLayout(LayoutKind.Sequential)] private struct POINT { public int x; public int y; } [StructLayout(LayoutKind.Sequential)] private struct RAWINPUTHEADER { public uint dwType; public uint dwSize; public IntPtr hDevice; public IntPtr wParam; } [StructLayout(LayoutKind.Sequential)] private struct RAWINPUT { public RAWINPUTHEADER header; public RAWKEYBOARD keyboard; } [StructLayout(LayoutKind.Sequential)] private struct RAWKEYBOARD { public ushort MakeCode; public ushort Flags; public ushort Reserved; public ushort VKey; public uint Message; public uint ExtraInformation; } [StructLayout(LayoutKind.Sequential)] public struct RAWINPUTDEVICE { public ushort UsagePage; public ushort Usage; public int Flags; public IntPtr Target; } } }
额外注意事项
- 确保替换
VENDOR_ID和PRODUCT_ID为实际设备的ID。 - 刷卡数据收集需要将虚拟键码转换为对应字符,可通过
ToUnicodeAPI实现。 - 服务运行时需确保有足够权限访问输入设备。
内容的提问来源于stack exchange,提问作者Srikanth S
相关产品推荐
相关产品推荐

