前往
大廳
主題

【程式】讀取鍵盤與滑鼠輸入 (X Window)

Shark | 2022-03-02 19:01:31 | 巴幣 1028 | 人氣 718

X Window的GUI功能只有在畫面上開一個矩形區域,沒有提供按鈕、文字輸入框這些元件的繪製和事件處理,想用GUI元件必須用Motif、GTK、Qt等較上層的函式庫。但遊戲程式只要建一個頂層視窗,之後畫圖和處理輸入都自己來,用底層的X Window可以減少一些多餘的負擔,就要用本篇的方法處理輸入。

跟Windows篇一樣,觸控板、繪圖板等指標裝置也可以視為滑鼠輸入,但這個方法不能讀取感壓和多點觸控;如果電腦接兩個鍵盤或指標裝置,不能分辨是哪個裝置輸入;手把輸入要用另一種方法讀取,以後另寫一篇介紹。



以「如何建一個視窗—X Window篇」為基礎增加一些東西。

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

  //設定事件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);

  //設定標題
  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根據目前狀態,如游標位置、目前作用中視窗判斷由哪一個視窗處理事件,再把訊息傳給那個X client。
比前一篇增加的部分是,eventMask要根據想處理的事件填寫;以及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;
是個union,type、xkey、xbutton這些成員都佔用相同空間,先檢查evt.type的值再看要把這一塊資料解釋成哪個struct。

之前說過,寫X Window程式有個困難是說明文件很少,這個網站是我找到比較詳細的。
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

有時候只看這些文件還不夠,像如何切換全螢幕模式、暫停螢幕保護程式這些文件也沒有說明,筆者開發遊戲的時候參考了一些函式庫的程式碼(如SDL)才知道怎麼做。

一、滑鼠輸入
首先要查文件確認mask、event type和struct各是什麼,然後填寫mask,且switch{}裡面增加對應的處理。
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:游標在視窗工作區裡的坐標。
x_root、y_root:游標在root window裡的坐標,通常是指螢幕上的位置。
state:目前一些鍵盤和滑鼠按鈕是否被按下,是一組bit flag,有以下的常數。
  右行是在筆者的電腦上試的結果,Shift、Ctrl、Alt不分左右
常數名稱 按鍵
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,這是游標有移動就會觸發,為了避免printf()印出太多訊息,本篇不處理此事件,有興趣請自己試。

二、鍵盤輸入
本篇處理的事件如下
event mask evt.type struct
KeyPressMask KeyPress XKeyEvent 按鍵按下
KeyReleaseMask KeyRelease XKeyEvent 按鍵放開

XKeyEvent定義如下
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;
與XButtonEvent只有一個欄位不同:keycode,代表實體按鍵。X Window還有一種鍵盤碼叫keysym,代表打出的字元,一個keycode可以對應到多個keysym,例如台灣的鍵盤1和!、小寫和大寫字母是相同keycode不同keysym。
getKeysym()裡用XkbKeycodeToKeysym()把keycode轉換成keysym,如果有按shift第四參數要填1。
上面說的網站這一頁有介紹keycode與keysym
  12.7 Keyboard Encoding
XkbKeycodeToKeysym()函式說明
  https://linux.die.net/ : xkbkeycodetokeysym(3)
第三參數group我也不知道做什麼用的,這個參數填1的話無論keycode傳入什麼都傳回0,也許台灣的鍵盤沒有這個功能,其他語言的鍵盤才有。

keysym有一些定義好的常數如XK_A=0x41,XK_space=0x20,在這個檔案:
/usr/include/X11/keysymdef.h
keycode定義在這個檔案,將裡面「KEY_」開頭的常數+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
因為要用printf()印出訊息,請用命令列打指令執行而不是用滑鼠點。

執行時按按看各個滑鼠和鍵盤按鍵

如果視窗不是作用中(用滑鼠點一下視窗外面,讓標題變成灰色),則程式不會收到鍵盤和滑鼠事件。

圖中一部分訊息的意義
key release 36 keysym:ff0d state:0 放開enter。在命令列打指令按enter執行,程式起動後放開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會被作業系統特殊處理所以程式接收不到。

鍵盤、滑鼠、還有之後要介紹的手把輸入,有很多細節官方文件也沒寫,是筆者自己寫程式、把輸入裝置用所有方法操作一遍、用printf()印出內部數值,才知道規則,例如以下幾點:

keysymdef.h裡面常數相當多,各種語言的鍵盤都有,筆者有寫個程式試過自己的鍵盤上有哪些keycode和keysym。
鍵盤碼與按鈕名稱對應表

本程式不能處理CapsLock鍵,無論CapsLock燈有沒有亮,按鍵都傳回相同的keycode,想支援CapsLock要自己根據evt.xkey.state做判斷;也不能處理輸入法,切換輸入法之後程式仍然收不到中文字碼。X Window要處理這些情況有些複雜,本篇不介紹,一般遊戲程式也只需要知道實體按鍵而不需要用輸入法打中日韓文。如果使用GTK或Qt之類的GUI函式庫,它內部會處理好輸入法。

在一般軟體的文字輸入框按住一個鍵不放,會重覆出現同一個字元,X Windows底層的事件會先收到一個press訊息,然後每次收到一組release和press訊息,處理方式和Windows API不一樣。
有兩個函式可以設定自動重覆:XAutoRepeatOn()和XAutoRepeatOff(),但電腦上的其他程式也會受影響。



據我所知,Linux讀取鍵盤和滑鼠輸入有以下幾種方法:
  1. X Window訊息,本篇介紹的方法。
  2. 用以下的函式。X Window訊息的方法是輸入裝置有變化的時候才傳訊息給你寫的程式,這些函式是一次取得全部按鍵狀態和游標位置。
    XQueryKeymap()
    XQueryPointer()
    前一篇說過X server和X client是兩個分開的程式,這些函式實際不是直接讀取硬體狀態,而是X server把事件傳過來後X client將狀態暫存,這些函式讀取X client暫存的狀態。
  3. 使用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。
  4. 以上三個都是X Window,這個是比較直接存取硬體:用檔案操作函式開啟/dev/input/裡的檔案並讀取內容,可以用「檔案操作—Linux篇」的open()和read()。但需要分辨裡面一堆event*哪個才是鍵盤,而且可能需要root權限。

創作回應

=﹏=
我可以看做Linus、Windows和X Window是三個作業系統?
2022-03-03 03:40:40
Shark
簡單來說,Linux和Windows是作業系統。
X Window是應用程式,除了Linux以外也有其他作業系統的版本,只是很少人在Windows上面用X Window。
2022-03-07 18:05:40

相關創作

更多創作