You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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。
  • 刷卡数据收集需要将虚拟键码转换为对应字符,可通过ToUnicode API实现。
  • 服务运行时需确保有足够权限访问输入设备。

内容的提问来源于stack exchange,提问作者Srikanth S

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 17:55:55