Tutorial 6: Keyboard Input

このチュートリアルでは、Windowsがどのようにキーボードの入力を認識しているかを説明する。
   ソース      実行結果  

Theory:

普通、パソコンには1つのキーボードしかないので、 全てのWindows上で起動するプログラムは、その1つのキーボードをみんなで共有することになる。 なので、Windowsは今フォーカスされているウィンドウだけに、 キーボードの入力情報を送信するようになっている。

つまり、画面上にウィンドウが複数あっても、 アクティブなウィンドウだけにキーボード入力情報が送られ、 そのウィンドウがそのキーストロークを受け取る。 キーストロークを受け取るウィンドウかそうでないかは、 タイトルバーを見ればわかるようになっている。 キーストロークを受け取るウィンドのタイトルバーはハイライトされている。

キーボードメッセージには2つの主なメッセージタイプがあり、 これはキーボードの考え方による。 まずは、キーボードをキーの集合として考えるという考え方がある。 この場合、なにかキーを押すと、 WindowsはフォーカスされたウィンドウにWM_KEYDOWNメッセージを送信し、 そのウィンドウにキーが押されたことを認識させる。 そして、キーを離すと、今度はWindowsからWM_KEYUPメッセージが送信される。 よって、キーをボタンとして扱うことになる。 次に、キーボードを固有の入力装置とする考え方があり、 この場合は"a" キーを押したら、 WindowsはWM_CHARメッセージをウィンドウに送信するとともに、 "a" という文字を伝えることになる。

実際のところは、WindowsはWM_KEYDOWNとWM_KEYUPの2つのメッセージを送信していて、 それらのメッセージをTarnslateMessage関数で変換して、WM_CHARメッセージとして送信している。 ウィンドウプロシージャはこれら3つを全て処理するようにするかもしれないし、 1つだけを対象にするかもしれないが、ほとんどの場合、 WM_KEYDOWN と WM_KEYUP メッセージは無視していい。 というのは、メッセージループ中でTranslateMessage関数を呼ぶことにより、 WM_KEYDOWNとWM_KEYUPメッセージをWM_CHARメッセージに変換すればよいからだ。 なので、このチュートリアルでは、WM_CHARメッセージに焦点を当てることにする。

Example:

.386
.model flat,stdcall
option casemap:none

WinMain proto :DWORD,:DWORD,:DWORD,:DWORD

include \masm32\include\windows.inc
include \masm32\include\user32.inc
include \masm32\include\kernel32.inc
include \masm32\include\gdi32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib
includelib \masm32\lib\gdi32.lib

.data
ClassName db "SimpleWinClass",0
AppName db "Our First Window",0
char WPARAM 20h                        ; the character the program receives from keyboard

.data?
hInstance HINSTANCE ?
CommandLine LPSTR ?

.code
start:
   invoke GetModuleHandle, NULL
   mov   hInstance,eax
   invoke GetCommandLine
    mov CommandLine,eax
   invoke WinMain, hInstance,NULL,CommandLine, SW_SHOWDEFAULT
   invoke ExitProcess,eax

WinMain proc hInst:HINSTANCE,hPrevInst:HINSTANCE,CmdLine:LPSTR,CmdShow:DWORD
   LOCAL wc:WNDCLASSEX
   LOCAL msg:MSG
   LOCAL hwnd:HWND
   mov  wc.cbSize,SIZEOF WNDCLASSEX
   mov  wc.style, CS_HREDRAW or CS_VREDRAW
   mov  wc.lpfnWndProc, OFFSET WndProc
   mov  wc.cbClsExtra,NULL
   mov  wc.cbWndExtra,NULL
   push hInst
   pop  wc.hInstance
   mov  wc.hbrBackground,COLOR_WINDOW+1
   mov  wc.lpszMenuName,NULL
   mov  wc.lpszClassName,OFFSET ClassName
   invoke LoadIcon,NULL,IDI_APPLICATION
   mov  wc.hIcon,eax
   mov  wc.hIconSm,eax
   invoke LoadCursor,NULL,IDC_ARROW
   mov  wc.hCursor,eax
   invoke RegisterClassEx, addr wc
   invoke CreateWindowEx,NULL,ADDR ClassName,ADDR AppName,\
          WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,\
          CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,\
          hInst,NULL
   mov  hwnd,eax
   invoke ShowWindow, hwnd,SW_SHOWNORMAL
   invoke UpdateWindow, hwnd
   .WHILE TRUE
               invoke GetMessage, ADDR msg,NULL,0,0
               .BREAK .IF (!eax)
               invoke TranslateMessage, ADDR msg
               invoke DispatchMessage, ADDR msg
       .ENDW
   mov    eax,msg.wParam
   ret
