튜토리얼 6: Keyboard Input



이번 시간에는 Windows 프로그램에서 키보드 입력을 받는 방법에 대해 알아 보도록 하겠습니다.

 
Theory:

보통 PC 하나에 키보드가 하나이기 때문에, 실행중인 Windows 프로그램은 서로 키보드를 공유해야만 합니다. 현재 입력 포커스를 가지고 있는 윈도우에게 키 입력을 전달할 임무는 Windows 가 가지고 있습니다.

비록 화면에 여러개의 윈도우가 떠 있더라도, 그 중 단 하나만이 입력 포커스를 가지고 있습니다. 입력 포커스를 가지고 있는 이 한 개의 윈도우만이 키보드 입력을 받을 수 있습니다. 이 윈도우는 타이틀 바를 보면 알 수 있습니다. 입력 포커스를 가지고 있는 윈도우의 타이틀바는 하이라이팅 되어 있기 때문이죠.


실제로 키보드 메세지에는 두가지가 있습니다. 이것은 키보드를 보는 관점에 따라 달라집니다. 하나는 키보드를 키의 집합으로 보는 것입니다. 이 경우 키를 눌렀을 때, Windows 는 WM_KEYDOWN 메세지를 입력 포커스를 가지고 있는 윈도우에게 보내서 키가 눌러졌음을 알립니다. 키를 땐 경우에는, Windows 는 WM_KEYUP 메세지를 보냅니다. 키를 버튼이라고 생각하는 것이죠. 다른 관점에서 보면 키보드는 문자를 입력하는 장치입니다. "a" 를 눌렀을 때, Windows 는 WM_CHAR 메세지를 입력 포커스를 가지고 있는 윈도우에게 전달함으로써 사용자가 "a" 문자를 입력했음을 알려줍니다. 사실 Windows 는 WM_KEYDOWN 과 WM_KEYUP 메세지를 입력 포커스를 가지고 있는 윈도우에게 전달하고, 이 메세지들은 TranslateMessage 호출에 의해서 WM_CHAR 메세지로 변경될 것입니다. 윈도우 프로시져에 따라 이 세개의 메세지를 모두 사용 할수도 있고, 아니면 한개의 메세지만 사용할 수 있습니다. 대부분의 경우, 메세지 루프에서 TranslateMessage 함수를 호출하면 WM_KEYDOWN 과 WM_KEYUP 를 WM_CHAR 메세지로 변경해주기 때문에, WM_KEYDOWN 과 WM_KEYUP 을 신경쓰지 않아도 됩니다. 이번 시간에는 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(빈 문자 - 스페이스) 로 지정했습니다. 그래서 스페이스가 표시될 수 있도록 하였습니다.


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





위 코드는 WM_CHAR 메세지를 처리할 수 있도록 윈도우 프로시저에 추가되었습니다. 이것은 단지 "char" 라는 이름의 변수에 문자를 입력하고 InvalidateRect 함수를 호출한 것입니다. InvalidateRect 는 윈도우가 윈도우 프로시져에게 WM_PAINT 메세지를 보내도록 함으로써, 유효하지 않은 클라이언트 영역을 명시합니다.


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





lpRect
클라이언트 영역에서 유효하지 않은 범위를 나타내는 사각형을 가리키는 포인터 입니다. 만약 이 값이 null 이라면 클라이언트 영역 전체가 유효하지 않은 것으로 설정됩니다.

bErase
배경을 지워야 할지를 나타내는 플래그입니다. 만약 플래그 값을 TRUE 로 하면, Windows 는 BeginPaint 가 호출 되었을 때, 유효하지 않은 사각 영역의 배경을 지워버립니다.



여기서 우리의 의도는 다음과 같습니다. 클라이언트 영역을 그리는데 필요한 모든 정보를 저장합니다. 그리고 클라이언트 영역을 그리기 위해서 WM_PAINT  메세지를 생성합니다. 물론 WM_PAINT 섹션에서는 무엇을 해야 하는지 미리 알고 있어야 합니다. 빙 돌면서 일을 처리하는 것 같지만 이것이 Windows 방식입니다.
사실 GetDC 와 ReleaseDC 를 이용하면, WM_CHAR 메세지를 받았을 때 클라이언트 영역에 그리도록 할 수 있습니다. 그리고 문제가 될 것이 없습니다. 하지만 문제는 윈도우가 클라이언트 영역을 다시 그려야 할 때 발생합니다. 문자를 그리는 코드가 WM_CHAR 섹션에 있기 때문에, 윈도우 프로시져는 우리의 문자를 클라이언트 영역에 다시 그릴 수가 없습니다. 그래서 마지막 라인처럼 WM_PAINT 에 필요한 모든 데이터와 그리는 코드를 넣었습니다. 덕분에 우리는 클라이언트 영역을 다시 그리고 싶을 땐, 코드 어디에서든 언제든지 WM_PAINT 메세지만 보내면 됩니다.


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




InvalidateRect 함수가 호출되면, WM_PAINT 메세지가 윈도우 프로시져로 전달됩니다. 그리고 WM_PAINT 섹션의 코드가 호출되죠.  이것은 디바이스 컨텍스트 핸들을 얻기 위한 BeginPaint 와 클라이언트 영역 x=0, y=0 위치에 문자를 그리기 위한 TextOut 을 호출합니다. 프로그램을 실행하고 아무키나 눌러 보면, 윈도우의 좌측 상단 구석에 문자가 찍히는 것을 보실 수 있습니다. 그리고 그리는데 필요한 모든 데이터와 코드가 WM_PAINT 섹션 안에 들어 있기 때문에, 창을 최소화 했다가 다시 최대화 시켜도 여전히 문자가 그려져 있는 것을 확인 하실 수 있습니다. 


Epilogue:

이전에 말씀 드렸듯이, 이 강좌는 영문으로 된 원서를 번역하고 있는 것입니다용~ 오해 없으시기 바랍니다.
어셈블리가 처음에는 어렵지만 나중에 뒤로 갈수록 C/C++ 과 전혀 다를게 없다라는 생각이 마구마구 듭니다. 왜냐면 메크로 어셈블리기 때문이죠. ㅎㅎ 아무튼 드리고 싶은 말씀은... 그냥 넋두리입니다. 오늘은 2010 년 12월 31일! 마지막 날입니다. 내일이면 2011년 이네요. 한 해 동안 무엇을 하였나 다시한번 생각해 봅니다. 엇.. 저녁 시간입니다. 저녁먹고 오겠습니다. 여러분 2010년 한해 마무리 잘 하시고, 2011년 준비 잘 하시기 바랍니다~

그럼 전 이만~