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, 加入武漢最純粹的開源線下技術聚會。;)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容