목표
Win32 API를 활용해 키보드를 통한 입력을 처리하는 것에 대해 이해하도록 하겠습니다.
목차 클릭하면 해당 목차로 이동합니다.
개요
이전 포스팅에서, 화면에 텍스트를 출력하는 프로젝트를 진행하면서 TextOut, DrawText 함수에 대해서 다뤘습니다. 화면에 출력하기 위한 Device Context에 대한 개념과 GetDC, BeginPaint 함수의 차이점에 대해서 배웠었는데요. 이번 포스팅에서는, 키보드를 통한 입력에 대해서 배워볼 예정입니다.
Win32 API의 입력
Win32 API의 장점 중 하나는, 멀티 태스킹 환경을 지원한다는 것입니다. 하나의 프로그램이 입력을 받기 위해 대기한다고 해서, 시스템이 멈추면 안됩니다. 입력을 기다린다고 다른 것을 못하면 멀티 태스킹이 아니겠지요. 따라서, 입력이 들어오면 포커스(입력 초점)을 가진 프로그램에 키보드 메세지를 보내게 되고, 프로그램은 키보드 메세지를 받아 입력을 처리하게 됩니다. 다시 말하면, 포커스를 가진 컨트롤만 키보드 입력을 받을 수 있고, 이와 관련한 메세지도 포커스를 가진 컨트롤에게만 전달되는 것입니다.
정리하면, 포커스를 가진 컨트롤에게 전달되는 메세지를 처리해 입력을 다루는 것입니다. 그렇다면 포커스와 컨트롤이 뭘까요?
- 포커스: 입력 초점
- 컨트롤: 인터페이스를 통해 사용자로부터 명령과 입력을 받아들이고 출력 결과를 보여주는 과정에서 입출력 도구
입력 초점이라 함은, 우리가 메모장에 글을 쓸 때, 커서가 깜빡거리죠? 이렇게 다음 입력이 들어올 곳을 의미합니다.
입출력 도구라 함은, 저번 포스팅에서 출력했던 메세지 박스, 버튼, 등과 같이사용자와 상호작용하며 입출력을 해주는 것을 의미합니다.
보시는 것처럼, 2번 메모장을 클릭하여 포커스를 얻으면 커서가 깜빡거립니다. 키보드로 입력을 하면 2번 메모장에 작성됩니다. 이렇게 포커스를 갖고 있어야 사용자의 입력을 받을 수 있고, 2번 메모장은 포커스를 갖고 있기 때문에 사용자가 입력한 문자를 메모장에 작성(메세지를 처리)하는 것입니다.
내용을 수정하고 저장하지 않고 닫으려고 하면 위와 같은 메세지 박스가 출력됩니다. 사용자가 "저장" 버튼을 누른다면 수정된 내용을 저장하고 닫을 것이고, "저장 안 함"을 누르면 저장하지 않고 닫을 것입니다. 이렇게 사용자의 입력(버튼을 누름)을 받아서 프로그램에 전달하고, 메세지를 처리(내용을 저장)하는 것이 컨트롤입니다. 컨트롤은 다양한 것들이 있고, 추후에 자세히 다룰 예정입니다.
키보드 입력
WinMain 함수에서 메세지 루프를 돌고 있다가, 사용자가 키보드로 문자를 입력하면 발생한 메세지를 WndProc 함수로 전달하게 됩니다. WinMain 함수에 있는 메세지 루프를 보면 다음과 같습니다.
while(GetMessage(&Message, NULL, 0, 0)) {
TranslateMessage(&Message);
DispatchMessage(&Message);
}
문자를 입력할 때, 확인해야 할 함수는 TranslateMessage 함수입니다. 해당 함수는 메세지 루프에서 입력된 문자 정보를 알아내기 위해 필요합니다. 사용자가 키보드로 입력을 하면, WM_KEYDOWN 메세지가 발생합니다.(정확히는 키를 눌렀을 때 발생하는 것이고, 키를 떼면 WM_KEYUP이 발생합니다.) 이는, 키보드가 눌렸다는 메세지 일 뿐, 사용자가 어떤 버튼을 눌렀는지 알 수 없습니다. 이 때, TranslateMessage 함수를 통해 문자 정보를 확인해 WM_CHAR 메세지로 변환해서 DispatchMessage 함수를 통해서 윈도우 프로시저(WndProc)로 전달하게 되는 것입니다.
WndProc 함수의 선언문을 보면 다음과 같습니다.
LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam)
각 인수의 내용은 다음과 같습니다.
- HWND hWnd : 입력이 발생한 윈도우
- UINT iMessage : 전달되는 메세지(여기서는 WM_CHAR)
- WPARAM wParam : 입력 받은 문자 정보(어떤 키를 입력했는지?)
- LPARAM lParam : 부가 정보(키 반복 횟수, 이전 키 상태 등)
TranslateMessage 함수에서 WM_CHAR 메세지를 발생시키면서 wParam에 어떤 키를 눌렀는지에 대한 정보를 전달하는 것입니다. 이 메세지를 프로시저 내 switch문에서 처리하면 됩니다.
사용자가 입력한 문자 출력
위에서 배운 내용을 활용해서 사용자가 입력한 문자를 출력하는 프로그램을 작성해보도록 하겠습니다.
Key 프로젝트
사용자가 문자를 입력하면 화면에 출력하고, 스페이스 바를 입력하면 다 지워지는 프로그램입니다.
해당 코드는 다음과 같습니다.
LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage,
WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
static TCHAR str[256] = _T(""); // 입력한 문자열 저장하기 위한 문자열 배열
int len; // 배열에 문자를 넣기 위한 인덱스
switch (iMessage) {
case WM_CHAR:
if (wParam == 32) { //입력한 문자에 따른 메세지 처리
str[0] = 0;
}
else {
len = _tcslen(str);
str[len] = wParam;
str[len + 1] = 0;
}
InvalidateRect(hWnd, NULL, TRUE); // 화면에 출력하기 위한 함수
return 0;
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
TextOut(hdc, 50, 50, str, _tcslen(str));
EndPaint(hWnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}
변수 선언
사용자가 입력한 문자열을 저장하기 위한 문자열 배열 str을 선언했습니다.
배열에 문자를 넣기 위한 인덱스 len을 선언했습니다.
문자열을 static으로 선언한 이유?
문자열 str을 보면 static으로 선언한 것을 볼 수 있습니다. 왜 static으로 선언했을까요? 만약, static을 사용하지 않았다면, 메세지를 처리하고 나서 함수가 종료되면 해당 문자열은 지역 변수이기 때문에 메모리에서 없어지게 됩니다. 하지만, 우리는 스페이스바가 입력되기 전까지 입력한 문자를 화면에 출력해야 합니다. 따라서, static을 사용해 전역 변수의 성격도 띌 수 있도록 작성합니다. 이말인 즉슨, WndProc 함수에서만 접근을 할 수 있지만(지역함수), 함수가 종료되어도 메모리에서 사라지지 않습니다.(전역변수)
따라서, WndProc 함수가 종료되어도 입력된 문자를 저장한 채 계속해서 출력할 수 있는 것입니다.
WM_CHAR 메세지
WinMain에서 TranslateMessage 함수를 통해서 발생한 WM_CHAR 메세지가 들어오면, wParam에 저장된 문자 정보를 통해서 메세지 처리를 할 수 있습니다. 지금은 스페이스바가 들어오면 초기화하는 기능을 하기 때문에 if(wParam==32)라는 조건문을 통해서 메세지를 처리하는 모습입니다. (32는 스페이스바입니다.) 문자열 첫 번째 인덱스에 0을 넣으므로써, 문자열에 아무것도 없다고 눈속임하는 것입니다.
다른 문자가 들어오면 _tcslen() 함수를 통해서 문자열의 길이를 확인합니다. 해당 함수는 0번째 인덱스부터 NULL 값까지의 길이를 확인하기 때문에 첫 번째 인덱스에 0을 넣으면 아무것도 없다고 눈속임하는 것이 가능한 것입니다. 결과적으로, 해당 인덱스에 문자를 넣고 그 다음 인덱스에 0을 넣어서 방금 입력된 문자까지 출력하게 합니다. 여기서 중요한 것은, 문자 배열의 값만 변경했을 뿐 화면에 변화가 일어나지 않았다는 것입니다. 이는, 윈도우가 화면에 변화가 없기 때문에 WM_PAINT 메세지를 발생시키지 않습니다. 이 말은, 변경된 문자열을 화면에 출력하지 않는다는 것이지요. 따라서, 우리는 윈도우에 변화가 있다는 것을 알릴 필요가 있습니다. 이 때, 사용하는 것이 InvalidateRect 함수입니다.
InvalidateRect 함수와 무효 영역(Invalid Region)
InvalidateRect 함수를 이해하기 위해 무효 영역이라는 개념을 알고 있어야 합니다. 무효 영역은 이전 포스팅에서도 다룬 내용입니다. 다음과 같이 2개의 메모장이 있다고 가정하겠습니다.
1번 메모장의 내용이 2번 메모장에 의해 일부 가려져 보이지 않습니다. 1번 메모장의 내용을 확인하기 위해 2번 메모장을 이동하면, 윈도우는 이를 판단하고 WM_PAINT 메세지를 발생시켜 다시 출력하게 됩니다. 이처럼, 다른 응용 프로그램에 의해 일부 가려져 있는 부분을 무효 영역이라고 합니다. 위와 같이 WM_CHAR 메세지를 처리하는 것처럼 내부적인 문자 입력은 화면에 보여지는 것인지, 단순 내부적인 계산, 전송인지 운영체제가 판단할 수 없습니다. 이 때, InvalidateRect 혹은 InvalidateRgn 함수를 사용해서 윈도우의 작업 영역을 무효화하여 운영체제로 하여금 WM_PAINT 메세지를 해당 윈도우로 보내도록 하는 것입니다. InvalidateRect 함수의 원형은 다음과 같습니다.
BOOL InvalidateRect(HWND hWnd, CONST RECT RECT *lpRect, BOOL bErase);
InvalidateRect 함수의 인수는 다음과 같습니다.
- HWND hWnd: 무효화 시킬 윈도우(윈도우의 핸들)
- CONST RECT *lpRect: 무효화할 영역 지정(NULL이면 전체 영역 무효화)
- BOOL bErase: TRUE이면 지우고 다시 그리고, FALSE이면 지우지 않고 덮어쓴다.
2번째 인수를 활용해서 일부 영역만 다시 그릴 수 있고, 3번째 인수를 활용해서 아예 지우고 다시 그리거나, 기존에 있는 것에 덮어씌울 수도 있습니다. 위 Key 프로젝트의 코드를 변경해보면 이해가 빠를 것입니다.
InvalidateRect 함수를 통해서 화면의 변화가 있다고 운영체제에게 알린 뒤, WM_PAINT 메세지가 발생해 화면에 문자열을 출력하는 것입니다.
결과
입력한 문자열을 화면에 출력하고, 스페이스 바를 누를 경우 문자열이 초기화되는 것을 볼 수 있습니다.
하지만, 해당 프로젝트는 백스페이스를 눌러도 지워지지 않고, 띄어쓰기 또한 불가능합니다. 이를 어떻게 해결할지 생각해볼 필요가 있습니다.
방향키를 통해서 이동하기
대부분의 게임은 화살표 방향키를 통해서 상,하,좌,우로 이동하곤 합니다. 이번 프로젝트는 화면에 네모를 출력해 방향키를 통해서 이동시키는 것을 진행해보도록 하겠습니다.
KeyDown 프로젝트
위에서 진행한 Key 프로젝트를 보면, 스페이스바는 32로 키 코드(Key Code)가 정해져있었습니다. 세상에 수많은 키보드 제작회사가 있는데, 각 제조업체에 따라 코드를 다르게 한다면 개발자의 입장에서 아주 번거로워질 것입니다. 이를 위해 가상 키 코드(Virtual Key Code)라는 범용적인 코드값을 정해놨습니다. 해당 코드값이 wParam에 전달되는 것입니다.
가상키 코드 | 값 | 키 |
VK_BACK | 08 | Backspace |
VK_TAB | 09 | Tab |
VK_RETURN | 0D | Enter |
VK_SHIFT | 10 | Shift |
VK_CONTROL | 11 | Ctrl |
VK_MENU | 12 | Alt |
VK_PAUSE | 13 | Pause |
VK_CAPITAL | 14 | Caps Lock |
VK_ESCAPE | 1B | Esc |
VK_SPACE | 20 | Space |
VK_LEFT | 25 | 좌측 이동키 |
위와 같은 가상키 코드를 활용해 메세지를 처리할 수 있습니다. 메세지 처리 코드는 다음과 같습니다.
LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage,
WPARAM wParam, LPARAM lParam)
{
HDC hdc;
PAINTSTRUCT ps;
static int x = 100; // x좌표
static int y = 100; // y좌표
switch (iMessage) {
case WM_KEYDOWN:
switch (wParam) { // 가상키 처리
case VK_LEFT:
x -= 8;
break;
case VK_RIGHT:
x += 8;
break;
case VK_UP:
y -= 8;
break;
case VK_DOWN:
y += 8;
break;
}
InvalidateRect(hWnd, NULL, TRUE); // 출력을 위한 무효영역 처리
return 0;
case WM_PAINT:
hdc = BeginPaint(hWnd, &ps);
TextOut(hdc, x, y, _T("■"), 1); // 출력
EndPaint(hWnd, &ps);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}
위에서 문자열을 static으로 선언한 것과 같이, (x,y)좌표도 static으로 선언한 것을 볼 수 있습니다.
그리고 가상키 코드를 활용해서 좌표를 이동하면서 메세지를 출력합니다. 만약, InvalidateRect 함수의 세번째 인수를 FALSE로 선언했다면, 사각형이 이동해도 이전 위치에 그대로 출력해 있을 것입니다.
결과
해당 결과를 사진으로 잘 보이기 위해, InvalidateRect(hWnd, NULL, FALSE)로 수정하여 실행한 결과입니다.
위 코드와 같이 작성했다면 사각형이 이동하면서 잔상을 남기지 않을 것입니다.
정리
이번 포스팅에서는 키보드 입력에 따른 메세지 처리에 대해 다뤘습니다. 다음 포스팅에서는 마우스를 통한 입력에 대해서 다루도록 하겠습니다.
'개발 > Win32 API Programming' 카테고리의 다른 글
[Windows API] Win32 API의 타이머를 활용해 시계 만들기 - (1) (0) | 2022.01.11 |
---|---|
[Windows API] Win32를 활용해 마우스 입력하기 (0) | 2022.01.09 |
[Windows API] Win32 API를 활용해 긴 텍스트, 도형, 메세지박스 출력하기 (0) | 2022.01.02 |
[Windows API] Device Context란?, Win32 API를 활용해 문자열 출력하기 (0) | 2021.12.31 |
[Windows API] Win32 API의 기본구조, 윈도우 프로시저 (0) | 2021.12.26 |