WinMain endp

WndProc proc hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
   LOCAL hdc:HDC
   LOCAL ps:PAINTSTRUCT

   .IF uMsg==WM_DESTROY
       invoke PostQuitMessage,NULL
   .ELSEIF uMsg==WM_CHAR
       push wParam
       pop char
       invoke InvalidateRect, hWnd,NULL,TRUE
   .ELSEIF uMsg==WM_PAINT
       invoke BeginPaint,hWnd, ADDR ps
       mov   hdc,eax
       invoke TextOut,hdc,0,0,ADDR char,1
       invoke EndPaint,hWnd, ADDR ps
   .ELSE
       invoke DefWindowProc,hWnd,uMsg,wParam,lParam
       ret
   .ENDIF
   xor   eax,eax
   ret
WndProc endp
end start

Analysis:

char WPARAM 20h  ; the character the program receives from keyboard

この変数は、キーボードから受け取った文字コードを保持しておくものだ。 その文字はウィンドウプロシージャのWPARAM変数に入れられてくるので、 単純にWPARAM型として変数を定義している。 どうして初期値を 20h つまりスペースの文字コードとしているのかというと、 ウィンドウを1番最初に表示した時には、キーボードから何もインプットされていないので、 代わりにスペースを表示しているのである。

.ELSEIF uMsg==WM_CHAR
    push   wParam
    pop    char
    invoke InvalidateRect, hWnd,NULL,TRUE

ウィンドウプロシージャで WM_CHAR メッセージを扱うようにしている。 この例では、char という変数名にインプットされた文字を格納し、 その後、InvalidateRect 関数を呼び出している。 InvalidateRect 関数は指定したウィンドウのクライアント領域の矩形領域を 強制的に不正矩形としてWindowsに認識させ、 そのウィンドウのウィンドウプロシージャにWM_PAINTメッセージを送信させる関数だ。

この関数のプロトタイプは以下のとおりだ

InvalidateRect proto hWnd   :HWND,\
                     lpRect :DWORD,\
                     bErase :DWORD

lpRect はクライアントエリアにおける矩形領域を指定する矩形構造体へのポインタで、 その領域が不正矩形領域となる。 この変数が NULL なら、クライアントエリア全体が不正矩形領域となる。

bErase はWindowsに背景を消すかどうかを指示するフラグだ。 このパラメータが TRUE なら BeginPaint 関数が呼ばれた時に、 不正矩形領域の背景をWindowsが消去することになる。

結局、クライアントエリアの描画に関する全ての必要な情報を設定し、 クライアントエリアにWM_PAINTメッセージを発生させることになる。 もちろん、ウィンドウプロシージャのWM_PAINTセクションでは、 メッセージをどのように処理するかを事前に知っておかなければならない。 これは回りくどいやり方のように見えるが、Windowsにおけるルールなので仕方が無い。

GetDC関数とReleaseDC関数の間でWM_CHARメッセージを処理している間に、クライアントエリアに描画することが可能だ。 そこには何の問題もない。 ただ、クライアントエリアの再描画する必要が出てきた時に、興味深いことが起こるのである。 文字を描画する処理はWM_CHARセクションなので、 ウィンドウプロシージャではクライアントエリアの文字を再描画することができないだろう。 そのため、(WM_CHARセクションの)最下行(invoke InvalidateRect, hWnd,NULL,TRUE)に、 WM_PAINTメッセージで描画するために必要なデータとコードを書いておくのである。 このような方法を用いれば、あなとのコードのどこからでもWM_PAINTメッセージを発生させて、 いつでもクライアントエリアを再描画することができる。

invoke TextOut,hdc,0,0,ADDR char,1

InvalidateRect関数が呼ばれたとき、 ウィンドウプロシージャにWM_PAINTメッセージが送り返されてくる。 そのため、WM_PAINTメッセージを扱う部分をWM_PAINTセクションというのである。 そこでは、いつもと同じように、BeginPaint関数を呼び出して、 デバコンのハンドルを取得し、TextOut関数でクライアントエリアにおける x=0 y=0 の座標値から 文字を描画するのである。 プログラムを実行し、何かキーを押すと、 クライアントエリアの左上隅に押したキーが表示される。 そして、ウィンドウをいったん最小化してから最大化すると、 文字はまだ描画されたままとなっているのだが、 裏では、WM_PAINTメッセージが送信され、再描画が行われているのである。


[戻る]