Linux全局事件监听技术

应用场景

开发应用程序的过程本质就是通过图形库获得用户的输入事件(鼠标、键盘或者触摸屏等)和数据以后,对这些用户的事件和数据进行处理后,通过界面或其他交互形式展现给用户结果。

应用程序完成后,拥有美观的界面和简洁易用的使用逻辑,让用户在使用过程中感到舒服和爽快,这样的应用程序我们就可以称为交互体验优秀的产品。

一般来说,应用程序窗口的所有事件都可以通过图形库(Gtk+、Qt等)自己来获取的,但是有时候我们需要一种技术来获取整个操作系统的事件,来满足以下场景:

  • 监听用户输入的鼠标事件,比如屏幕取词
  • 监听用户输入的键盘事件,比如全局快捷键

这时候Gtk+和Qt就无法做到,需要X11相关的技术才能做到对系统的事件进行监听。
X11相关的技术有两种方案:

XGrabPointer 和 XGrabKeyboard 一般主要用于菜单的实现,而且这种方法必须要抢占用户的鼠标或键盘焦点,导致一旦焦点被抢占时,别的程序就无法正常使用(比如菜单弹出时,其他程序就无法输入字符或响应鼠标事件了)。

大部分应用程序监听事件时往往并不需要抢占系统的事件焦点,希望在监听事件的时候用户可以正常操作系统。所以,今天讲解一下怎么用 XRecord 这个X11的扩展库来进行鼠标事件以及键盘事件的监听。

技术原理

X11是Linux下最古老和通用的技术,不论用户的输入事件还是最后画到屏幕的绘制动作其实都是 XServer 来实现的。

Linux下所有图形应用的底层消息顺序都是按照下面的顺序来执行的:
硬件产生事件→XServer发送输入事件给图形库→图形库(X Client)包装输入事件传递给应用程序→应用根据输入事件产生绘制命令→图形库(X Client)根据应用绘制命令产生绘制消息→XServer接受绘制消息→绘制图形到屏幕上。

上面顺序中的 X Client 就是我们通常说的 Gtk+、Qt这些图形库,通过 xcb/xlib 和 XServer 进行输入输出通讯,保证输入事件和输出绘制都可以及时响应,同时图形开发库提供高级的API封装,让开发的同学不用直接编写复杂的Xcb/Xlib 通讯代码和参数细节。

而 XRecord 就是一个 XServer 端的扩展,你可以想象 XRecord 就像一条寄生虫寄生到 XServer 里面,只要 XServer 从硬件那里接收到所有输入事件都会告诉一下 XRecord, 我们只需把对应的代码挂到 XRecord 循环中,只有系统一有输入事件产生,XServer就会告诉XRecord, XRecord接着就通过事件循环告诉我们写的应用程序,我们的应用程序再利用实时截获到的输入事件进行处理。

这一切都发生的悄无声息,既监听了系统上所有的输入事件又不会影响系统中的任何应用,是不是听着很邪恶?(刀能切菜也能伤害别人,千万不要做坏事哟)

代码讲解

输入事件监听的核心代码都在 event_monitor.cpp 中,下面我一个一个函数的讲解:

