기본적인 키보드 메시지
윈도우즈에서 한글을 고려하지 않고 키보드 입력을 다루는 방법은 정말 쉽다.
Windows는 키보드 입력이 들어올 때 마다 현재 입력 포커스를 가지고 있는 윈도우에게로 아래 메시지를 보내준다.
메시지 |
내용 |
WPARAM |
LPARAM |
WM_CHAR |
문자가 조합된 경우 |
조합된 글자
(TCHAR형으로 넘어옴) |
0~15: 반복 횟수
16~23: 스캔 코드
24: 확장 키가 눌렸는가?
29: alt키가 눌려졌는가
30: 이전에 눌려진 상태로 있었는가?
31: 아직 눌려진 상태로 있는가? |
WM_KEYDOWN |
키보드에서 키가 눌려짐 |
눌러진 virtual key |
동일 |
WM_KEYUP |
키보드에서 키가 눌려졌다 떼어짐 |
떼어진 virtual key |
동일 |
WM_SYSKEYDOWN |
Alt키가 눌려진 상태에서 키가 눌려짐 |
눌러진 virtual key |
동일, 29번 비트는 항상 1 |
WM_SYSKEYUP |
Alt키가 눌려진 상태에서 키가 떼어짐 |
떼어진 virtual key |
동일, 29번 비트는 항상 1 |
위의 표에서 대부분은 자명한 것이다. 이 중 몇 가지 사항만 짚고 넘어가면 된다.
가장 먼저 이야기 해야 할 것이 WM_CHAR 메세지 이다. 이 메시지는 메인 루프의 TranslateMessage(&msg)에 의해 생겨나는 것이다. 이 함수는 MSG msg 구조체 내의 message, wParam, lParam 멤버와 time멤버를 분석하여 WM_KEYDOWN과 WM_KEYUP으로부터 적당한 갯수의 WM_CHAR를 만들어 낸다. 이 때, 제어판에서 설정한 키 입력 반복 속도 정보가 사용된다.
다음으로 기억해야 할 것은 TCHAR형이다. TCHAR를 알기 전에 WCHAR를 알아야 한다. Win32 API는 유니코드를 지원한다. 2바이트 유니코드 문자 한개를 저장하는 자료형이 WCHAR이다. 다음은 각각 유니코드를 사용하지 않는 소스 코드와(이러한 방식의 한글 표현을 Double Bytes Character Set, DBCS라고 한다) 유니코드를 사용하는 소스 코드를 보여준다.
DBCS 방식 |
char a[] = "안녕하세요";
cout << strlen(a); /* 아마 10이 출력될 것이다 */ |
유니 코드 방식 |
WCHAR w[] = L"안녕하세요";
cout << wcslen(w); /* 아마 5가 출력될 것이다 */ |
만일 DBCS 방식으로 작성된 코드를 아시아권 국가에 배포하기 위해 유니코드 방식으로 바꾸려면 어떻게 하면 될까? 혹은 유니코드를 지원하는 소스 코드를 유니코드를 필요로 하지 않는 사용자들을 위해(대신 코드는 가벼워질 것이다) DBCS 방식으로 바꾸려면 어떻게 하면 될까? 아래와 같이 코딩하면 이를 간단히 할 수 있다.
#define _UNICODE /* 만일 유니코드를 사용하고 싶으면 이 문장을 추가하고 아니면 이 문장을 빼시오 */
TCHAR w[] = _T("안녕하세요");
cout << _tcslen(w);
만일 유니코드를 사용하고 싶지 않으면 #define _UNICODE를 지우면 된다. 이것은 의 구현이다. 내부적으로는 다음과 같이 구현되어 있다.
#ifdef _UNICODE
typedef WCHAR TCHAR;
#define _T(x) L##x
#define _tcslen wcslen
#else
typedef char TCHAR;
#define _T(x) x
#define _tcslen strlen
#endif
_T(x) 매크로를 정의할 때 ANSI C의 토큰 페이스트 ##가 사용되었다. 이 매크로는 "abc"를 L"abc"로 바꾸어야 하지만(떼어서 쓰면 안되고 반드시 붙여서 써야함), #define _T(x) Lx라고 할 수 도 없지 않은가? ##기호는 L과 x가 서로 다른 기호라는 것을 알려준다.
_T 매크로 대신 __TEXT, _TEXT, TEXT, __T를 사용하여도 동일한 결과를 얻을 수 있으며, 실제로는 _tcslen보다 Windows API 함수인 lstrlen, lstrcmp, lstrcpy 등이 훨씬 많이 사용된다.
정리하면 우리가 _UNICODE를 define 했는가 아닌가에 따라서 윈도우즈는 때때로 char형을, 때때로 WCHAR 형을 WM_CHAR 메세지의 wParam으로 넘겨줄 것이다.
마지막으로 virtual key에 대해 이야기해 보자. 각각의 키보드는 눌려지거나 떼어졌을 때 컴퓨터로 눌려지거나 떼어진 키 번호를 보낸다. 이 키 번호를 'scan code'라고 한다.^^;; 그러나 scan code는 키보드 종류에 따라 다르다. 따라서 윈도우즈는 응용 프로그램 제작자들의 편의를 위해 이 스캔 코드를 virtual key로 바꾸어준다.
이러한 서비스는 아주 편리하다. 분명히 모든 키보드에에서 현재 내 오른쪽 새끼손가락 근처에 있는 엔터키와 Numpad에 있는 엔터키의 스캔 코드는 다르다. 그러나 어느 경우나 WM_KEYDOWN 메시지에는 VK_RETURN으로 들어올 것이다.(그리고 WM_CHAR 메시지에는 '\r' 문자가 들어올 것이다)
키 코드 |
내용 |
실제 값 |
VK_CANCEL |
Ctrl+Break를 눌렀을때 |
03 |
VK_BACK |
Backspace 키 |
08 |
VK_TAB |
Tab키 |
09 |
VK_RETURN |
Enter 키 |
13 |
VK_SHIFT |
Shift키 |
16 |
VK_CONTROL |
Ctrl 키 |
17 |
VK_MENU |
Alt 키 |
18 |
VK_CAPITAL |
Caps Lock 키 |
20 |
VK_ESCAPE |
Esc 키 |
27 |
VK_SPACE |
Space 바 |
32 |
VK_PRIOR |
Page Up 키 |
33 |
VK_NEXT |
Page Down 키 |
34 |
VK_END |
End 키 |
35 |
VK_HOME |
Home 키 |
36 |
VK_LEFT |
좌측 화살표 |
37 |
VK_UP |
위쪽 화살표 |
38 |
VK_RIGHT |
우측 화살표 |
39 |
VK_DOWN |
아랫쪽 화살표 |
40 |
VK_INSERT |
Insert키 |
45 |
VK_DELETE |
Delete키 |
46 |
VK_F1 ~ VKF10 |
F1~F10 |
112-121 |
VK_NUMLOCK |
Num Lock |
144 |
VK_SCROLL |
Scroll Lock |
145 |
Windows에 운영체제의 성능에 대해서 못마땅해 하는 사람들은, 이러한 다대일 대응이 문제를 일으키기를 기대할 수 도 있다. 예를 들어 Numpad의 4, 6, 8, 2는 게임등에서 왼쪽, 오른쪽, 위, 아래를 나타내는데 사용될 수 있지만, 이것이 키보드의 숫자가 눌려졌을 때도 작동한다는 것은 비극적인 일이다. 그러나 설마 그 정도로 어정쩡 하겠는가? 전자의 경우 lParam의 24번째 비트(확장 키가 눌렸는가?)는 1이 되고, 후자의 경우는 0이 된다.
캐럿
캐럿과 관련되어 용어상 분명히 해 두어야 할 것이 있다. 캐럿은 커서가 아니다. 여러분이 워드 프로세서나 메모장 등을 띄웠을 때 여러분의 입력을 기다리는 깜박이는 것이 캐럿이다. 그렇다면 커서는 무엇이란 말인가? 여러분이 책상 위에서 마우스를 움직일 때 모니터 상에서 따라 움직이는 화살표가 바로 커서이다. WNDCLASS를 만들 때 LoadCursor(NULL, IDC_ARROW);를 했던 사실을 잊었는가?
Win32에서 캐럿을 다루는 함수는 다음과 같다.
- 캐럿을 생성: CreateCaret(HWND hwnd, HBITMAP hBitmap, int width, int height)
- 캐럿을 보임: ShowCaret(HWND hwnd)
- 캐럿의 이동: SetCaretPos(int x, int y)
- 캐럿을 숨김: HideCaret(HWND hwnd)
- 캐럿을 파괴: DestroyCaret()
응용 프로그램 범위에서 캐럿을 단 한 개만 생성할 수 있음에 주의하라. 오직 하나의 윈도우만이 캐럿을 가져야 한다. 가장 좋은 방법은 WM_SETFOCUS에서 캐럿을 생성하고, WM_KILLFOCUS에서 캐럿을 삭제 하는 것이다. 이 두 메시지는 반드시 쌍으로 불려지므로 메모리상을 둥둥 떠다니는 캐럿을 걱정할 필요는 없다.
또한 HideCaret을 10번 호출했으면, ShowCaret을 10번 호출해야 캐럿을 볼 수 있음을 주의하라. 캐럿은 암기력이 좋은 친구이다.
IME 메시지
한글 Windows를 비롯한 DBCS을 사용하는 국가의 윈도우즈에는 여러개의 키보드 입력이 한 개의 글자를 만드는 일을 관리해 주는 IMM(Input Method Manager)이 내장되어 있다. 키보드에서 문자가 입력되면 IMM이 여러분의 입력을 가로채서 한 글자가 완성되었을 때 비로소 WM_CHAR 메시지를 보내준다.
여러분의 프로그램이 IMM을 무시한채, 단지 WM_CAHR 만을 처리하더라도, 여러분의 프로그램은 대부분의 경우에 잘 작동할 것이다.(잠시후에 왜 잘 작동하는가를 알게 된다)그러나 한 글자가 완성되었을 때 비로소 화면에 나타나는 그런 프로그램이 아니라 한 개의 자모 단위로 입력되고 지워지는 과정을 전부 처리할 수 있는 프로그램을 작성하고 싶다면, IMM이 보내는 IME(Input Method Editor) 메시지들을 핸들링하라.
이 절에서는 IME 메시지들을 알아보고 다음 절에서 한글 타자 연습 프로그램을 만들 것이다. 만일 여러분이 프로그램에서 IMM을 다루고 싶다면, Project 메뉴의 Setting에서 imm32.lib를 링크에 추가하고 소스 코드에는 를 include 해야 한다.
다음은 기본적인 IME 메시지들이다.
메시지 |
내용 |
WM_IME_KEYDOWN |
쉬프트+스페이스나 한글키와 같은 IME 관련 키가 눌려졌을 때 IMM에의해 보내지는 메시지이다. 이 메시지 DefWndProc에 넘기면 그에 해당하는 WM_KEYDOWN이 불린다. |
WM_IME_KEYUP |
쉬프트+스페이스나 한글키와 같은 IME 관련 키가 떼어졌을 때 IMM에의해 보내지는 메시지이다. 이 메시지를 DefWndProc에 넘기면 그에 해당하는 WM_KEYUP이 불린다. |
WM_IME_CHAR |
문자가 조합이 끝났을 때 IMM에의해 보내지는 메시지이다. |
만일 _UNICODE가 #define되어 있다면 WM_IME_CHAR는 디폴트 동작으로 한번의 WM_CHAR를 발생시키고 wParam에 WCHAR형의 글자를 넘긴다. 만일 _UNICODE가 #define되어있지 않다면, WM_IME_CHAR는 디폴트 동작으로 두 번의 WM_CHAR를 발생시키고 wParam으로는 상위바이트, 하위바이트를 차례대로 넘긴다.
만일 입력이 들어오면 배열에 문자를 저장하는 코드가 있다면, 이 코드가 한글과같은 Double Bytes Character를 고려하지 않고 작성되었더라도, 한글이 입력되었을 때는 두 번의 WM_CHAR가 차례로 상위바이트, 하위 바이트를 넘기므로, 아무런 문제 없이 작동할 것이다. 많은 외국 회사에서 작성한 에디터나 텔넷 프로그램들이 한글 Windows에서 그렇게 작동하고 있는 것이다.
그러나 위에 소개한 세 개의 IME 메시지보다 더 중요한 메시지들이 있다.
메시지 |
의미 |
WM_IME_STARTCOMPOSITION |
문자가 조합이 시작 |
WM_IME_COMPOSITION |
문자가 조합 중 |
WM_IME_ENDCOMPOSITION |
문자가 조합이 끝남 |
WM_IME_STARTCOMPOSITION과 WM_IME_ENDCOMPOSITION 메시지는 한글 조합이 시작할 때와 끝났을 때 알려주는 메시지이며, 필요하지 않으면 대게의 경우 DefWndProc로 넘겨진다. 그러나 WM_IME_COMPOSITION 메세지는 한글모드에서 한글이 조합되는 중 발생하므로 만일 완전한 조합이 되지 않은 글자의 출력을 원한다면 반드시 처리해야 한다.
WM_IME_COMPOSITION이 발생하면 lParam의 값을 검사해서 현재 글자가 확정되었는지 확정되지 않았는지 알수 있다. 이 때 wParam으로는 새로 추가된 글자가 넘어오지만 대부분의 경우 ImmGetCompositionString 함수를 호출하여 현재까지의 변경된 버퍼 상태를 얻어오는 것이 일반적이다. 다음은 키보드 입력에 따른 Composition 버퍼의 내용이다.
입력순서 |
lParam의 내용 |
Composition 버퍼의 내용 |
ㄱ |
GCS_COMPSTR |
ㄱ |
ㅏ |
GCS_COMPSTR |
가 |
ㄴ |
GCS_COMPSTR |
간 |
ㅏ |
GCS_RESULTSTR |
가 |
|
GCS_COMPSTR |
나 |
ㄷ |
GCS_COMPSTR |
낟 |
ㅏ |
GCS_RESULTSTR |
나 |
|
GCS_COMPSTR |
다 |
3 |
GCS_RESULTSTR |
다3 |
다음은 사용자가 backspace를 눌러 조합중인 음소를 지우는 경우이다.
입력순서 |
lParam의 내용 |
Composition 버퍼의 내용 |
ㄱ |
GCS_COMPSTR |
ㄱ |
ㅏ |
GCS_COMPSTR |
가 |
Back Space |
GCS_COMPSTR |
ㄱ |
Back Space |
GCS_COMPSTR |
NULL (길이 0) |
위의 표를 잘 분석해 보면, 우리는 lParam에 GCS_COMPSTR이 들어올 때는 캐럿 위치를 증가시키지 않고 단지 Composition 버퍼의 내용을 현재 캐럿 위치에 출력하고, lParam에 GCS_RESULTSTR이 들어오면 이 때는 WM_CHAR 또한 불릴 것이므로 WM_CHAR에서 글자를 출력하고 캐럿 위치를 이동하여야 함을 알 수 있다.
마지막으로 IMM의 상태가 변경되었을 때 우리에게 알려주는 메시지를 알아보고 이 절을 마치도록 하자.
메시지 |
내용 |
wParam |
WM_IME_NOTIFY |
IMM의 상태가 바뀔 때 |
글자 입력창 관련 |
IMN_OPENCANDIDATE
IMN_CHANGECANDIDATE
IMN_SETCANDIDATEPOS
IMN_CLOSECANDIDATE |
상태 창 관련 |
IMN_OPENSTATUSWINDOW
IMN_SETOPENSTATUS
IMN_SETSTATUSWINDOWPOS
IMN_CLOSE_STATUSWINDOW
|
문자 조합 관련
|
IMN_SETCOMPOSITIONFONT
IMN_SETCOMPOSITIONWINDOW |
모드 관련 |
IMN_SETCONVERSIONMODE
IMN_SETSENTENCEMODE
|
예를 들어 한/영 상태가 변경될 때 마다 상태바에 한/영 상태를 표시해 주고 싶다면 WM_IME_NOTIFY에서 wParam이 IMN_SETCONVERSIONMODE가 들어올 때 현재 IME 상태를 얻어 상태바에 표시해 주면 된다. 다음은 현재 IME 상태를 얻어오는 코드이다.
DWORD dwConversion, dwSentence; /* 현재의 IME 상태가 저장될 곳이다 */
HIMC himc = ImmGetContext(hwnd); /* 현재 input context의 핸들을 얻어 온다 */
ImmGetConversionStatus(himc, &dwConversion, &dwSentence); /* 현재 IME 상태를 얻어 온다. */
ImmReleaseContext(hwnd, himc); /* input context의 핸들을 반납한다 */
출처 :
http://cafe.naver.com/cyberzone.cafe?iframe_url=/ArticleRead.nhn%3Farticleid=400