linux 使用X11监听键盘鼠标输入

前言

在Linux桌面开发中,监控全局键盘鼠标输入是实现快捷键管理器、自动化脚本、屏幕录制工具等功能的核心技术。

X Window System(X11)提供了丰富的API来捕获和处理输入事件。

本文将深入探讨如何使用X11监听全局键盘鼠标输入,构建功能强大的输入监控工具。

X11输入事件系统架构

1.1 输入事件传递机制
text 复制代码
输入设备 → X Server → 焦点窗口 → 应用程序

X11采用事件驱动模型,所有输入事件首先由X Server接收,然后分发给具有焦点的客户端窗口。

1.2 输入事件类型

键盘事件:KeyPress、KeyRelease

鼠标事件:ButtonPress、ButtonRelease、MotionNotify

焦点事件:FocusIn、FocusOut

输入法事件:MappingNotify

1.3 事件掩码与选择

X11使用事件掩码机制,客户端需要显式选择要接收的事件类型:

c++ 复制代码
// 常用输入事件掩码
KeyPressMask | KeyReleaseMask    // 键盘按下/释放
ButtonPressMask | ButtonReleaseMask  // 鼠标按钮
PointerMotionMask                 // 鼠标移动
EnterWindowMask | LeaveWindowMask    // 窗口进入/离开

基础键鼠事件监听

由于X11原生的XSelectInput无法实现全局监听,而XGrab/XUngrab族函数在执行期间会独占键鼠活动,影响原有的功能。因此,只能通过额外的扩展来实现监听。

2.1 键码解析

由于X11使用KeyCode描述具体的物理按键,因此需要通过XkbKeycodeToKeysymKeyCode转换为KeySym符号,进而获取按键的具体描述。

cpp 复制代码
KeySym LookupKeysym(Display *display, KeyCode key, KeyButMask mask) {
    // 获取按键的映射
    XkbDescPtr xkb = XkbGetMap(display.get(), XkbAllComponentsMask, XkbUseCoreKbd);
    unsigned mods_rtrn;
    KeySym sym;
    // 转换字符码
    XkbTranslateKeyCode(xkb, key, mask, &mods_rtrn, &sym);
    XkbFreeClientMap(xkb, 0, True);
    return sym;
}

高级键鼠事件监听

3.1 XInput扩展
cpp 复制代码
int main()
{
    Display *display = XOpenDisplay(NULL);
    if (!display) {
        fprintf(stderr, "无法打开 X 显示\n");
        return -1;
    }
    
    int eventv, xi_opcode;
    if (!XQueryExtension(display, "XInputExtension", &xi_opcode, &eventv,
                         NULL)) {
        fprintf(stderr, "XInput 扩展不可用\n");
        return -1;
    }

    XIEventMask event_mask;
    unsigned char mask[2] = {0};  // 位掩码
    // XI_ButtonPress 和 XI_ButtonRelease
    XISetMask(mask, XI_ButtonPress);
    XISetMask(mask, XI_KeyPress);

    event_mask.deviceid = XIAllDevices;  // 这里必须指定XIAllDevices,才能监听全局的点击事件
    event_mask.mask_len = sizeof(mask);
    event_mask.mask = mask;

    XISelectEvents(display, DefaultRootWindow(display), &event_mask,1);
    XFlush(display);

    XEvent event;
    XGenericEventCookie* cookie;
    XIDeviceEvent* dev_event;
    while (1) {
        XNextEvent(display, &event);
        // 检查是否是XInput2事件
        if (event.type != GenericEvent) continue;
        cookie = &event.xcookie;
        // 检查是否是XInput扩展事件
        if (cookie->extension != xi_opcode) continue;
        // 获取事件数据
        if (!XGetEventData(display, cookie)) continue;
        // 处理不同的事件类型
        switch (cookie->evtype) {
        case XI_ButtonPress: {
            dev_event = (XIDeviceEvent*)cookie->data;
            printf("=== Mouse Button Press ===\n");
            printf("  Button: %d\n", dev_event->detail);
            printf("  Root Coordinates: (%f, %f)\n", dev_event->root_x,
                   dev_event->root_y);
            break;
        }
        case XI_KeyPress: {
            dev_event = (XIDeviceEvent*)cookie->data;
            printf("=== Key Press ===\n");
            printf("  Key: %d\n", dev_event->detail);
            printf("  Mask: %d\n", dev_event->mods.effective);
            break;
        }
        }

        // 释放事件数据
        XFreeEventData(display, cookie);
    }

    XCloseDisplay(display);
    return 0;
}
3.2 Record扩展
cpp 复制代码
extern "C" void event_callback(XPointer closure, XRecordInterceptData* data) {
    if (data->category != XRecordFromServer) {
        XRecordFreeData(data);
        return;
    }

    xEvent* xevent = reinterpret_cast<xEvent*>(data->data);
    if (xevent->u.u.type == ButtonPress) {  // ButtonPress
        short x = xevent->u.keyButtonPointer.rootX;
        short y = xevent->u.keyButtonPointer.rootY;
        printf("=== Mouse Button Press ===\n");
        printf("  Button: %d\n", xevent->u.u.detail);
        printf("  Root Coordinates: (%f, %f)\n", x, y);
            
    } else if (xevent->u.u.type == KeyPress) {
        printf("=== Key Press ===\n");
        printf("  Key: %d\n", xevent->u.u.detail);
        printf("  Mask: %d\n", xevent->u.keyButtonPointer.state);
        printf("Key Press = %d %d\n", xevent->u.u.detail);
    }
    XRecordFreeData(data);
}

