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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 02:35:01