天天看點

Linux全局事件監聽技術

原文位址::http://www.51testing.com/html/22/n-3716622.html

應用場景   開發應用程式的過程本質就是通過圖形庫獲得使用者的輸入事件(滑鼠、鍵盤或者觸摸屏等)和資料以後,對這些使用者的事件和資料進行處理後,通過界面或其他互動形式展現給使用者結果。   應用程式完成後,擁有美觀的界面和簡潔易用的使用邏輯,讓使用者在使用過程中感到舒服和爽快,這樣的應用程式我們就可以稱為互動體驗優秀的産品。   一般來說,應用程式視窗的所有事件都可以通過圖形庫(Gtk+、Qt等)自己來擷取的,但是有時候我們需要一種 技術來擷取整個 作業系統的事件,來滿足以下場景:   · 監聽使用者輸入的滑鼠事件,比如螢幕取詞   · 監聽使用者輸入的鍵盤事件,比如全局快捷鍵   這時候Gtk+和Qt就無法做到,需要X11相關的技術才能做到對系統的事件進行監聽。    X11相關的技術有兩種方案:   · 通過 XGrabPointer 和 XGrabKeyboard 抓取系統的焦點後監聽全局事件   · 通過 XRecord Extension 非侵入式的監聽全局事件   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視窗,可以實時檢視滑鼠和鍵盤的事件資訊,大家可以基于上面的代碼進行改造,以融合到自己的項目中。  

Linux全局事件監聽技術

   我對開發者的學習一項新技術的建議是:   先拷貝現有代碼→精簡提煉出核心代碼→融合到自己的項目中,先會用→用的熟練以後再研究API和每一個參數細節→最後檢視底層庫源代碼   隻有先實踐才能真正了解開源項目的原作者為什麼這麼寫,最後才能真正吸收這些技術,做好開源貢獻。