應用場景
開發應用程序的過程本質就是通過圖形庫獲得用戶的輸入事件(鼠標、鍵盤或者觸摸屏等)和數據以后,對這些用戶的事件和數據進行處理后,通過界面或其他交互形式展現給用戶結果。
應用程序完成后,擁有美觀的界面和簡潔易用的使用邏輯,讓用戶在使用過程中感到舒服和爽快,這樣的應用程序我們就可以稱為交互體驗優秀的產品。
一般來說,應用程序窗口的所有事件都可以通過圖形庫(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窗口,可以實時查看鼠標和鍵盤的事件信息,大家可以基于上面的代碼進行改造,以融合到自己的項目中。
我對開發者的學習一項新技術的建議是:
先拷貝現有代碼→精簡提煉出核心代碼→融合到自己的項目中,先會用→用的熟練以后再研究API和每一個參數細節→最后查看底層庫源代碼
只有先實踐才能真正理解開源項目的原作者為什么這么寫,最后才能真正吸收這些技術,做好開源貢獻。
歡迎加入WHLUG
每周三晚上 WHLUG 都會有這樣的技術干貨和大家分享,歡迎全國父老鄉親加入WHLUG, 加入武漢最純粹的開源線下技術聚會。;)