X Window的GUI功能只有在畫面上開一個矩形區(qū)域,沒有提供按鈕、文字輸入框這些元件的繪製和事件處理,想用GUI元件必須用Motif、GTK、Qt等較上層的函式庫。但遊戲程式只要建一個頂層視窗,之後畫圖和處理輸入都自己來,用底層的X Window可以減少一些多餘的負擔(dān),就要用本篇的方法處理輸入。
跟Windows篇一樣,觸控板、繪圖板等指標裝置也可以視為滑鼠輸入,但這個方法不能讀取感壓和多點觸控;如果電腦接兩個鍵盤或指標裝置,不能分辨是哪個裝置輸入;手把輸入要用另一種方法讀取,以後另寫一篇介紹。
以「如何建一個視窗—X Window篇」為基礎(chǔ)增加一些東西。
key_mouse.c
人操作鍵盤和滑鼠的時候,輸入裝置的資訊會送給X server,X server根據(jù)目前狀態(tài),如游標位置、目前作用中視窗判斷由哪一個視窗處理事件,再把訊息傳給那個X client。
比前一篇增加的部分是,eventMask要根據(jù)想處理的事件填寫;以及XNextEvent()取得事件資料後switch{}裡面增加一些處理。
XNextEvent()把事件資料存在XEvent裡面,前一篇說過XEvent的宣告如下:
是個union,type、xkey、xbutton這些成員都佔用相同空間,先檢查evt.type的值再看要把這一塊資料解釋成哪個struct。
之前說過,寫X Window程式有個困難是說明文件很少,這個網(wǎng)站是我找到比較詳細的。
https://tronche.com/gui/x/
https://tronche.com/gui/x/xlib/
其中Chapter 10和10.5.2是本篇主要看的部分。
Chapter 10: Events
10.5.2 Keyboard and Pointer Events
有時候只看這些文件還不夠,像如何切換全螢?zāi)荒J健和N災(zāi)槐Wo程式這些文件也沒有說明,筆者開發(fā)遊戲的時候參考了一些函式庫的程式碼(如SDL)才知道怎麼做。
一、滑鼠輸入
二、鍵盤輸入
用這個指令build。
因為要用printf()印出訊息,請用命令列打指令執(zhí)行而不是用滑鼠點。
執(zhí)行時按按看各個滑鼠和鍵盤按鍵
如果視窗不是作用中(用滑鼠點一下視窗外面,讓標題變成灰色),則程式不會收到鍵盤和滑鼠事件。
圖中一部分訊息的意義
鍵盤、滑鼠、還有之後要介紹的手把輸入,有很多細節(jié)官方文件也沒寫,是筆者自己寫程式、把輸入裝置用所有方法操作一遍、用printf()印出內(nèi)部數(shù)值,才知道規(guī)則,例如以下幾點:
keysymdef.h裡面常數(shù)相當多,各種語言的鍵盤都有,筆者有寫個程式試過自己的鍵盤上有哪些keycode和keysym。
鍵盤碼與按鈕名稱對應(yīng)表
本程式不能處理CapsLock鍵,無論CapsLock燈有沒有亮,按鍵都傳回相同的keycode,想支援CapsLock要自己根據(jù)evt.xkey.state做判斷;也不能處理輸入法,切換輸入法之後程式仍然收不到中文字碼。X Window要處理這些情況有些複雜,本篇不介紹,一般遊戲程式也只需要知道實體按鍵而不需要用輸入法打中日韓文。如果使用GTK或Qt之類的GUI函式庫,它內(nèi)部會處理好輸入法。
在一般軟體的文字輸入框按住一個鍵不放,會重覆出現(xiàn)同一個字元,X Windows底層的事件會先收到一個press訊息,然後每次收到一組release和press訊息,處理方式和Windows API不一樣。
有兩個函式可以設(shè)定自動重覆:XAutoRepeatOn()和XAutoRepeatOff(),但電腦上的其他程式也會受影響。
據(jù)我所知,Linux讀取鍵盤和滑鼠輸入有以下幾種方法:
跟Windows篇一樣,觸控板、繪圖板等指標裝置也可以視為滑鼠輸入,但這個方法不能讀取感壓和多點觸控;如果電腦接兩個鍵盤或指標裝置,不能分辨是哪個裝置輸入;手把輸入要用另一種方法讀取,以後另寫一篇介紹。
以「如何建一個視窗—X Window篇」為基礎(chǔ)增加一些東西。
key_mouse.c
#include<X11/XKBlib.h> //使用XkbKeycodeToKeysym(),且間接引用其他Xlib header #include<stdio.h> #include<stdint.h> static uint32_t getKeysym(Display* dsp, int keycode, int hasShift){ if(hasShift){ hasShift=1; } return XkbKeycodeToKeysym(dsp, keycode, 0, hasShift); } int main(){ Display* dsp = XOpenDisplay( NULL ); Window window = XCreateSimpleWindow(dsp, DefaultRootWindow(dsp), 100, 100, 200, 200, //x,y,w,h 0, 0, // border 0 ); // backgd //設(shè)定事件mask const int eventMask= ButtonPressMask|ButtonReleaseMask|KeyPressMask|KeyReleaseMask|PointerMotionMask; XSelectInput(dsp,window,eventMask); Atom wmDelete=XInternAtom(dsp,"WM_DELETE_WINDOW",False); XSetWMProtocols(dsp,window,&wmDelete,1); //設(shè)定標題 XStoreName(dsp, window, "title"); XMapWindow(dsp, window); XEvent evt; int isRunning=1; while(isRunning){ XNextEvent(dsp,&evt); switch(evt.type){ //滑鼠輸入 case ButtonPress: printf("button press %u cursor:(%d,%d) state:%d\n", evt.xbutton.button, evt.xbutton.x, evt.xbutton.y, evt.xbutton.state); break; case ButtonRelease: printf("button release %u cursor:(%d,%d) state:%d\n", evt.xbutton.button, evt.xbutton.x, evt.xbutton.y, evt.xbutton.state); break; /*case MotionNotify: printf("pointer motion %u cursor:(%d,%d)\n", evt.xmotion.is_hint, evt.xmotion.x, evt.xmotion.y); break;*/ //鍵盤輸入 case KeyPress: printf("key press %u keysym:%4x state:%u\n", evt.xkey.keycode, getKeysym(dsp, evt.xkey.keycode, evt.xkey.state&ShiftMask), evt.xkey.state); break; case KeyRelease: printf("key release %u keysym:%4x state:%u\n", evt.xkey.keycode, getKeysym(dsp, evt.xkey.keycode, evt.xkey.state&ShiftMask), evt.xkey.state); break; case ClientMessage: if(evt.xclient.data.l[0]==wmDelete){ XDestroyWindow(dsp,window); XFlush(dsp); isRunning=0; } break; } } XCloseDisplay(dsp); return 0; } |
人操作鍵盤和滑鼠的時候,輸入裝置的資訊會送給X server,X server根據(jù)目前狀態(tài),如游標位置、目前作用中視窗判斷由哪一個視窗處理事件,再把訊息傳給那個X client。
比前一篇增加的部分是,eventMask要根據(jù)想處理的事件填寫;以及XNextEvent()取得事件資料後switch{}裡面增加一些處理。
XNextEvent()把事件資料存在XEvent裡面,前一篇說過XEvent的宣告如下:
//在這個檔案:/usr/include/X11/Xlib.h typedef union _XEvent { int type; XAnyEvent xany; XKeyEvent xkey; XButtonEvent xbutton; …… XClientMessageEvent xclient; XMappingEvent xmapping; XErrorEvent xerror; } XEvent; |
之前說過,寫X Window程式有個困難是說明文件很少,這個網(wǎng)站是我找到比較詳細的。
https://tronche.com/gui/x/
https://tronche.com/gui/x/xlib/
其中Chapter 10和10.5.2是本篇主要看的部分。
Chapter 10: Events
10.5.2 Keyboard and Pointer Events
有時候只看這些文件還不夠,像如何切換全螢?zāi)荒J健和N災(zāi)槐Wo程式這些文件也沒有說明,筆者開發(fā)遊戲的時候參考了一些函式庫的程式碼(如SDL)才知道怎麼做。
一、滑鼠輸入
首先要查文件確認mask、event type和struct各是什麼,然後填寫mask,且switch{}裡面增加對應(yīng)的處理。
10.4 Event Processing Overview
本篇處理的事件如下滾輪也是用這兩個事件處理。
event mask evt.type struct ButtonPressMask ButtonPress XButtonEvent 按鈕按下 ButtonReleaseMask ButtonRelease XButtonEvent 按鈕放開
XButtonEvent定義如下比較重要的欄位有以下,其他的就請自己看註解或自己試。
typedef struct {
int type; /* ButtonPress or ButtonRelease */
unsigned long serial; /* # of last request processed by server */
Bool send_event; /* true if this came from a SendEvent request */
Display *display; /* Display the event was read from */
Window window; /* "event" window it is reported relative to */
Window root; /* root window that the event occurred on */
Window subwindow; /* child window */
Time time; /* milliseconds */
int x, y; /* pointer x, y coordinates in event window */
int x_root, y_root; /* coordinates relative to root */
unsigned int state; /* key or button mask */
unsigned int button; /* detail */
Bool same_screen; /* same screen flag */
} XButtonEvent;
type:與evt.type佔用相同空間,所以值也相同。
button:按鈕編號,1~5分別是左鍵、中鍵、右鍵、滾輪往上、滾輪往下。
x、y:游標在視窗工作區(qū)裡的坐標。
x_root、y_root:游標在root window裡的坐標,通常是指螢?zāi)簧系奈恢谩?br>state:目前一些鍵盤和滑鼠按鈕是否被按下,是一組bit flag,有以下的常數(shù)。
右行是在筆者的電腦上試的結(jié)果,Shift、Ctrl、Alt不分左右
常數(shù)名稱 值 按鍵 ShiftMask 1 Shift LockMask 2 Caps Lock ControlMask 4 Ctrl Mod1Mask 8 Alt Mod2Mask 16 Num Lock Mod3Mask 32 Mod4Mask 64 Mod5Mask 128 Button1Mask 256 滑鼠按鈕與滾輪 Button2Mask 512 Button3Mask 1024 Button4Mask 2048 Button5Mask 4096
程式裡還有一個註解掉的事件MotionNotify,這是游標有移動就會觸發(fā),為了避免printf()印出太多訊息,本篇不處理此事件,有興趣請自己試。
二、鍵盤輸入
本篇處理的事件如下
event mask evt.type struct KeyPressMask KeyPress XKeyEvent 按鍵按下 KeyReleaseMask KeyRelease XKeyEvent 按鍵放開
XKeyEvent定義如下與XButtonEvent只有一個欄位不同:keycode,代表實體按鍵。X Window還有一種鍵盤碼叫keysym,代表打出的字元,一個keycode可以對應(yīng)到多個keysym,例如臺灣的鍵盤1和!、小寫和大寫字母是相同keycode不同keysym。
typedef struct {
int type; /* KeyPress or KeyRelease */
unsigned long serial; /* # of last request processed by server */
Bool send_event; /* true if this came from a SendEvent request */
Display *display; /* Display the event was read from */
Window window; /* "event" window it is reported relative to */
Window root; /* root window that the event occurred on */
Window subwindow; /* child window */
Time time; /* milliseconds */
int x, y; /* pointer x, y coordinates in event window */
int x_root, y_root; /* coordinates relative to root */
unsigned int state; /* key or button mask */
unsigned int keycode; /* detail */
Bool same_screen; /* same screen flag */
} XKeyEvent;
getKeysym()裡用XkbKeycodeToKeysym()把keycode轉(zhuǎn)換成keysym,如果有按shift第四參數(shù)要填1。
上面說的網(wǎng)站這一頁有介紹keycode與keysym
12.7 Keyboard Encoding
XkbKeycodeToKeysym()函式說明
https://linux.die.net/ : xkbkeycodetokeysym(3)
第三參數(shù)group我也不知道做什麼用的,這個參數(shù)填1的話無論keycode傳入什麼都傳回0,也許臺灣的鍵盤沒有這個功能,其他語言的鍵盤才有。
keysym有一些定義好的常數(shù)如XK_A=0x41,XK_space=0x20,在這個檔案:
/usr/include/X11/keysymdef.h
keycode定義在這個檔案,將裡面「KEY_」開頭的常數(shù)+8就是X Window的keycode。
/usr/include/linux/input-event-codes.h
其實還有一個函式叫XKeycodeToKeysym(),但compiler會跳出這個函式是deprecated的訊息,要改用XkbKeycodeToKeysym()。
用這個指令build。
gcc key_mouse.c -o key_mouse -Os -s -lX11 |
執(zhí)行時按按看各個滑鼠和鍵盤按鍵
如果視窗不是作用中(用滑鼠點一下視窗外面,讓標題變成灰色),則程式不會收到鍵盤和滑鼠事件。
圖中一部分訊息的意義
key release 36 keysym:ff0d state:0 | 放開enter。在命令列打指令按enter執(zhí)行,程式起動後放開enter,程式就收到放開enter的訊息。 |
key press 24 keysym: 71 state:0 key release 24 keysym: 71 state:0 |
按下與放開Q |
button press 1 cursor:(67,47) state:0 button release 1 cursor:(67,47) state:256 |
滑鼠左按鈕 |
button press 3 cursor:(134,120) state:0 button release 3 cursor:(134,120) state:1024 |
滑鼠右按鈕,除了button以外state也會變化 |
button press 4 cursor:(134,120) state:0 button release 4 cursor:(134,120) state:2048 |
滾輪往上 |
button press 5 cursor:(134,120) state:0 button release 5 cursor:(134,120) state:4096 |
滾輪往下 |
key press 50 keysym:ffe1 state:0 | 按下左shift |
key press 24 keysym: 51 state:1 key release 24 keysym: 51 state:1 |
按下與放開Q,keysym與state跟上面不一樣。 |
key release 50 keysym:ffe1 state:1 | 放開左shift。 |
key release 107 keysym:ff61 state:0 | 放開printscreen。按下printscreen會被作業(yè)系統(tǒng)特殊處理所以程式接收不到。 |
鍵盤、滑鼠、還有之後要介紹的手把輸入,有很多細節(jié)官方文件也沒寫,是筆者自己寫程式、把輸入裝置用所有方法操作一遍、用printf()印出內(nèi)部數(shù)值,才知道規(guī)則,例如以下幾點:
keysymdef.h裡面常數(shù)相當多,各種語言的鍵盤都有,筆者有寫個程式試過自己的鍵盤上有哪些keycode和keysym。
鍵盤碼與按鈕名稱對應(yīng)表
本程式不能處理CapsLock鍵,無論CapsLock燈有沒有亮,按鍵都傳回相同的keycode,想支援CapsLock要自己根據(jù)evt.xkey.state做判斷;也不能處理輸入法,切換輸入法之後程式仍然收不到中文字碼。X Window要處理這些情況有些複雜,本篇不介紹,一般遊戲程式也只需要知道實體按鍵而不需要用輸入法打中日韓文。如果使用GTK或Qt之類的GUI函式庫,它內(nèi)部會處理好輸入法。
在一般軟體的文字輸入框按住一個鍵不放,會重覆出現(xiàn)同一個字元,X Windows底層的事件會先收到一個press訊息,然後每次收到一組release和press訊息,處理方式和Windows API不一樣。
有兩個函式可以設(shè)定自動重覆:XAutoRepeatOn()和XAutoRepeatOff(),但電腦上的其他程式也會受影響。
據(jù)我所知,Linux讀取鍵盤和滑鼠輸入有以下幾種方法:
- X Window訊息,本篇介紹的方法。
- 用以下的函式。X Window訊息的方法是輸入裝置有變化的時候才傳訊息給你寫的程式,這些函式是一次取得全部按鍵狀態(tài)和游標位置。
XQueryKeymap()
XQueryPointer()
前一篇說過X server和X client是兩個分開的程式,這些函式實際不是直接讀取硬體狀態(tài),而是X server把事件傳過來後X client將狀態(tài)暫存,這些函式讀取X client暫存的狀態(tài)。 - 使用X Input extension。可以讀取多個鍵盤或指標裝置,也能支援鍵盤和滑鼠以外的裝置,但做法比較麻煩。
https://www.x.org/releases/X11R7.7/doc/libXi/inputlib.html
話說Windows和X Window都有一個函式庫叫XInput,Windows的是DirectX的一部分,用來讀取XBox系列手把輸入,X Window的是本項的X Input extension。 - 以上三個都是X Window,這個是比較直接存取硬體:用檔案操作函式開啟/dev/input/裡的檔案並讀取內(nèi)容,可以用「檔案操作—Linux篇」的open()和read()。但需要分辨裡面一堆event*哪個才是鍵盤,而且可能需要root權(quán)限。