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

Ubuntu下如何修改X11应用代码实现全局键盘输入捕获?

如何修改Ubuntu下的键盘捕获代码以实现全局键盘监听

现有一款基于X11库开发、在Ubuntu环境测试的应用,可接收简单键盘输入并输出对应值,但仅在应用窗口(Ubuntu终端)处于活跃状态时正常工作。实际需求是捕获操作系统内所有应用的键盘输入并进行后续处理。


现有代码

linux-kbhit.h

#define     ESC     "\033"
#define     UP      "\033[A"
#define     DOWN    "\033[B"
#define     LEFT    "\033[D"
#define     RIGHT   "\033[C"
#define     A   "a"

void term_setup(void (*sighandler)(int));
void term_restore();
bool kbhit();
bool keydown(const char* key);

linux-kbhit.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <unistd.h>
#include <poll.h>
#include <signal.h>
#include <termios.h>
#include <sys/ioctl.h>

static struct termios oldtio;
static struct termios curtio;

void term_setup(void (*sighandler)(int)){
    struct sigaction sa;

    /* Save stdin terminal attributes */
    tcgetattr(0, &oldtio);

    if(sighandler){
    /* Make sure we exit cleanly */
        memset(&sa, 0, sizeof(struct sigaction));
        sa.sa_handler = sighandler;
        sigaction(SIGINT, &sa, NULL);
        sigaction(SIGQUIT, &sa, NULL);
        sigaction(SIGTERM, &sa, NULL);
    }

    /* This is needed to be able to tcsetattr() after a hangup (Ctrl-C)
     * see tcsetattr() on POSIX
     */
    memset(&sa, 0, sizeof(struct sigaction));
    sa.sa_handler = SIG_IGN;
    sigaction(SIGTTOU, &sa, NULL);

    /* Set non-canonical no-echo for stdin */
    tcgetattr(0, &curtio);
    curtio.c_lflag &= ~(ICANON | ECHO);
    tcsetattr(0, TCSANOW, &curtio);
}

void term_restore(){
    /* restore terminal attributes */
    tcsetattr(0, TCSANOW, &oldtio);
}

static char get[4]= {0};
bool kbhit(){
    struct pollfd pfds[1];
    int ret;
    memset(get, 0, sizeof(char) * 4);

    /* See if there is data available */
    pfds[0].fd = 0;
    pfds[0].events = POLLIN;
    ret = poll(pfds, 1, 0);

    /* Consume data */
    if (ret > 0) {
        read(0, get, 3);
        return strlen(get) > 0;
    }
    return false;
}

bool keydown(const char* key){
    return !strcmp(get, key);
}

main.c

#include "linux-kbhit.h"
#include "stdio.h"
#include <signal.h>

static sig_atomic_t end = 0;
static void sighandler(int signo)
{
    end = 1;
    printf("good beye!\n");
}

int main()
{
    term_setup(sighandler);

    while (!end) {
        if (kbhit()) {
            if (keydown(ESC))
                printf("This is \"ESC\" button!\n");
            if (keydown(UP))
                printf("This is \"UP\" button!\n");
            if (keydown(DOWN))
                printf("This is \"DOWN\" button!\n");
            if (keydown(LEFT))
                printf("This is \"LEFT\" button!\n");
            if (keydown(RIGHT))
                printf("This is \"RIGHT\" button!\n");
            if (keydown(A))
                printf("This is \"A\" button!\n");
        }
    }

    term_restore();

    return 0;
}

全局键盘监听修改方案

原代码仅监听当前终端的标准输入,无法捕获全局事件。要实现全局监听,需借助X11的XRecord扩展(适用于Xorg环境),它能捕获X服务器上的所有输入事件。以下是具体修改步骤:

1. 更新头文件(linux-kbhit.h)

移除终端相关定义,添加X11全局监听的声明:

#ifndef LINUX_KBHIT_H
#define LINUX_KBHIT_H

// 键盘按键对应的keycode(可通过xev工具查看)
#define KEY_ESC 9
#define KEY_UP 111
#define KEY_DOWN 116
#define KEY_LEFT 113
#define KEY_RIGHT 114
#define KEY_A 38

void setup_global_key_listener(void (*sighandler)(int));
void cleanup_global_key_listener();
// 按键事件回调函数类型
typedef void (*KeyEventCallback)(int keycode, bool is_pressed);
void set_key_event_callback(KeyEventCallback callback);

#endif

2. 重写全局监听逻辑(linux-kbhit.c)

替换原有终端处理代码,使用XRecord实现全局键盘捕获:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <X11/Xlib.h>
#include <X11/extensions/Xrecord.h>

static Display *dpy = NULL;
static XRecordContext record_context = 0;
static KeyEventCallback key_callback = NULL;
static sig_atomic_t end = 0;