// 因为 XRecord 的事件循环会堵塞当前线程,避免监听事件的时候应用程序卡主
// 我们建立一个继承于 QThread 的EventMonitor类,通过子线程进行事件监听操作
EventMonitor::EventMonitor(QObject *parent) : QThread(parent)
{
 // 鼠标按下标志位,用于识别鼠标的拖拽操作
    isPress = false;
}
void EventMonitor::run()
{
    // 创建记录 XRecord 协议的 X 专用连接
    Display* display = XOpenDisplay(0);

    // 连接打开检查
    if (display == 0) {
        fprintf(stderr, "unable to open display\n");
        return;
    }

    // 初始化 XRecordCreateContext 所需的 XRecordClientSpec 参数
    // XRecordAllClients 的意思是 "记录所有 X Client" 的事件
    XRecordClientSpec clients = XRecordAllClients;

    // 创建 XRecordRange 变量,XRecordRange 用于控制记录事件的范围
    XRecordRange* range = XRecordAllocRange();

    // 记录事件范围检查
    if (range == 0) {
        fprintf(stderr, "unable to allocate XRecordRange\n");
        return;
    }

    // 初始化记录事件范围,范围开头设置成 KeyPress, 范围结尾设置成 MotionNotify 后
    // 事件的类型就包括 KeyPress、KeyRelase、ButtonPress、ButtonRelease、MotionNotify五种事件
    memset(range, 0, sizeof(XRecordRange));
    range->device_events.first = KeyPress;
    range->device_events.last  = MotionNotify;

    // 根据上面的记录客户端类型和记录事件范围来创建 “记录上下文”
    // 然后把 XRecordContext 传递给 XRecordEnableContext 函数来开启事件记录循环
    XRecordContext context = XRecordCreateContext (display, 0, &clients, 1, &range, 1);
    if (context == 0) {
        fprintf(stderr, "XRecordCreateContext failed\n");
        return;
    }

    // 释放 range 指针
    XFree(range);

    // XSync 的作用就是把上面的 X 代码立即发给 X Server
    // 这样 X Server 接受到事件以后会立即发送给 XRecord 的 Client 连接
    XSync(display, True);

    // 建立一个专门读取 XRecord 协议数据的 X 链接
    Display* display_datalink = XOpenDisplay(0);

    // 连接打开检查
    if (display_datalink == 0) {
        fprintf(stderr, "unable to open second display\n");
        return;
    }

    // 调用 XRecordEnableContext 函数建立 XRecord 上下文
    // XRecordEnableContext 函数一旦调用就开始进入堵塞时的事件循环,直到线程或所属进程结束
    // X Server 事件一旦发生就传递给事件处理回调函数
    if (!XRecordEnableContext(display_datalink, context,  callback, (XPointer) this)) {
        fprintf(stderr, "XRecordEnableContext() failed\n");
        return;
    }
}
// handleRecordEvent 函数的wrapper,避免 XRecord 代码编译不过的问题
void EventMonitor::callback(XPointer ptr, XRecordInterceptData* data)
{
    ((EventMonitor *) ptr)->handleRecordEvent(data);
}

// 真实处理 X 事件监听的回调函数
void EventMonitor::handleRecordEvent(XRecordInterceptData* data)
{
    if (data->category == XRecordFromServer) {
        // 得到 xEvent 对象
        xEvent * event = (xEvent *)data->data;
        switch (event->u.u.type) {
        case ButtonPress:
            // 过滤掉滚轮事件后,发送 buttonPress 信号
            if (filterWheelEvent(event->u.u.detail)) {
                isPress = true;
                emit buttonPress(
                    event->u.keyButtonPointer.rootX, 
                    event->u.keyButtonPointer.rootY);
            }
            
            break;
        case MotionNotify:
            // 只有在按下鼠标的时候移动,才发送 buttonDrag 信号
            if (isPress) {
                emit buttonDrag(
                    event->u.keyButtonPointer.rootX, 
                    event->u.keyButtonPointer.rootY);
            }
            
            break;
        case ButtonRelease:
            // 过滤掉滚轮事件后,发送 buttonRelase 信号            
            if (filterWheelEvent(event->u.u.detail)) {
                isPress = false;
                emit buttonRelease(
                    event->u.keyButtonPointer.rootX, 
                    event->u.keyButtonPointer.rootY);
            }
            
            break;
        case KeyPress:
            // 发送 keyPress 信号,附带按键的 code
            emit keyPress(((unsigned char*) data->data)[1]);
            
            break;
        case KeyRelease:
            // 发送 keyRelease 信号,附带按键的 code
            emit keyRelease(((unsigned char*) data->data)[1]);
            
            break;
        default:
            break;
        }
    }

    // 资源释放
    fflush(stdout);
    XRecordFreeData(data);
}

// 过滤滚轮事件
bool EventMonitor::filterWheelEvent(int detail)
{
    return detail != WheelUp && detail != WheelDown && detail != WheelLeft && detail != WheelRight;
}

代码下载

可编译的代码请在 https://github.com/WHLUG/xrecord-example 下载后,执行下面的命令来测试:

mkdir build
cd build
qmake ..
make
./xrecord-example

编译完成以后,会弹出一个Qt窗口,可以实时查看鼠标和键盘的事件信息,大家可以基于上面的代码进行改造,以融合到自己的项目中。

深度截图20170321170201.png

我对开发者的学习一项新技术的建议是:

先拷贝现有代码→精简提炼出核心代码→融合到自己的项目中,先会用→用的熟练以后再研究API和每一个参数细节→最后查看底层库源代码

只有先实践才能真正理解开源项目的原作者为什么这么写,最后才能真正吸收这些技术,做好开源贡献。

欢迎加入WHLUG

每周三晚上 WHLUG 都会有这样的技术干货和大家分享,欢迎全国父老乡亲加入WHLUG, 加入武汉最纯粹的开源线下技术聚会。;)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,636评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,890评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,680评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,766评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,665评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,045评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,515评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,182评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,334评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,274评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,319评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,002评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,599评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,675评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,917评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,309评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,885评论 2 341

推荐阅读更多精彩内容