如何在可视化应用中正确调用GetRawInputBuffer()获取稳定输入?
嘿,我来帮你搞定这个RawInput的坑!你遇到的输入丢失和线程满载的问题其实很典型,咱们一步步拆解解决:
先搞清楚为什么会丢数据
你用GetRawInputBuffer()的时候,系统会把设备输入暂存在一个缓冲区里。如果你的调用间隔太长,或者缓冲区分配太小,新的输入就会覆盖旧的——条码扫描器每秒2次扫描、每次700个按键,这个速度其实不算快,但如果读取不及时,缓冲区很容易被填满溢出,自然就丢数据了。
之前的三个方案缺陷都很明确:
- Sleep()后读取:线程休眠期间完全不处理输入,缓冲区溢出是必然的,还会卡死UI,绝对不可取。
- GetMessage():它只监听窗口消息队列,而
GetRawInputBuffer()是直接读系统的RawInput缓冲区,两者不是一个通道。当扫描器输入过来时,GetMessage()可能还在傻等窗口消息,导致输入堆积丢失。 - PeekMessage()+PM_NOREMOVE:无限轮询直接把CPU拉满,完全是浪费资源,也不是正确的姿势。
正确的调用方式和时机
核心思路是:让线程在没有输入时休眠,有输入时立刻被唤醒读取,并且一次性读完所有pending的输入,既不浪费CPU,也不会丢数据。
1. 配置足够的RawInput缓冲区
首先注册RawInput时,要确保开启RIDEV_INPUTSINK(保证应用失焦也能接收输入),同时过滤只接收条码扫描器的设备(比如指定键盘类的Usage Page 0x01、Usage 0x06,读取时再通过hDevice验证是不是你的扫描器)。
另外,每次调用GetRawInputBuffer()前,一定要先获取当前需要的缓冲区大小:
var dwBufferSize: DWORD; begin // 先获取当前pending输入所需的字节数 GetRawInputBuffer(nil, dwBufferSize, SizeOf(RAWINPUTHEADER)); if dwBufferSize = 0 then Exit; // 无输入可读
2. 用MsgWaitForMultipleObjectsEx实现高效等待
不要用轮询或Sleep,改用MsgWaitForMultipleObjectsEx()让线程休眠,直到有RawInput可用才唤醒。这样线程在空闲时完全不占用CPU,有输入时立刻响应。
推荐开一个单独的后台线程处理RawInput(别阻塞UI线程),线程逻辑大概是这样:
procedure RawInputProcessingThread; var WaitResult: DWORD; Msg: TMsg; begin while not Terminated do begin // 等待RawInput或窗口消息,线程休眠直到有事件触发 WaitResult := MsgWaitForMultipleObjectsEx(0, nil, INFINITE, QS_RAWINPUT, MWMO_INPUTAVAILABLE); case WaitResult of WAIT_OBJECT_0: begin // 有RawInput可用,一次性读完所有数据 ProcessPendingRawInput; end; WAIT_OBJECT_0 + 1: begin // 处理线程消息队列的消息,避免堆积 while PeekMessage(Msg, 0, 0, 0, PM_REMOVE) do begin TranslateMessage(Msg); DispatchMessage(Msg); end; end; end; end; end;
3. 一次性读取所有pending输入
在ProcessPendingRawInput()里,要循环读取直到缓冲区为空,确保不会漏掉任何按键事件:
procedure ProcessPendingRawInput; var dwBufferSize: DWORD; pRawInput: PRAWINPUT; ReadCount: UINT; begin repeat // 获取当前所需缓冲区大小 GetRawInputBuffer(nil, dwBufferSize, SizeOf(RAWINPUTHEADER)); if dwBufferSize = 0 then Break; // 分配足够的内存 GetMem(pRawInput, dwBufferSize); try // 读取所有可用的RawInput数据 ReadCount := GetRawInputBuffer(pRawInput, dwBufferSize, SizeOf(RAWINPUTHEADER)); if ReadCount = 0 then Break; // 处理读取到的输入:过滤扫描器设备、拼接按键为条码 if IsBarcodeScannerDevice(pRawInput^.header.hDevice) then begin ParseRawInputKeys(pRawInput); // 这里实现按键拼接逻辑 end; finally FreeMem(pRawInput); end; until False; end;
4. 额外优化:区分扫描器和手动输入
条码扫描器的按键间隔通常远小于手动输入(比如<10ms),你可以在拼接按键时记录时间戳,间隔超过阈值就重置拼接内容,这样能彻底忽略其他设备的手动输入。
最后再强调几个关键点
- 绝对不要在UI线程里处理RawInput读取,一定要开后台线程,避免UI卡顿。
- 每次读取前必须获取缓冲区大小,不要固定分配内存,否则可能读不全。
- 注册RawInput时一定要过滤设备,避免接收其他键盘的输入,减少不必要的处理。
内容的提问来源于stack exchange,提问作者emno