static void sighandler(int signo) {
    end = 1;
}

// XRecord事件处理回调
static void record_callback(XPointer ptr, XRecordInterceptData *data) {
    if (end) {
        XRecordFreeData(data);
        return;
    }

    if (data->category == XRecordFromServer) {
        xEvent *event = (xEvent *)data->data;
        // 仅处理键盘按下/释放事件
        if (event->u.u.type == KeyPress || event->u.u.type == KeyRelease) {
            int keycode = event->u.keyButtonPointer.detail;
            bool is_pressed = (event->u.u.type == KeyPress);
            if (key_callback != NULL) {
                key_callback(keycode, is_pressed);
            }
        }
    }
    XRecordFreeData(data);
}

void set_key_event_callback(KeyEventCallback callback) {
    key_callback = callback;
}

void setup_global_key_listener(void (*sighandler)(int)) {
    if (sighandler) {
        signal(SIGINT, sighandler);
        signal(SIGQUIT, sighandler);
        signal(SIGTERM, sighandler);
    }

    // 打开X显示
    dpy = XOpenDisplay(NULL);
    if (!dpy) {
        fprintf(stderr, "无法打开X显示\n");
        exit(EXIT_FAILURE);
    }

    // 配置XRecord监听范围
    XRecordClientSpec clients = XRecordAllClients;
    XRecordRange *range = XRecordAllocRange();
    if (!range) {
        fprintf(stderr, "无法分配XRecord范围\n");
        XCloseDisplay(dpy);
        exit(EXIT_FAILURE);
    }
    memset(range, 0, sizeof(XRecordRange));
    range->device_events.first = KeyPress;
    range->device_events.last = KeyRelease;

    // 创建XRecord上下文
    record_context = XRecordCreateContext(dpy, 0, &clients, 1, &range, 1);
    XFree(range);

    if (!record_context) {
        fprintf(stderr, "无法创建XRecord上下文\n");
        XCloseDisplay(dpy);
        exit(EXIT_FAILURE);
    }

    // 启动异步监听
    if (!XRecordEnableContextAsync(dpy, record_context, record_callback, NULL)) {
        fprintf(stderr, "无法启用XRecord上下文\n");
        XRecordFreeContext(dpy, record_context);
        XCloseDisplay(dpy);
        exit(EXIT_FAILURE);
    }
}

void cleanup_global_key_listener() {
    if (dpy && record_context) {
        XRecordDisableContext(dpy, record_context);
        XRecordFreeContext(dpy, record_context);
        XCloseDisplay(dpy);
    }
}

3. 修改main.c适配全局监听

更新主函数,设置按键回调并运行X事件循环:

#include "linux-kbhit.h"
#include <stdio.h>
#include <signal.h>

static sig_atomic_t end = 0;

static void sighandler(int signo) {
    end = 1;
    printf("再见!\n");
}

// 处理捕获到的按键事件
static void handle_key_event(int keycode, bool is_pressed) {
    if (!is_pressed) {
        return; // 仅处理按键按下事件,可按需调整为处理释放事件
    }
    switch (keycode) {
        case KEY_ESC:
            printf("这是\"ESC\"按键!\n");
            break;
        case KEY_UP:
            printf("这是\"UP\"按键!\n");
            break;
        case KEY_DOWN:
            printf("这是\"DOWN\"按键!\n");
            break;
        case KEY_LEFT:
            printf("这是\"LEFT\"按键!\n");
            break;
        case KEY_RIGHT:
            printf("这是\"RIGHT\"按键!\n");
            break;
        case KEY_A:
            printf("这是\"A\"按键!\n");
            break;
        default:
            // printf("捕获到按键: %d\n", keycode); // 可开启查看所有按键的keycode
            break;
    }
}

int main() {
    setup_global_key_listener(sighandler);
    set_key_event_callback(handle_key_event);

    // 事件循环,保持程序运行并处理X事件
    while (!end) {
        XEvent event;
        XNextEvent(dpy, &event);
    }

    cleanup_global_key_listener();
    return 0;
}

4. 编译与运行

编译时需要链接X11和XRecord相关库:

gcc main.c linux-kbhit.c -o global-key-listener -lX11 -lXrecord

运行程序:

./global-key-listener

若遇到权限问题,可尝试使用sudo运行。

关键注意事项

  • 环境适配:该方案仅支持Xorg显示服务器。如果你的Ubuntu使用Wayland,需改用其他方案(如监听/dev/input/event*设备,需要root权限)。
  • 按键码调整:不同键盘布局的按键码可能不同,可使用xev工具查看目标按键的实际keycode,再修改头文件中的定义。
  • 权限问题:全局输入监听需要较高权限,部分系统可能需要配置X访问权限或使用sudo运行。

内容的提问来源于stack exchange,提问作者Jack3n

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 22:15:54