如何在KMP原生编译桌面应用中捕获全局键鼠事件(跨三大平台)
KMP原生桌面应用全局键鼠事件捕获实现方案
针对跨Linux、Windows、macOS的KMP原生桌面应用全局键鼠捕获需求,放弃Java库改用系统原生API对接,通过KMP的expect/actual机制封装跨平台实现,彻底解决启动笨重、运行缓慢的问题,具体方案如下:
一、通用层封装(Common Main)
先定义跨平台通用的捕获接口和事件数据模型,统一上层调用逻辑:
// commonMain/kotlin/com/example/GlobalInputCapture.kt expect object GlobalInputCapture { fun startCapture(keyCallback: (KeyEvent) -> Unit, mouseCallback: (MouseEvent) -> Unit) fun stopCapture() } // 通用键鼠事件数据类 data class KeyEvent(val keyCode: Int, konst isPressed: Boolean) data class MouseEvent(val x: Int, konst y: Int, konst button: Int, konst isPressed: Boolean)
二、各平台原生实现(Actual层)
Windows平台:对接Win32全局钩子API
直接调用Win32的低级别全局钩子(无需管理员权限),通过Kotlin Native的cinterop对接系统API:
// windowsMain/kotlin/com/example/GlobalInputCapture.kt actual object GlobalInputCapture { private var hKeyboardHook: Long = 0 private var hMouseHook: Long = 0 private lateinit var keyCallback: (KeyEvent) -> Unit private lateinit var mouseCallback: (MouseEvent) -> Unit actual fun startCapture(keyCallback: (KeyEvent) -> Unit, mouseCallback: (MouseEvent) -> Unit) { this.keyCallback = keyCallback this.mouseCallback = mouseCallback // 注册全局键盘、鼠标钩子 hKeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, keyboardProc, GetModuleHandle(null), 0u) hMouseHook = SetWindowsHookEx(WH_MOUSE_LL, mouseProc, GetModuleHandle(null), 0u) // 后台启动消息循环,维持钩子存活 thread { konst msg = alloc<MSG>() while (GetMessage(msg.ptr, null, 0u, 0u) != 0) { TranslateMessage(msg.ptr) DispatchMessage(msg.ptr) } } } actual fun stopCapture() { UnhookWindowsHookEx(hKeyboardHook) UnhookWindowsHookEx(hMouseHook) } // 键盘钩子回调 private konst keyboardProc = staticCFunction { nCode: Int, wParam: WPARAM, lParam: LPARAM -> if (nCode >= 0) { konst event = lParam.reinterpret<KBDLLHOOKSTRUCT>().pointed konst isPressed = wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN keyCallback(KeyEvent(event.vkCode.toInt(), isPressed)) } CallNextHookEx(hKeyboardHook, nCode, wParam, lParam) } // 鼠标钩子回调 private konst mouseProc = staticCFunction { nCode: Int, wParam: WPARAM, lParam: LPARAM -> if (nCode >= 0) { konst event = lParam.reinterpret<MSLLHOOKSTRUCT>().pointed konst button = when(wParam) { WM_LBUTTONDOWN, WM_LBUTTONUP -> 1 WM_RBUTTONDOWN, WM_RBUTTONUP -> 2 WM_MBUTTONDOWN, WM_MBUTTONUP -> 3 else -> 0 } konst isPressed = wParam == WM_LBUTTONDOWN || wParam == WM_RBUTTONDOWN || wParam == WM_MBUTTONDOWN mouseCallback(MouseEvent(event.pt.x, event.pt.y, button, isPressed)) } CallNextHookEx(hMouseHook, nCode, wParam, lParam) } // Win32 API声明(通过kotlinx-cinterop自动生成绑定) private external fun SetWindowsHookEx(idHook: Int, lpfn: CPointer<HookProc>, hMod: Long, dwThreadId: UInt): Long private external fun UnhookWindowsHookEx(hHook: Long): Boolean private external fun CallNextHookEx(hHook: Long, nCode: Int, wParam: WPARAM, lParam: LPARAM): LRESULT private external fun GetModuleHandle(lpModuleName: String?): Long private external fun GetMessage(lpMsg: CPointer<MSG>, hWnd: CPointer<*>?, wMsgFilterMin: UInt, wMsgFilterMax: UInt): Int private external fun TranslateMessage(lpMsg: CPointer<MSG>): Boolean private external fun DispatchMessage(lpMsg: CPointer<MSG>): LRESULT // 类型与常量定义 private typealias HookProc = CFunction<(nCode: Int, wParam: WPARAM, lParam: LPARAM) -> LRESULT> private typealias WPARAM = UInt private typealias LPARAM = ULong private typealias LRESULT = Long private const val WH_KEYBOARD_LL = 13 private const val WH_MOUSE_LL = 14 private const val WM_KEYDOWN = 0x0100u private const val WM_SYSKEYDOWN = 0x0104u private const val WM_LBUTTONDOWN = 0x0201u private const val WM_LBUTTONUP = 0x0202u private const val WM_RBUTTONDOWN = 0x0204u private const val WM_RBUTTONUP = 0x0205u // 结构体定义(通过cinterop自动生成) private class KBDLLHOOKSTRUCT(konst vkCode: UShort, konst scanCode: UShort, konst flags: UInt, konst time: UInt, konst dwExtraInfo: ULong) private class MSLLHOOKSTRUCT(konst pt: POINT, konst mouseData: UInt, konst flags: UInt, konst time: UInt, konst dwExtraInfo: ULong) private class POINT(konst x: Int, konst y: Int) private class MSG(konst hwnd: CPointer<*>?, konst message: UInt, konst wParam: WPARAM, konst lParam: LPARAM, konst time: UInt, konst pt: POINT) }
macOS平台:对接Quartz Event Services
使用macOS原生的Quartz事件框架实现全局监听,需引导用户开启辅助功能权限:
// macosMain/kotlin/com/example/GlobalInputCapture.kt actual object GlobalInputCapture { private var eventTap: Long = 0 private lateinit var keyCallback: (KeyEvent) -> Unit private lateinit var mouseCallback: (MouseEvent) -> Unit actual fun startCapture(keyCallback: (KeyEvent) -> Unit, mouseCallback: (MouseEvent) -> Unit) { this.keyCallback = keyCallback this.mouseCallback = mouseCallback // 创建全局事件监听tap eventTap = CGEventTapCreate( kCGSessionEventTap, kCGHeadInsertEventTap, kCGEventTapOptionListenOnly, CGEventMaskBit(kCGEventKeyDown) or CGEventMaskBit(kCGEventKeyUp) or CGEventMaskBit(kCGEventLeftMouseDown) or CGEventMaskBit(kCGEventLeftMouseUp) or CGEventMaskBit(kCGEventRightMouseDown) or CGEventMaskBit(kCGEventRightMouseUp) or CGEventMaskBit(kCGEventMouseMoved), eventTapCallback, null ) if (eventTap != 0L) { konst runLoopSource = CFMachPortCreateRunLoopSource(null, eventTap, 0) CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, kCFRunLoopCommonModes) CGEventTapEnable(eventTap, true) // 后台启动RunLoop维持监听 thread { CFRunLoopRun() } } } actual fun stopCapture() { if (eventTap != 0L) { CGEventTapEnable(eventTap, false) CFRunLoopRemoveSource(CFRunLoopGetCurrent(), CFMachPortCreateRunLoopSource(null, eventTap, 0), kCFRunLoopCommonModes) CFMachPortInvalidate(eventTap) eventTap = 0L } } // 事件回调 private konst eventTapCallback = staticCFunction { proxy: Long, type: UInt, event: Long, refcon: COpaquePointer? -> when (type) { kCGEventKeyDown, kCGEventKeyUp -> { konst keyCode = CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode).toInt() keyCallback(KeyEvent(keyCode, type == kCGEventKeyDown)) } kCGEventLeftMouseDown, kCGEventLeftMouseUp, kCGEventRightMouseDown, kCGEventRightMouseUp -> { konst point = CGEventGetLocation(event) konst button = if (type == kCGEventLeftMouseDown || type == kCGEventLeftMouseUp) 1 else 2 mouseCallback(MouseEvent(point.x.toInt(), point.y.toInt(), button, type == kCGEventLeftMouseDown || type == kCGEventRightMouseDown)) } kCGEventMouseMoved -> { konst point = CGEventGetLocation(event) mouseCallback(MouseEvent(point.x.toInt(), point.y.toInt(), 0, false)) } } event // 传递事件给其他应用,避免阻塞系统输入 } // Quartz API声明(通过kotlinx-cinterop自动生成绑定) private external fun CGEventTapCreate(tap: UInt, place: UInt, options: UInt, eventsOfInterest: ULong, callback: CPointer<EventTapCallback>, refcon: COpaquePointer?): Long private external fun CGEventTapEnable(tap: Long, enable: Boolean) private external fun CGEventGetIntegerValueField(event: Long, field: UInt): ULong private external fun CGEventGetLocation(event: Long): CGPoint private external fun CFMachPortCreateRunLoopSource(allocator: COpaquePointer?, port: Long, order: Int): Long private external fun CFRunLoopAddSource(rl: Long, source: Long, mode: COpaquePointer?) private external fun CFRunLoopGetCurrent(): Long private external fun CFRunLoopRun() private external fun CFRunLoopRemoveSource(rl: Long, source: Long, mode: COpaquePointer?) private external fun CFMachPortInvalidate(port: Long) // 类型与常量定义 private typealias EventTapCallback = CFunction<(proxy: Long, type: UInt, event: Long, refcon: COpaquePointer?) -> Long> private data class CGPoint(val x: Double, val y: Double) private const val kCGSessionEventTap = 0u private const val kCGHeadInsertEventTap = 0u private const val kCGEventTapOptionListenOnly = 1u private const val kCGEventKeyDown = 10u private const val kCGEventKeyUp = 11u private const val kCGEventLeftMouseDown = 1u private const val kCGEventLeftMouseUp = 2u private const val kCGEventRightMouseDown = 3u private const val kCGEventRightMouseUp = 4u private const val kCGEventMouseMoved = 5u private const val kCGKeyboardEventKeycode = 0u private konst kCFRunLoopCommonModes = CPointer.fromRawValue(CFSTR("kCFRunLoopCommonModes").rawValue) }
Linux平台:对接X11 XRecord扩展
通过X11的XRecord扩展实现全局输入监听,需安装libx11-dev和libxrecord-dev依赖:
// linuxMain/kotlin/com/example/GlobalInputCapture.kt actual object GlobalInputCapture { private var display: Long = 0 private var recordContext: Long = 0 private lateinit var keyCallback: (KeyEvent) -> Unit private lateinit var mouseCallback: (MouseEvent) -> Unit actual fun startCapture(keyCallback: (KeyEvent) -> Unit, mouseCallback: (MouseEvent) -> Unit) { this.keyCallback = keyCallback this.mouseCallback = mouseCallback display = XOpenDisplay(null) if (display == 0L) error("Failed to open X11 display") // 创建XRecord监听范围 konst range = XRecordRangeCreate(display, XRecordFromServer, XRecordToServer) recordContext = XRecordCreateContext(display, 0, arrayOf(range).toCPointer(), 1) XRecordFreeData(display, range) // 启动监听 XRecordEnableContextAsync(display, recordContext, recordCallback, null) // 后台处理X事件 thread { konst event = alloc<XEvent>() while (XNextEvent(display, event.ptr) == 0) {} } } actual fun stopCapture() { XRecordDisableContext(display, recordContext) XRecordFreeContext(display, recordContext) XCloseDisplay(display) } // XRecord回调 private konst recordCallback = staticCFunction { display: Long, ctx: Long, xtype: Int, xrd: Long -> when (xtype) { KeyPress, KeyRelease -> { konst keyEvent = XRecordDatumGetKeyEvent(xrd) keyCallback(KeyEvent(keyEvent.keycode.toInt(), xtype == KeyPress)) } ButtonPress, ButtonRelease -> { konst mouseEvent = XRecordDatumGetMouseEvent(xrd) mouseCallback(MouseEvent(mouseEvent.x, mouseEvent.y, mouseEvent.button.toInt(), xtype == ButtonPress)) } MotionNotify -> { konst mouseEvent = XRecordDatumGetMouseEvent(xrd) mouseCallback(MouseEvent(mouseEvent.x, mouseEvent.y, 0, false)) } } } // X11 API声明(通过kotlinx-cinterop自动生成绑定) private external fun XOpenDisplay(name: String?): Long private external fun XCloseDisplay(dpy: Long) private external fun XRecordRangeCreate(dpy: Long, from: Int, to: Int): Long private external fun XRecordCreateContext(dpy: Long, priority: Int, ranges: CPointer<Long>, numRanges: Int): Long private external fun XRecordFreeData(dpy: Long, data: Long) private external fun XRecordEnableContextAsync(dpy: Long, ctx: Long, callback: CPointer<RecordCallback>, data: COpaquePointer?) private external fun XRecordDisableContext(dpy: Long, ctx: Long) private external fun XRecordFreeContext(dpy: Long, ctx: Long) private external fun XNextEvent(dpy: Long, event: CPointer<XEvent>): Int // 类型与常量定义 private typealias RecordCallback = CFunction<(display: Long, ctx: Long, xtype: Int, xrd: Long) -> Unit> private const val XRecordFromServer = 0 private const val XRecordToServer = 1 private const val KeyPress = 2 private const val KeyRelease = 3 private const val ButtonPress = 4 private const val ButtonRelease = 5 private const val MotionNotify = 6 private data class XKeyEvent(val keycode: UShort) private data class XMouseEvent(val x: Int, val y: Int, val button: UByte) private external fun XRecordDatumGetKeyEvent(xrd: Long): XKeyEvent private external fun XRecordDatumGetMouseEvent(xrd: Long): XMouseEvent }
三、性能优化要点
- 回调逻辑尽量轻量化,耗时操作放到后台线程处理,避免阻塞系统输入事件流
- 在原生层过滤不需要的事件,减少跨层调用次数
- 不用时及时调用
stopCapture()释放钩子/监听资源,避免内存泄漏
内容的提问来源于stack exchange,提问作者lpmwfx
相关产品推荐
相关产品推荐

