当系统中连接了多个键盘时,操作系统默认将所有键盘输入汇总到同一个输入流,无法区分按键来自哪个物理设备。本文介绍如何通过原生模块 + Electron + React 实现指定键盘的原始输入监听,精确区分不同键盘设备的按键事件。
背景
在某些工业或测试场景中,需要区分不同键盘的输入。例如:
- 多操作员共用一台电脑,各自使用不同键盘,需要识别操作员身份
- 测试键盘的按键响应和扫描码
- 工业设备中,特定键盘触发特定功能
Windows 提供了 Raw Input API,可以获取每个 HID 设备的原始输入数据,从而区分不同键盘。
技术架构
1 2 3 4 5
| 原生模块 (raw-keyboard / N-API) ↓ 通过 IPC 暴露 Electron Preload (window.api.rawKeyboard) ↓ React 组件调用 RawKeyboardPage.tsx
|
- 原生模块:通过 Windows Raw Input API 枚举键盘设备、注册原始输入、捕获 WM_INPUT 消息
- Electron Preload:将原生模块能力通过
contextBridge 暴露为 window.api.rawKeyboard
- React 前端:设备选择、监听控制、事件日志展示
原生模块接口
原生模块通过 Electron IPC 暴露以下方法:
| 方法 |
说明 |
check() |
检查模块是否可用 |
listDevices() |
枚举所有键盘设备 |
start() |
开始监听原始输入 |
stop() |
停止监听 |
onData(callback) |
注册按键事件回调 |
设备数据结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| interface KeyboardDevice { handle: number name: string vid: number pid: number }
interface KeyEvent { handle: number vKey: number scanCode: number keyDown: boolean keyName: string }
|
VID/PID 格式化显示:
1 2 3
| const formatVidPid = (vid: number, pid: number) => { return `VID_${vid.toString(16).toUpperCase().padStart(4, '0')} PID_${pid.toString(16).toUpperCase().padStart(4, '0')}` }
|
React 组件实现
模块可用性检查
组件挂载时首先检查原生模块是否可用(仅 Windows 支持):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| useEffect(() => { const check = async () => { try { const result = await window.api.rawKeyboard.check() setModuleAvailable(result.available) if (!result.available) { setModuleError(result.error || '模块不可用') } } catch { setModuleAvailable(false) setModuleError('检查模块失败') } } check() }, [])
|
设备列表加载
模块可用后,自动加载键盘设备列表并默认选中第一个:
1 2 3 4 5 6 7 8
| useEffect(() => { const load = async () => { const list = await window.api.rawKeyboard.listDevices() setDevices(list) if (list.length > 0) setSelectedHandle(list[0].handle) } if (moduleAvailable) load() }, [moduleAvailable])
|
开始/停止监听
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const handleStart = useCallback(async () => { await window.api.rawKeyboard.start() setListening(true)
const cleanup = window.api.rawKeyboard.onData((evt: KeyEvent) => { setEvents(prev => { const next = [...prev, evt] return next.length > MAX_EVENTS ? next.slice(-MAX_EVENTS) : next }) }) cleanupRef.current = cleanup }, [])
const handleStop = useCallback(async () => { cleanupRef.current?.() cleanupRef.current = null await window.api.rawKeyboard.stop() setListening(false) }, [])
|
组件卸载时自动停止监听,防止资源泄漏:
1 2 3 4 5 6 7 8
| useEffect(() => { return () => { if (listening) { window.api.rawKeyboard.stop().catch(() => {}) } cleanupRef.current?.() } }, [])
|
事件过滤
支持按选中设备过滤事件,只显示目标键盘的输入:
1 2 3 4
| const isMatchedEvent = (evt: KeyEvent) => { if (!filterSelected || selectedHandle === null) return true return evt.handle === selectedHandle }
|
界面设计
设备选择区
以卡片形式展示所有检测到的键盘设备,点击选中目标设备:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| {devices.map((device) => ( <div key={device.handle} onClick={() => !listening && setSelectedHandle(device.handle)} style={{ border: `2px solid ${device.handle === selectedHandle ? '#4fc3f7' : '#ddd'}`, background: device.handle === selectedHandle ? '#e3f2fd' : '#fff', }} > <div>{formatVidPid(device.vid, device.pid)}</div> <div>Handle: {device.handle}</div> <div>{device.name}</div> </div> ))}
|
事件日志区
深色终端风格,按键按下(DOWN)显示绿色,释放(UP)显示蓝色,选中设备事件左侧高亮标记:
1 2 3 4 5 6 7 8 9 10 11 12
| <div style={{ background: evt.keyDown ? '#1a3a1a' : '#1a1a2a', color: evt.keyDown ? '#81c784' : '#90caf9', borderLeft: `3px solid ${evt.handle === selectedHandle ? '#4fc3f7' : '#555'}` }}> <span>#{idx + 1}</span> <span>h:{evt.handle}</span> <span>{evt.keyName}</span> <span>0x{evt.vKey.toString(16).toUpperCase().padStart(2, '0')}</span> <span>sc:{evt.scanCode}</span> <span>{evt.keyDown ? 'DOWN' : 'UP'}</span> </div>
|
状态指示
实时显示监听状态、事件数量、设备数量,支持自动滚动和设备过滤开关。
注意事项
- 平台限制:Raw Input API 仅 Windows 可用,其他平台需使用不同方案
- 原生模块编译:
raw-keyboard 原生模块需要 VS Build Tools 编译
- 管理员权限:某些场景下可能需要管理员权限才能枚举 HID 设备
- 事件缓冲:组件保留最近 200 条事件,超出自动丢弃最早的记录
- 资源清理:组件卸载时必须调用
stop() 和回调 cleanup(),否则原生层会持续监听
总结
通过 Windows Raw Input API + Electron 原生模块,可以突破操作系统对键盘输入的统一抽象,实现精确到物理设备的按键监听。React 前端提供直观的设备选择和事件日志,使多键盘场景下的输入区分变得简单可行。