void record_thread(Display* display, XRecordContext context) {
    // 启用异步记录上下文
    if (!XRecordEnableContext(display, context, event_callback, NULL)) {
        fprintf(stderr, "无法启用记录上下文\n");
        return;
    }
}

int main()
{
    Display *display = XOpenDisplay(NULL);
    if (!display) {
        fprintf(stderr, "无法打开 X 显示\n");
        return -1;
    }
    
    int major = 0, minor = 0;
    if (!XRecordQueryVersion(display, &major, &minor)) {
        fprintf(stderr, "XRecord 扩展不可用\n");
        return -1;
    }

    XRecordRange* range = XRecordAllocRange();
    if (!range) {
        fprintf(stderr, "无法分配范围\n");
        return -1;
    }

    memset(range, 0, sizeof(XRecordRange));
    range->device_events.first = KeyPress;    // 4 ButtonPress
    range->device_events.last = ButtonPress;  // 5 ButtonRelease

    XRecordClientSpec clients = XRecordAllClients;

    // 创建记录上下文
    XRecordContext context =
        XRecordCreateContext(display, 0, &clients, 1, &range, 1);
    if (!context) {
        fprintf(stderr, "无法创建记录上下文\n");
        XFree(range);
        return -1;
    }
    XFree(range);

    std::thread notify(&record_thread, display, context);
 
    // 此处接收停止信号后,禁用上下文结束监控
    XRecordDisableContext(display, context);

    // 等待线程退出
    notify.join();
    // 清理
    XRecordFreeContext(display, context);

    XCloseDisplay(display);
    return 0;
}
相关推荐
kida_yuan2 小时前
【Linux】说说我对 Wine 与 deepin-wine 的理解
linux·运维·wine
嵌入小生0073 小时前
基于Linux系统下的C语言程序错误及常见内存问题调试方法教程(嵌入式-Linux-C语言)
linux·c语言·开发语言·嵌入式·小白·内存管理调试·程序错误调试
松涛和鸣3 小时前
DAY63 IMX6ULL ADC Driver Development
linux·运维·arm开发·单片机·嵌入式硬件·ubuntu
帅得不敢出门4 小时前
Android Framework在mk中新增类似PRODUCT_MODEL的变量并传递给buildinfo.sh及prop属性中
android·linux·前端
阿拉伯柠檬5 小时前
网络层协议IP(三)
linux·网络·网络协议·tcp/ip·面试
Miracle&5 小时前
在Linux VirtualBox中安装系统失败
linux·运维·服务器
hweiyu005 小时前
Linux 命令:ar
linux·运维
江畔何人初6 小时前
理解容器挂载点
linux·运维·云原生
YMWM_6 小时前
cursor连接Ubuntu远程
linux·运维·ubuntu