暫時離開一下Direct3D和OpenGL,這次介紹作業系統讀取鍵盤和滑鼠輸入的方法,因為要滿足科技需求才能進入下一篇D3D和OpenGL教學。
如果用Windows內建的視窗元件,其實不需要自己處理鍵盤和滑鼠輸入,用滑鼠按按鈕、在文字框裡打字都有專用的事件可以處理,但是寫遊戲程式時,建好頂層視窗後繪圖、處理輸入都要自己做,就要用本篇的方法。
手把輸入則要用另外的方法,以後另寫一篇介紹。
與滑鼠類似的指標裝置,如觸控板、觸控螢幕、繪圖板也可以用滑鼠輸入處理,但不能讀取感壓和多點觸控;如果電腦接兩個鍵盤或指標裝置,本篇的方法也不能分辨是哪個裝置輸入。
以「如何建一個視窗—Windows API篇」為基礎,加上一些東西。
key_mouse.c
主要是在WndProc()裡增加一些東西。人操作鍵盤和滑鼠的時候,鍵盤和滑鼠的資訊會先送到作業系統,作業系統根據目前狀態,如游標位置、目前作用中視窗判斷由哪一個視窗處理,再把事件送到該視窗的message queue,你寫的程式在WndProc裡處理事件。(之前有說過,視窗不只是top level window,所有GUI元件都是HWND型態)
本篇只用printf()印出一行字,如果要做其他事就請自己做變化。
Windows API裡所有事件的額外資料都是wparam和lparam這兩個,32位元程式裡它們是4 byte整數,64位程式裡是8 byte整數,具體意義依事件種類而異。如果事件的額外資料多於兩個,可能會把數個資料併入一個整數,也有些事件的wparam和lparam是指標指到一個struct(本篇不會用到這種)。
一、滑鼠輸入
二、鍵盤輸入,實體按鍵
三、鍵盤輸入,打出的字元
用這個指令build。
執行時按按看各個滑鼠和鍵盤按鍵
如果視窗不是作用中(用滑鼠點一下視窗外面,讓標題變成灰色),則程式不會收到鍵盤和滑鼠事件。
上面說到實體按鍵和字元是不同的事件,試試幾個例子
範例1:按下Q鍵。CapsLock燈沒亮的時候printf()輸出如下
(key code和字碼都是16進位)
CapsLock燈亮的時候如下
key code同樣是0x51,等於ascii code的大寫Q,但char code不一樣。
範例2:按1
按住Shift鍵再按1
key code相同但char code不一樣。
範例3:輸入法是倉頡的時候,按下「H、A、P、I、空白」(字根:竹日心戈),會印出以下訊息
按5個鍵畫面上才出現一個字元。
上面說過開啟中文輸入法的時候wparam都是0xe5,這裡可以看出來。
鍵盤、滑鼠、還有之後要介紹的手把輸入,有很多細節官方文件也沒寫,是筆者自己寫程式、把輸入裝置用所有方法操作一遍、用printf()印出內部數值,才試出來。
像是getVK()裡面為什麼要把lparam跟0xe100做xor運算,是寫一個程式把鍵盤上所有按鍵都試一下才弄清楚規則。
鍵盤碼與按鈕名稱對應表
據我所知,Windows讀取鍵盤和滑鼠輸入有以下幾種方法(可能還有其他我不知道的方法):
如果用Windows內建的視窗元件,其實不需要自己處理鍵盤和滑鼠輸入,用滑鼠按按鈕、在文字框裡打字都有專用的事件可以處理,但是寫遊戲程式時,建好頂層視窗後繪圖、處理輸入都要自己做,就要用本篇的方法。
手把輸入則要用另外的方法,以後另寫一篇介紹。
與滑鼠類似的指標裝置,如觸控板、觸控螢幕、繪圖板也可以用滑鼠輸入處理,但不能讀取感壓和多點觸控;如果電腦接兩個鍵盤或指標裝置,本篇的方法也不能分辨是哪個裝置輸入。
以「如何建一個視窗—Windows API篇」為基礎,加上一些東西。
key_mouse.c
#define UNICODE #include<windows.h> #include<windowsx.h> //使用GET_X_LPARAM()和GET_Y_LPARAM() #include<stdio.h> static int getVK(int lparam){ lparam=(lparam>>16)&0x1ff; //取出第17~25 bits,包含scan code和extended key bit if(lparam & 0x100){ //檢查左右各有一個的按鍵,如Ctrl和Alt lparam^=0xe100; //XOR運算,0x138->0xe038 } int vkCode = MapVirtualKey(lparam, MAPVK_VSC_TO_VK_EX); return vkCode; } LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam){ switch(message){ case WM_DESTROY: PostQuitMessage(0); break; //滑鼠輸入 case WM_LBUTTONDOWN: printf("left button down cursor:(%d,%d)\n", GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam)); break; case WM_LBUTTONUP: printf("left button up cursor:(%d,%d)\n", GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam)); break; case WM_RBUTTONDOWN: printf("right button down cursor:(%d,%d)\n", GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam)); break; case WM_RBUTTONUP: printf("right button up cursor:(%d,%d)\n", GET_X_LPARAM(lparam), GET_Y_LPARAM(lparam)); break; case WM_MOUSEWHEEL: printf("mouse wheel: %d\n", GET_Y_LPARAM(wparam)/WHEEL_DELTA); break; //鍵盤輸入,實體按鍵 case WM_KEYDOWN: printf("keydown wparam:%4x lparam:%8x virtual-key code:%4x\n", wparam, lparam, getVK(lparam)); break; case WM_KEYUP: printf("keyup wparam:%4x lparam:%8x virtual-key code:%4x\n", wparam, lparam, getVK(lparam)); break; case WM_SYSKEYDOWN: printf("syskeydown wparam:%4x lparam:%8x virtual-key code:%4x\n", wparam, lparam, getVK(lparam)); break; case WM_SYSKEYUP: printf("syskeyup wparam:%4x lparam:%8x virtual-key code:%4x\n", wparam, lparam, getVK(lparam)); break; //鍵盤輸入,打出的字元 case WM_CHAR: printf("char char code:%x lparam:%x\n",wparam,lparam); break; case WM_DEADCHAR: printf("deadchar char code:%x lparam:%x\n",wparam,lparam); break; default: return DefWindowProc(hwnd,message,wparam,lparam); } return 0; } int main(){ WNDCLASS wndclass; ZeroMemory(&wndclass, sizeof(WNDCLASS)); wndclass.style = CS_HREDRAW|CS_VREDRAW; wndclass.lpfnWndProc = WndProc; wndclass.hCursor = LoadCursor(NULL,IDC_ARROW); wndclass.hbrBackground = (HBRUSH)(COLOR_BTNFACE+1); wndclass.lpszClassName = L"window"; RegisterClass(&wndclass); RECT rect={0,0,200,200}; AdjustWindowRect(&rect,WS_CAPTION|WS_SYSMENU|WS_VISIBLE,0); HWND window=CreateWindow(L"window", L"title", WS_OVERLAPPED|WS_SYSMENU|WS_VISIBLE, CW_USEDEFAULT, CW_USEDEFAULT, rect.right-rect.left, rect.bottom-rect.top, NULL,NULL,NULL,NULL); MSG msg; int ret=0; for(;;){ ret=GetMessage(&msg,NULL,0,0); if(ret<=0) break; TranslateMessage(&msg); DispatchMessage(&msg); } return 0; } |
主要是在WndProc()裡增加一些東西。人操作鍵盤和滑鼠的時候,鍵盤和滑鼠的資訊會先送到作業系統,作業系統根據目前狀態,如游標位置、目前作用中視窗判斷由哪一個視窗處理,再把事件送到該視窗的message queue,你寫的程式在WndProc裡處理事件。(之前有說過,視窗不只是top level window,所有GUI元件都是HWND型態)
本篇只用printf()印出一行字,如果要做其他事就請自己做變化。
Windows API裡所有事件的額外資料都是wparam和lparam這兩個,32位元程式裡它們是4 byte整數,64位程式裡是8 byte整數,具體意義依事件種類而異。如果事件的額外資料多於兩個,可能會把數個資料併入一個整數,也有些事件的wparam和lparam是指標指到一個struct(本篇不會用到這種)。
一、滑鼠輸入
本篇處理的滑鼠事件如下,點選事件名稱會開啟MSDN的文件。由官方文件可以看出每個事件的額外資料都一樣:wparam是一組bit flag表示目前有哪些按鈕被按下;lparam是事件發生時游標在視窗工作區的位置,最低的兩個byte是X坐標,第3,4 byte是Y坐標,用GET_X_LPARAM()和GET_Y_LPARAM()這兩個macro可以取出坐標。
WM_LBUTTONDOWN 左鍵按下 WM_LBUTTONUP 左鍵放開 WM_RBUTTONDOWN 右鍵按下 WM_RBUTTONUP 右鍵放開 WM_MOUSEWHEEL 滾輪轉動
坐標有可能是負數,所以如果要把坐標指定給變數,要用short、int這些有號整數型態。
WM_MOUSEWHEEL另外還有一項資料:轉動的方向和格數,存在wparam的第3,4 byte,用GET_Y_LPARAM()取出再除以WHEEL_DELTA(=120)可以求出。
然後照文件寫的,只要你寫的程式有處理這些訊息,WndProc()要return 0,本程式用break跳出switch,跳到函式末尾的return 0。本篇沒處理的事件就傳給DefWindowProc()做預設的處理。
還有一個事件是WM_MOUSEMOVE,只要游標有移動就會觸發,為了避免printf()印出太多訊息,本篇不處理此事件。
全部能用的事件在這篇,除了本篇介紹的以外還有中鍵、按兩下等等的,有興趣自己試。
MSDN: Mouse Input Notifications
二、鍵盤輸入,實體按鍵
鍵盤事件有分實體按鍵和打出的字元。一個鍵可以打出兩種以上的字元,例如臺灣常用的美式鍵盤1和!是同一個鍵,而使用輸入法的時候按好幾個鍵畫面上才會出現一個字元,這些時候實體按鍵事件和字元事件不一樣,實際例子在本篇最後。
本篇處理的實體按鍵事件如下SYSKEYDOWN、SYSKEYUP包括F10、Alt、按住Alt再按另一個鍵,其他按鍵都是KEYDOWN、KEYUP。
WM_KEYDOWN 鍵按下 WM_KEYUP 鍵放開 WM_SYSKEYDOWN 系統鍵按下 WM_SYSKEYUP 系統鍵放開
wparam、lparam包含的資訊,首先當然有按鍵碼讓你知道是哪個鍵被按下或放開,此外還有一些額外資訊,詳細請參照官方文件。
按鍵碼有scan code和virtual-key code兩種,文件裡寫scan code在不同鍵盤可能會不一樣,應該用virtual-key code判斷是哪個鍵比較好。
MSDN: Virtual-Key Code一覽
文件裡寫wparam是virtual-key code,但是經過實測用wparam來分辨按鍵有以下問題:
1. 左右各有一個的鍵如Shift和Ctrl,無法分辨左右邊。
2. 開啟輸入法的時候不論按哪個鍵wparam都是0xe5,筆者試過中日韓輸入法有此現象,其他語言應該也會。
要像本篇getVK()裡的方法,取出scan code再用MapVirtualKey()取得virtual-key code。
這是鍵盤事件一覽
MSDN: Keyboard Input Notifications
三、鍵盤輸入,打出的字元
首先DispatchMessage()前面有一行「TranslateMessage(&msg);」,它判斷實體按鍵事件會打出什麼字元,如果有打出字就產生WM_CHAR事件放入message queue,沒有這行就不會有WM_CHAR事件。如果程式只需要知道實體按鍵而不需要用輸入法打字,可以把TranslateMessage()拿掉,然後WndProc裡不用處理WM_CHAR。
WM_CHAR 打出的字元 WM_DEADCHAR 死鍵(dead key)
在一般軟體的文字輸入框按住一個鍵不放,會重覆出現同一個字元,底層的事件處理也一樣,按住一個鍵不放會產生一連串WM_KEYDOWN和WM_CHAR事件,放開時才產生WM_KEYUP事件。
X Window自動重覆的事件和Windows不一樣,詳細見X Window篇教學。
至於WM_DEADCHAR,臺灣的鍵盤沒這種鍵,有些歐洲語言的鍵盤有,用來打出有變音符號的字母如ê、?、é、è。
例如法語的AZERTY鍵盤。 Wikipedia: AZERTY
用這個指令build。
cl key_mouse.c /Fekey_mouse.exe /O2 /MD /link user32.lib |
執行時按按看各個滑鼠和鍵盤按鍵
如果視窗不是作用中(用滑鼠點一下視窗外面,讓標題變成灰色),則程式不會收到鍵盤和滑鼠事件。
上面說到實體按鍵和字元是不同的事件,試試幾個例子
範例1:按下Q鍵。CapsLock燈沒亮的時候printf()輸出如下
(key code和字碼都是16進位)
printf()輸出 | key code或char code的意義 |
keydown wparam: 51 lparam: 100001 virtual-key code: 51 | 51: Q |
char char code:71 lparam:100001 | 71: q |
keyup wparam: 51 lparam:c0100001 | 51: Q |
keydown wparam: 51 lparam: 100001 virtual-key code: 51 |
51: Q |
char char code:51 lparam:100001 |
51: Q |
keyup wparam: 51 lparam:c0100001 virtual-key code: 51 | 51: Q |
範例2:按1
keydown wparam: 31 lparam: 20001 virtual-key code: 31 | 31: 1 |
char char code:31 lparam:20001 | 31: 1 |
keyup wparam: 31 lparam:c0020001 virtual-key code: 31 | 31: 1 |
keydown wparam: 10 lparam: 2a0001 virtual-key code: a0 | a0: 左Shift |
keydown wparam: 31 lparam: 20001 virtual-key code: 31 | 31: 1 |
char char code:21 lparam:20001 | 21: ! |
keyup wparam: 31 lparam:c0020001 virtual-key code: 31 | 31: 1 |
keyup wparam: 10 lparam:c02a0001 virtual-key code: a0 | a0: 左Shift |
範例3:輸入法是倉頡的時候,按下「H、A、P、I、空白」(字根:竹日心戈),會印出以下訊息
keydown wparam: e5 lparam: 230001 virtual-key code: 48 keyup wparam: 48 lparam:c0230001 virtual-key code: 48 |
48: H |
keydown wparam: e5 lparam: 1e0001 virtual-key code: 41 keyup wparam: 41 lparam:c01e0001 virtual-key code: 41 |
41: A |
keydown wparam: e5 lparam: 190001 virtual-key code: 50 keyup wparam: 50 lparam:c0190001 virtual-key code: 50 |
50: P |
keydown wparam: e5 lparam: 170001 virtual-key code: 49 keyup wparam: 49 lparam:c0170001 virtual-key code: 49 |
49: I |
keydown wparam: e5 lparam: 390001 virtual-key code: 20 | 20: 空白鍵 |
char char code:7684 lparam:1 | 7684: 「的」的Unicode字碼 |
keyup wparam: 20 lparam:c0390001 virtual-key code: 20 | 20: 空白鍵 |
上面說過開啟中文輸入法的時候wparam都是0xe5,這裡可以看出來。
鍵盤、滑鼠、還有之後要介紹的手把輸入,有很多細節官方文件也沒寫,是筆者自己寫程式、把輸入裝置用所有方法操作一遍、用printf()印出內部數值,才試出來。
像是getVK()裡面為什麼要把lparam跟0xe100做xor運算,是寫一個程式把鍵盤上所有按鍵都試一下才弄清楚規則。
鍵盤碼與按鈕名稱對應表
據我所知,Windows讀取鍵盤和滑鼠輸入有以下幾種方法(可能還有其他我不知道的方法):
- 接收key和mouse訊息,本篇介紹的方法。
- 用以下的函式。window message的方法是輸入裝置有變化的時候系統才傳訊息給程式,這些函式是主動取得按鍵狀態和游標位置。
MSDN: GetKeyboardState()
MSDN: GetKeyState()
MSDN: GetAsyncKeyState()
MSDN: GetCursorPos()
照官方說明,GetKeyboardState()和GetKeyState()不是直接讀取硬體狀態,而是WndProc()處理事件後將狀態暫存在程式裡,這兩個函式讀取暫存的狀態。 - 接收raw input訊息。可以讀取多個鍵盤或指標裝置,也能支援鍵盤和滑鼠以外的裝置,但做法比較麻煩。
MSDN: Raw Input - DirectInput。類似2.主動讀取狀態,可以支援多個鍵盤或指標裝置。
早期可能因為電腦不夠快,以上3種方法會有延遲,所以特別設計一個比較直接存取系統的方法,但現在的電腦已經夠快,以上3種方法也沒有明顯延遲,而且比DirectInput簡便,現在已經不太有必要使用DirectInput。