티스토리 뷰
키보드 이벤트 처리하기
본장은 키보드로부터 문자키와 펑션키등이 입력되었을 때 이벤트를 처리하는 방법에 대해서 설명합니다. 키보드에 있는 모든키와 시스템에서 사용하는 키들을 직접 제어 할수 있는 방법을 배우시게 될것입니다. 한들윈도우에서는 한영전환이 자동으로 내장되어 있습니다. 키보드 입력키로 현재 한글상태인지 영문상태인지 확인할 수가 없습니다. 이것을 확인하는 방법은 IME를 이용하는 것인데 본장에서는 키보브에서 한영 전환이 되는 것을 알아보는 방법까시 설명하게습니다.
본장을 통해서 키보드에서 발생하는 모든 이벤트를 만드는 방법을 알수 있을것입니다.
키보드 입력 메시지
키보드로부터 키가 입력될때에는 표1과 같은 내용의 메시지들이 전달됩니다.
(표1) 키눌림 메시지
메시지 | 내용 |
WM_CHAR | 문자키가 눌려졌 을경우 |
WM_KEYDOWN | 키보드에서 키가 눌려짐 |
WM_KEYUP | 키보드에서 키가 눌려졌다 띄어짐 |
WM_SYSKEYDOWN | Alt키와 조합된 시스템 키가 눌려짐 |
WM_SYSKEYUP | Alt키와 조합된 시스템 키가 눌려졌다 띄어짐 |
WM_KEYDOWN은 키보드에서 키가 눌려 졌을 경우 발생됩니다. 키가 눌려진 상태에서는 WM_KEYDOWN이 키를 띄었을 경우 WM_KEYUP가 발생됩니다.
만일 'a'라는 키가 눌렀다 띄었다고 가정을 한다면 제일 처음으로 발생되는 메시지가 WM_KEYDOWN입니다. 이후에 WM_CHAR가 발생되고 그다음 WM_KEYUP가 발생됩니다. 만일 Shift키를 누르면서 'A'키를 키보드에서 설정하였다면 어떻게 메시지가 발생될까요? 먼저 Shift키가 눌려졌다는 WM_KEYDOWN 이 발생되며 다음으로 'A'에 해당하는 WM_KEYDOWN 그다음으로 ‘A'에 해당하는 WM_CHAR 그다음으로 'A' 에 해당하는 WM_KEYUP 그다음으로 Shift 에 해당하는 WM_KEYUP 가 발생됩니다. 이런식으로 WM_CHAR와 WM_KEYDOWN과 WM_KDYUP는 상호 연관되어 발생됩니다. 그러나 F1키 같은 펑션키를 눌렀을경우에는 WM_CHAR은 발생되지 않습니다.
WM_SYSKEYDOWN은 시스템 키를 눌렀을 때 발생됩니다. 이키란 Alt와 함께 조합된 키늘 말합니다. Alt+F4는 종료키며 Alt+Tab는 어플리케이션 프로그램의 상호 전환인 것 같은 Alt와 조합된 키를 시스템 키라고 하며 이것은 특별하게 WM_SYSKEYDOWN과 WM_SYSKEYUP로 분리하여 메시지를 설정하였습니다.
WinMain에서 다음과 같은 문장을 보신적이 있을것입니다.
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
여기에서 TranslateMessage는 WM_KEYDOWN을 받아서 현재 눌려지 키의 값을 가지고 문자를 만들어 내는 기능을합니다. 예를 들어 Caps Lock가 켜지지 않은 상태에서 Shift 키를 누른상태에서 A자를 눌렀다면 실제 코드는 “A"입니다. 그러나 Shift키를 누르지 않은 상태에서 A자를 눌렀다면 이코드는 ”a"인것입니다. 이렇게 TranslateMessage는 WM_KEYDOWN에의해서 WM_CHAR를 만들어 내는 작업을 합니다. WM_CHAR이나 WM_KEYDOWN 메시지와 함께 넘겨주는 wParam에는 key값이 설정되어 있습니다. lParam에는 이키들의 정보가 들어 있는데 키의 정보는 표2와 같습니다.
바이트 번호 (상위첫바이트가 0) | 키 값 | 내용 |
0 | Transition Statate | 키가 눌려지면 1 띄어지면 0 |
1 | Previous Key State | 이전에 어떤 키가 눌려졌는가에 대한 값 |
2 | Context Code | Alt키가 눌려지면 1 그렇지 않으면 0 |
3-6 | 사용하지 않음 |
|
7 | Extended Key Flag | 확장 키보드에서 키가 눌려지면 1 그렇지 않으면 0 |
8-15 | OEM Scan Code | 키보드 스캔코드 |
16-31 | Repeat Count | 킷값의 반복횟수 |
lParam의 상위 0비트가 1로 설정되어 있으면 키가 눌려진것이고 0이면 키가 띄어진 형태입니다. 두 번째 비트는 Previous Key State로 이전에 어떤 키가 눌려졌는가에 대한 값입니다. 이런 의미라고 생각한다면 어플리케이션이 처음 시작되고 키를 누른다음부터는 Previous Key State는 어떤 키가 눌려진상태는 0이고 띈상태에서 1로 설정됩니다. Contex Code란 Alt키가 눌려졌을 경우 1로 그렇지 않을 경우 0이됩니다. Extended Key Flag는 확장키보드 즉 숫자판이 있는 키보드에서 NumLock나 / 또는 Enter키가 눌려졌을 경우 1로 그렇지 않을 경우 0으로 설정됩니다. OEM Scan Code는 현재 눌려진 키보드 스캔코드 값입니다. 일반적으로 문자 값으로 보아도 되지만 윈도우에서느 가상키를 사용하고 이가상키는 wParam으로부터 얻을수 있고 또한 문자값은 WM_CHAR와 함께 발생된 wParam으로부터 얻을수 있기 때문에 사용하지 않습니다. 16-31은 킷값의 반복횟수가 저장됩니다. 이것은 키를 누른 메시지가 처리되기전에 킷값이 또 입력되었을 때 연속적으로 메시지 큐에 저장하는 것이 아니라 WM_KEYDOWN 메시지를 하나로 묶고 반복 계수를 증가시킴으로써 현재 키가 몇번의 반복으로 눌려졌다는 것을 알려줍니다.
이반복 계수를 무시하게 되면 키를 연속적으로 몇번을 눌러도 그것이 수행되지 않았을 경우 잃어 버리게 됩니다. 결국 WM_MOUSEMOVE처럼 현재성을 중시하여 이전에 들어왔던 메시지가 무시되는 것입니다. 그러나 키보드 입력에서는 킷값을 놓치지 말아야 할경우가 있습니다. 이럴경우에는 반복계수를 확인하여 그만큼 연속적으로 키를 설정해 주어야 합니다.
가상키 코드
가상 키코드란 windows.h에 정의한 키코드 값들입니다. 표3은 가상키의 일부분입니다.
키 코드 | 내용 | 코드값 |
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 |
WM_KEYDOWN 키가 눌려졌을 경우 해당되는 키가 눌려진 것에 대한 프로그램을 제작한다면 위의 정의 코드를 이용하여 다음과 같이 할수 있습니다.
case WM_KEYDOWN:
switch(wParam)
{
case VK_F1:
//F1 키가 눌려졌을 때 내용
break;
case VK_F2:
//F2 키가 눌려졌을 때 내용
break;
case VK_RETURN:
//Enter 키가 눌려졌을 때 내용
break;
case VK_SPACE:
//Space 바가 눌려졌을 때 내용
break;
:
}
break;
이외에서 마우스 버튼이 눌려지게 되면 VK_LBUTTON,VK_RBUTTON,VK_MBUTTON의 값이 들어오게 됩니다. 여기에서 VK_MBUTTON은 마우스 중간 버튼을 의미하는데 이값이 들어오기 위해서는 특정 마우스 드라이버가 설정되어 있거나 또는 다른 방법에 의해서 중앙버튼이 활성화 된상태에서만 가능합니다. 실제로 윈도우에서는 마우스 중앙버튼을 거히 사용하지 않기 때문입니다.
키상태 얻기
WM_KEYDOWN 이나 WM_SYSKEYDOWN은 Shift키나 Ctrl키 Alt키 Caps Lock키 Scroll Lock키등의 눌려진 상태를 wParam이나 또는 lParam을 통해서 알려주지 않습니다. 이런 키들이 눌려진 상태를 알고자 할 경우 GetKeyState 함수를 사용합니다.
SHORT GetKeyState(
int nVirtKey //가상키값
);
예를 들어서 Shift키를 가 눌려졌는가를 알기 위해서는 다음과 같이 합니다.
short code;
code=GetKeyState(VK_SHIFT);
만일 code가 음수일경우에는 Shift키가 눌려진것이고 음수가 아닐경우에는 눌려지지 않은것입니다. GetKeyState함수는 표3에 정의되어 있지않는 특수한 경우의 키상태로 알수 있습니다. 즉 좌측 Alt키 우측 Alt 좌측 Shift 우측 Shift 좌측 Ctrl키 우측 Ctrl 키등을 얻을수 잇습니다. 이 키에 대한 정의 값은 다음과 같습니다.
VK_LSHIFT VK_RSHIFT
VK_LCONTROL VK_RCONTROL
VK_LMENU VK_RMENU
만일 좌측 Alt키가 눌려진 것을 알고자 한다면 다음과 같이 할수 있을것입니다.
short code;
code=GetKeyState(VK_LMENU);
키정보 얻기 예제 KeyEx
KeyEx는 키가 눌려 졌을 때 lParam에 있는 정보와 wParam에 있는 정보를 화면에 출력하는 예제입니다. lParam은 비트연산을 이용하여 각 비튼의 값의 정보를 얻어서 화면에 출력을 합니다.
(프로그램 소스)
//시스템 키입력 예제
//KeyEx.c
#include <windows.h>
void main();
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static char szAppName[] = "KeyEx" ;
HWND hwnd ;
MSG msg ;
WNDCLASSEX wndclass ;
wndclass.cbSize = sizeof (wndclass) ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
wndclass.hIconSm = LoadIcon (NULL, IDI_APPLICATION) ;
RegisterClassEx (&wndclass) ;
hwnd = CreateWindow (szAppName,
"키입력예제:KeyEx",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam)
{
HDC hdc ;
PAINTSTRUCT ps ;
int scancode;
char temp[80];
switch (iMsg)
{
case WM_CREATE :
return 0 ;
case WM_PAINT :
hdc = BeginPaint (hwnd, &ps) ;
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
case WM_KEYUP:
case WM_SYSKEYUP:
case WM_CHAR:
hdc=GetDC(hwnd);
//문자 코드 얻기
wsprintf(temp,"문자코드=%c",wParam);
TextOut(hdc,0,20,temp,strlen(temp));
//Transition 값 얻기
((lParam&0x80000000)==0x80000000) ? wsprintf(temp,"Transition=1") :wsprintf(temp,"Transition=0");
TextOut(hdc,0,40,temp,strlen(temp));
//Previous Key 값 얻기
((lParam&0x40000000)==0x40000000) ? wsprintf(temp,"PreviousKey=1") :wsprintf(temp,"PreviousKey=0") ;
TextOut(hdc,0,60,temp,strlen(temp));
//ContenxCode 값 얻기
((lParam&0x20000000)==0x20000000) ? wsprintf(temp,"ContextCode=1") :wsprintf(temp,"ContextCode=0") ;
TextOut(hdc,0,80,temp,strlen(temp));
//ExtendKey값 얻기
((lParam&0x01000000)) ? wsprintf(temp,"ExtendKey=1") :wsprintf(temp,"ExtendKey=0") ;
TextOut(hdc,0,100,temp,strlen(temp));
//Scan code 값 얻기
scancode=HIWORD(lParam)&0xff;
wsprintf(temp,"Scancode=%04d",scancode);
TextOut(hdc,0,120,temp,strlen(temp));
wsprintf(temp,"RepeatCount=%d",LOWORD(lParam));
TextOut(hdc,0,140,temp,strlen(temp));
ReleaseDC(hwnd,hdc);
return 0;
case WM_DESTROY :
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, iMsg, wParam, lParam) ;
}
(프로그램 소스끝)
그림 1은 KeyEx 출력 결과입니다.
(그림 1) KeyEx 출력 결과
카렛 만들기
카렛이란 윈도우에서 키보드입력시 사용되는 커서를 이야기 합니다. 도스 시절는 글자를 입력할 때 화면에 깜빡이는 직선 바를 커서라고 하였으나 윈도우에서는 카렛이라고 합니다. 윈도우에서 커서는 마우스 커서를 의미하며 워드 프로세서등에 나타나는 키보드 입력시의 커서를 카렛이라고 합니다.
카렛을 만들고 보여주고 그리고 삭제할경우에는 다음과 같은 순서로 됩니다.
//카렛을 만든다.
CreateCaret(hwnd,NULL,xsize,ysize);
//카렛을 보여준다
ShowCaret(hwnd);
//카렛의 위치를 설정한다
SetCaretPos(pos*xsize,0);
: 문자 입력을 받는동안 카렛의 위치를 바꾸어 준다.
//카렛을 감춘다
HideCaret(hwnd);
//카렛을 삭제한다.
DestroyCaret();
먼저 CreateCaret 함수를 이용하여 카렛을 만듭니다. 첫 번째 인자는 윈도우 핸들이며 두 번째 인자는 HBITMAP 형입니다. 이곳에 비트맵을 설정하면 비트맵 모양의 카렛이 설정됩니다. xsize와 ysize는 카렛의 가로와 세로 크기입니다. 보통 카렛을 만들고자 할 경우 설정된 폰트의 가로세로크기와 같게 만드는 것이 보기에 좋습니다. 그렇기 때문에 xsize와 ysize를 얻기 위해서는 GetTextMetrics 사용하는 것이 좋습니다.
GetTextMetrics(hdc,&tm);
xsize=tm.tmAveCharWidth;
ysize=tm.tmHeight;
GetTextMetrics는 GDI편에서 설명하였듯이 문자의 크기 정보를 리턴합니다. 여기에서 tmAveCharWidth 는 문자들의 평균 폭이며 tmHeight는 문자의 높이이므로 이크기에 맞게 카렛을 설정하는 것이 적당합니다.
이렇게 카렛을 만든다음 ShowCaret하시면 카렛이 나타나고 카렛을 특정위치로 이동시키고자 할경우에는 SetCaretPos를 하시면 됩니다.
SetCaretPos는 키보드에서 문자를 입력할 때 계속적으로 이동해야 합니다. 예를들어 "Ab“ 라는 입력이 있었다면 "Ab"다음의 위치로 커서가 이동해야 합니다. 윈도우에서는 고정 폰트를 사용하지않고 가변 폰트를 사용하기 때문에 글자의 폭이 다 다릅니다. 그렇기 때문에 문자의 수를 파악하여 카렛 위치를 이동시키면 커서가 문자열 바로 뒤로 붙지 않습니다. 예를 들어서 "Aj"라는 입력을 했을 경우 평균 폭이 10이라고 가정을 하고 문자열이 두 개이므로 첫 번째 위치부터 20포인트 이후로 이동한다면 "Aj" 보다 더 뒤에 커서가 위치할것입니다. 그이유는 평균 폭이 10이지 'j'같은 글자는 10이하는 작은 폭을 가지고 있기 때문입니다. 카렛 위치를 이동할 때 현재 위치까지의 문자열 전체 폭을 얻고 그위치로 이동하는 것이 좋습니다. 문자열의 폭을 얻는 함수는 GetTextExtentPoint 이며 이함수는 다음과 같습니다.
BOOL GetTextExtentPoint(
HDC hdc, // 디바이스 컨텍스트 핸들
LPCTSTR lpString, // 텍스트 문자열
int cbString, // 텍스트 문자열에서 특정위치까지의 번호
LPSIZE lpSize // 리턴되는 문자열의 가로와 세로 크기
);
예를 들어 "Abjced" 라는 문자열에서 j라는 문자열까지의 가로와 세로의 크기를 얻고자 한다면 다음과 같이 할수 있습니다.
SIZE size;
GetTextExtentPoint(hdc,"Abjced",2,&size);
위와 같이하면 "Abj"의 문자열의 가로크기와 세로크기가 size의 cx와 cy에 저장되게 됩니다. 보통 cy는 크게 필요가 없을것입니다. 문자열의 높이는 가변 폰트라 하더라도 동일하게 간주해도 되기 때문입니다. 그러나 cx의 값은 매우 중요합니다. 이값을 얻어서 SetCaretPos 에 넣어주니까요.
WM_CHAR메세지에 의해서 문자가 들어 올 경우 이문자를 버퍼에 놓고 커서를 문자열 끝으로 이동한다고 할 경우 다음과 같을 것입니다.
case WM_CHAR:
//키값을 szBuff에 넣는다
code=wParam;
szBuff[pos]=code;
pos++;
szBuff[pos]=0x00;
hdc=GetDC(hwnd);
//현재 위치의 문자열 크기를 얻는다.
GetTextExtentPoint(hdc,szBuff,pos,&size);
//카렛 을 이동시킨다.
SetCaretPos(size.cx,0);
ReleaseDC(hwnd,hdc);
break;
WM_SETFOCUS 와 WM_KILLFOCUS
포커스를 갖는다는 것은 현재 윈도우가 가장 상위에 활성화 되어 있다는 의미이며 입력을 받을수 있다는 의미입니다. 카렛은 입력 포커스를 받는 상황하에서만 활성화 될 수 있습니다. 입력포커스를 받는다는 것을 어떻게 알수가 있을까요. 윈도우가 입력포커스를 받는 상황이 되면 WM_SETFOCUS가 발생되면 포커스가 사라지면 WM_KILLFOCUS가 발생됩니다. 버튼이나 여러 자원들이 설정되어 있지 않는 그냥 윈도우 상태에서는 WM_SETFOCUS는 윈도우가 활성화 될 때 발생되며 윈도우가 다른 윈도우에 의해서 가려지거나 사라지거나 할 경우 WM_KILLFOCUS가 발생됩니다. 다른 윈도우에 의해서 가려져 비활성화 되다가 다시 활성화 되면 또다시 WM_SETFOCUS가 발생됩니다. 카렛을 만들고 화면에 보이고자 할경우에는 WM_SETFOUCUS에 그리고 WM_KILLFOCUS가 발생되면 카렛을 감추고 삭제하는 것이 일반적으로 카렛을 사용하는 방법입니다.
WM_SETFOCUS :
카렛을 만들고 화면에 나타냄
break;
WM_KILLFOCUS:
카렛을 감추고 화면에서 삭제한다.
break;
위의 카렛을 만들고 보여주는 부분은 WM_SETFOCUS에 감추는 부분은 WM_KILLFOCUS에 넣는 것이 이상적일것입니다.
WM_SETFOCUS는 카렛을 만들경우에만 사용되는 것이 아니라 여러개의 차일드 윈도우를 컨트롤 할 때도 사용됩니다. 이부분에 대해서는 뒤의 차일드 윈도우 컨트롤 부분에서 자세히 설명할것입니다.
한줄 문자 입력기 예제 TypingEx
TypingEx는 윈도우 클라이언트 영역(0,0) 좌표에서 총 80개의 문자열을 입력받는 타이핑 예제입니다. 지금까지 설명한 카렛과 WM_SETFOCUS와 WM_KILLFOCUS를 이용하여 1줄짜리의 간단한 에디터를 만든것입니다. 이에디터는 커서가 이동이 되지 않으며 또한 Backspace도 가동되지 않습니다. 다만 입력하는데로 문자가 화면에 출력될것입니다. 이 예제를 통해서 에디터를 어떻게 만드는가에 대한 기본을 알수 있을것입니다.
(프로그램 소스)
//타이핑 예제
//TypingEx.c
#include <windows.h>
void main();
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static char szAppName[] = "TypingEx" ;
HWND hwnd ;
MSG msg ;
WNDCLASSEX wndclass ;
wndclass.cbSize = sizeof (wndclass) ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
wndclass.hIconSm = LoadIcon (NULL, IDI_APPLICATION) ;
RegisterClassEx (&wndclass) ;
hwnd = CreateWindow (szAppName,
"타이핑예제:TypingEx",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam)
{
HDC hdc ;
PAINTSTRUCT ps ;
TEXTMETRIC tm;
static int xsize=0,ysize=0;
static int pos=0;
static char szBuff[80];
SIZE size;
char code;
char temp[80];
HRGN rgn;
switch (iMsg)
{
case WM_CREATE :
strcpy(szBuff,"");
return 0 ;
case WM_SETFOCUS:
hdc=GetDC(hwnd);
GetTextMetrics(hdc,&tm);
xsize=tm.tmAveCharWidth;
ysize=tm.tmHeight;
//카렛을 만든다.
CreateCaret(hwnd,NULL,xsize,ysize);
//카렛을 보여준다
ShowCaret(hwnd);
//카렛의 위치를 설정한다
SetCaretPos(pos*xsize,0);
ReleaseDC(hwnd,hdc);
return 0;
case WM_KILLFOCUS:
//카렛을 감춘다
HideCaret(hwnd);
//카렛을 삭제한다.
DestroyCaret();
return 0;
case WM_PAINT :
hdc = BeginPaint (hwnd, &ps) ;
TextOut(hdc,0,0,szBuff,strlen(szBuff));
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_CHAR:
//키값을 szBuff에 넣는다
code=wParam;
szBuff[pos]=code;
pos++;
szBuff[pos]=0x00;
//버퍼 크기는 80이므로 80이 넘으로 다시 0
if(pos>=80)
pos=0;
hdc=GetDC(hwnd);
//현재 위치의 문자열 크기를 얻는다.
GetTextExtentPoint(hdc,szBuff,pos,&size);
//카렛 을 이동시킨다.
SetCaretPos(size.cx,0);
//문자열 코드를 화면에 출력한다.
wsprintf(temp,"char:%03d",wParam);
TextOut(hdc,0,20,temp,strlen(temp));
ReleaseDC(hwnd,hdc);
//무효화 사각형 영역을 만든다.
rgn=CreateRectRgn(0,0,500,20);
InvalidateRgn(hwnd,rgn,TRUE);
DeleteObject(rgn);
break;
case WM_DESTROY :
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, iMsg, wParam, lParam) ;
}
(프로그램 소스끝)
본예제의 출력 결과는 그림 2와 같습니다.
(그림 2)TypingEx 예제 출력 결과
한글 입출력에 대한 컨트롤
TypingEx를 실행시키면 한글이 완벽하게 완성되었을경우에만 화면에 나타나는 것을 보게 될것입니다. 이것은 조금 불만스러운 느낌을 줍니다. 워드프로세서에서는 한글이 완벽하게 구현이 되지 않더라도 화면에 나타나는데 TypingEx는 그렇게 되지 않으니까요? 실제로 WM_CHAR는 한글이 완벽하게 설정되었을 때만 발생됩니다. 그전에는 발생이 되지 않기 때문에 한글 입력상태에서는 키보드 한문자 한문자에 대해서 WM_CHAR로 알수가 없게 되는 것입니다. 이때는 WM_KEYDOWN으로는 알수가 있으나 문제는 현재가 한글 입력상태인지 또는 영문 입력상태인지를 알수가 없다는 것입니다. 이부분을 해결하기 위해서는 IMM를 핸들링 해야 합니다. IMM이란 Input Method Manager 의 약자입니다. 즉 한글과 같이 여러 키의 조합으로 글자를 만들 경우 사용되는 함수 라이브러리라고 볼수 있습니다. IMM에 대한 여러 함수는 imm.h에 설정되어 있습니다. 본책에서는 IMM에 대한 모든 개념 설명은 하지 않습니다. 단 필요한 부분 즉 현재 입력모드가 한글인가? 영문인가? 와 한글일 경우 완성된 문자가 아니더라도 화면에 출력하는 방법에 대해서 설명을 합니다. 사실 이부분만 알아도 한글을 출력하는데는 문제가 없기 때문입니다.
한영 전환키를 눌렀을때의 메시지
한영 전환키를 눌렀을때는 WM_IME_NOTIFY 발생됩니다. 이때 lParam에는 0값이 wParam에는 6과 8이 교차적으로 나타납니다. 이값으로는 한영전환을 알수가 없습니다. ImmGetConversionStatus 함수를 이용하여 상태를 얻을수는 있습니다. WM_IME_NOTIFY는 한영전환에 대한 여러 Notify를 알려주기 때문에 이때마사 imm.h설정되어 있은 여러 함수를 이용하여 상태를 얻을수 있습니다. 이렇게 복잡한 방법이 아니더라도 한영전환을 알수 있는 또하나의 방법이 있습니다. 그것은 플러그를 만드는것입니다. 프로그램이 시작되어 입력 포커스를 받은 처음상태는 영문 상태입니다. 이때부터 WM_IME_NOTIFY가 발생될때마다 플러그를 변화시키는 것입니다.
//전역 변수로 한영 플러그를 설정하고 초기값을 FALSE로 (영문상태)
BOOL hanFlag=FALSE;
case WM_IME_NOTIFY:
hanFlag=1-hanFlag;//한영 플러그 전환
break;
위와 같이 하면 WM_IME_NOTIFY를 받았을 경우 플러그가 전환됨으로 한글과 영문 상태를 알수가 있습니다. 즉 한영키를 전환하면 1로 설정되면서 한글상태라는 것을 알려주고 다시 한영키를 누르면 0으로 설정되면서 영문상태를 알려주는 것입니다.
한글 조합시작과 종료 메시지
한글 조합의 시작 메시지는 WM_IME_STARTCOMPOSITION 이며 종료 메시지는 WM_IME_ENDCOMPOSITION입니다. 또한 현재 키가 입력되고 조합중이라는 메시지는 WM_IME_COMPOSITION입니다. 이 3개의 메시지를 이용하여 한글을 출력할 수가 있습니다.
//한글 조합 시작
case WM_IME_STARTCOMPOSITION:
://이때는 한글 모드라는 것을 완벽하게 알수 있습니다.
//한글 조합중
case WM_IME_COMPOSITION:
//이때는 현재 한글이 조합중이므로 완성되지 못한 한글 코드를 알수
// 있습니다.
break;
//한글 조합 완료 즉 영문모드로 전환했을경우
case WM_IME_ENDCOMPOSITION:
//이때는 조합이 끝이므로 영문 모드라는 것을 알수 있습니다.
break;
WM_IME_STARTCOMPOSITION은 한글 모드가 시작되면서 첫키를 눌렀을 때 발생하는 메시지입니다. 이때는 분명히 한글 모드라는 것을 알수가 있는것입니다.
WM_IME_COMPOSITION은 현재 한글이 조합중이라는 것을 알수 있습니다.
이때 wParam / 256 이 한글의 상위 비트이며 wParam % 256 한글의 하위비트입니다. 완성이 되지 못한 이글자를 두바이트 이상의 문자열로 만들어 화면에 출력하면 비완성된 문자가 화면에 나타납니다.
case WM_IME_COMPOSITION:
hdc=GetDC(hwnd);
//비완성된 문자를 Han에 받는다
Han[0] = (BYTE)(wParam / 256);
Han[1] = (BYTE)(wParam % 256);
Han[2]=0x00;
//화면에 Han를 출력한다.
TextOut(hdc,size.cx,0,Han,strlen(Han));
ReleaseDC(hwnd,hdc);
break;
WM_IME_ENDCOMPOSITION 한글 조합이 끝났다는 의미인데 한영전환키를 눌러서 영문 모드로 전환하였을 경우 발생합니다. 좀더 안전을 기하기 위해서 이때는 한영 전환 플러그 hanFlag를 FALSE로 설정하여 영문모드라는 것을 확인하는 것이 좋을것입니다.
case WM_IME_ENDCOMPOSITION:
hanFlag=FALSE;
break;
이렇게 함으로써 한글을 화면에 출력할수 있는 것입니다.
한글출력 1줄 에디터 예제 HanType
HanType는 한글 출력을 하면서 현재 모드가 한글인지 영문인지를 판독할수 있는예제입니다. 이예제는 "imm.h"를 꼭 포함시켜야 합니다.프로그램으 소스는 다음과 같습니다.
(프로그램 소스)
//한글타이핑 예제
//HanType.c
#include <windows.h>
#include "imm.h" //꼭 포함 시킬것
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static char szAppName[] = "HanType" ;
HWND hwnd ;
MSG msg ;
WNDCLASSEX wndclass ;
wndclass.cbSize = sizeof (wndclass) ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
wndclass.hIconSm = LoadIcon (NULL, IDI_APPLICATION) ;
RegisterClassEx (&wndclass) ;
hwnd = CreateWindow (szAppName,
"타이핑예제:Hantype",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
//전역 변수로 한영 플러그를 설정하고 초기값을 FALSE로 (영문상태)
BOOL hanFlag=FALSE;
LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam)
{
HDC hdc ;
PAINTSTRUCT ps ;
TEXTMETRIC tm;
static int xsize=0,ysize=0;
static int pos=0;
static char szBuff[80];
SIZE size;
char code;
char temp[80];
char Han[5];
HRGN rgn;
switch (iMsg)
{
case WM_CREATE :
strcpy(szBuff,"");
return 0 ;
case WM_SETFOCUS:
hdc=GetDC(hwnd);
GetTextMetrics(hdc,&tm);
xsize=tm.tmAveCharWidth;
ysize=tm.tmHeight;
CreateCaret(hwnd,NULL,xsize,ysize);
ShowCaret(hwnd);
SetCaretPos(pos*xsize,0);
ReleaseDC(hwnd,hdc);
return 0;
case WM_KILLFOCUS:
HideCaret(hwnd);
DestroyCaret();
return 0;
case WM_PAINT :
hdc = BeginPaint (hwnd, &ps) ;
TextOut(hdc,0,0,szBuff,strlen(szBuff));
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_CHAR:
code=wParam;
szBuff[pos]=code;
pos++;
szBuff[pos]=0x00;
if(pos>=80)
pos=0;
hdc=GetDC(hwnd);
GetTextExtentPoint(hdc,szBuff,pos,&size);
SetCaretPos(size.cx,0);
wsprintf(temp,"char:%03d",wParam);
TextOut(hdc,0,20,temp,strlen(temp));
ReleaseDC(hwnd,hdc);
rgn=CreateRectRgn(0,0,500,20);
InvalidateRgn(hwnd,rgn,TRUE);
DeleteObject(rgn);
break;
//한글 조합 시작
case WM_IME_STARTCOMPOSITION:
wsprintf(temp,"조합상태");
hdc=GetDC(hwnd);
TextOut(hdc,0,60,temp,strlen(temp));
ReleaseDC(hwnd,hdc);
break;
//한글 조합중
case WM_IME_COMPOSITION:
hdc=GetDC(hwnd);
GetTextExtentPoint(hdc,szBuff,pos,&size);
//비완성된 문자를 Han에 받는다
Han[0] = (BYTE)(wParam / 256);
Han[1] = (BYTE)(wParam % 256);
Han[2]=0x00;
//화면에 Han를 출력한다.
TextOut(hdc,size.cx,0,Han,strlen(Han));
ReleaseDC(hwnd,hdc);
break;
//한글 조합 완료 즉 영문모드로 전환했을경우
case WM_IME_ENDCOMPOSITION:
hanFlag=FALSE;
wsprintf(temp,"영문");
hdc=GetDC(hwnd);
TextOut(hdc,0,40,temp,strlen(temp));
ReleaseDC(hwnd,hdc);
break;
case WM_IME_NOTIFY:
hanFlag=1-hanFlag;//한영 플러그 전환
if(hanFlag)
{
wsprintf(temp,"한글");
hdc=GetDC(hwnd);
TextOut(hdc,0,40,temp,strlen(temp));
ReleaseDC(hwnd,hdc);
}
break;
case WM_DESTROY :
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, iMsg, wParam, lParam) ;
}
(프로그램 소스끝)
그림 3은 HanType 출력 결과입니다.
(그림 3)HanType 출력 결과
한글 키 컨트롤 및 상태 얻기
HanType 예제에서는 완벽하게 한글 전환이 되지 않습니다. 입력을 하다가 space바를 누르게 되면 갑자기 영문상태로 바뀌거나 잠시 잠깐 한글과 영문상태가 잘못 출력되는 오류가 나오게 됩니다. 이제 완벽하게 한글입출력을 할수 있는 방법을 알아보겠습니다.
첫 번째로 현재 상태가 한글인지 영문인지를 알고자 할 경우 이때는 다음과 같은 방법을 이용합니다.
HIMC data; //IMM핸들 변수 설정
//한글인지 영문인지를 알아본다
//현재 윈도우의 IMM 핸들러을 얻는다.
data=ImmGetContext(hwnd);
//한영상태를 얻는다.
//hanFlag가 1이면 한글 0이면 영문임
hanFlag=ImmGetOpenStatus(data);
//IMM핸들을 해제 한다.
ImmReleaseContext(hwnd,data);
ImmGetContext 함수를 이용하여 IMM핸들러를 얻은다음 ImmGetOpenStatus함수를 이용하여 현재 입력모드를 알아내면 됩니다. 이때 리턴되는 값이 1이면 한글이고 0이면 영문으로 설정된것입니다. ImmGetContext와 쌍인 ImmReleaseContext함수를 이용하여 IMM 핸들을 해제시켜 주어야 합니다.
두 번째로 우리가 한영전환을 마음대로 하고자 할경우가 있습니다. 이방법은 ImmSetConversionStatus 함수를 이용하면 됩니다.
//핸들러 얻기
data=ImmGetContext(hwnd);
//한글전환시
ImmSetConversionStatus(data,1,0);
//영문 전환시
ImmSetConversionStatus(data,0,0);
//핸들러 해제
ImmSetConversionStatus함수의 두 번째 인자값을 1로 하였을 경우 한글로 설정되고 0으로 하였을 경우 1로 설정됩니다. 많일 프로그램이 시작되면서 바로 한글모드로 하고 싶다면 ImmSetConversionStatus함수의 두 번째 인자를 1로 설정하고 실행시키면 됩니다.
한글전환 예제 HanType2
HanType2는 좌측 마우스 버튼을 클릭하면 토글로 한영이 전환되며 완벽하게 현재의 한영 상태를 알아내는 1줄 한글 입력 에디터 예제입니다.
이예제에서는 imm32.lib 라이브러리가 필요합니다. 프로젝트에서 Alt+F7키를 누르거나 Project메뉴에서 Settings항목을 선택하고 카드철에서 Link를 선택하고 그림 4와 같은 화면이 나올 때 Object/library modules: 항목에 imm32.lib를 입력하여 주셔야 합니다.
(그림 4) Settings 화면
프로그램 소스는 다음과 같습니다.
(프로그램 소스)
//한글타이핑 예제
//HanType.c
#include <windows.h>
#include "imm.h"
LRESULT CALLBACK WndProc (HWND, UINT, WPARAM, LPARAM) ;
int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR szCmdLine, int iCmdShow)
{
static char szAppName[] = "HanType" ;
HWND hwnd ;
MSG msg ;
WNDCLASSEX wndclass ;
wndclass.cbSize = sizeof (wndclass) ;
wndclass.style = CS_HREDRAW | CS_VREDRAW ;
wndclass.lpfnWndProc = WndProc ;
wndclass.cbClsExtra = 0 ;
wndclass.cbWndExtra = 0 ;
wndclass.hInstance = hInstance ;
wndclass.hIcon = LoadIcon (NULL, IDI_APPLICATION) ;
wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ;
wndclass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ;
wndclass.lpszMenuName = NULL ;
wndclass.lpszClassName = szAppName ;
wndclass.hIconSm = LoadIcon (NULL, IDI_APPLICATION) ;
RegisterClassEx (&wndclass) ;
hwnd = CreateWindow (szAppName,
"타이핑예제:HanType",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
hInstance,
NULL) ;
ShowWindow (hwnd, iCmdShow) ;
UpdateWindow (hwnd) ;
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
}
//전역 변수로 한영 플러그를 설정하고 초기값을 FALSE로 (영문상태)
BOOL hanFlag=FALSE;
HIMC data;
LRESULT CALLBACK WndProc (HWND hwnd, UINT iMsg, WPARAM wParam, LPARAM lParam)
{
HDC hdc ;
PAINTSTRUCT ps ;
TEXTMETRIC tm;
static int xsize=0,ysize=0;
static int pos=0;
static char szBuff[80];
SIZE size;
char code;
char temp[80];
char Han[5];
HRGN rgn;
switch (iMsg)
{
case WM_CREATE :
strcpy(szBuff,"");
return 0 ;
case WM_SETFOCUS:
hdc=GetDC(hwnd);
GetTextMetrics(hdc,&tm);
xsize=tm.tmAveCharWidth;
ysize=tm.tmHeight;
CreateCaret(hwnd,NULL,xsize,ysize);
ShowCaret(hwnd);
SetCaretPos(pos*xsize,0);
ReleaseDC(hwnd,hdc);
return 0;
case WM_KILLFOCUS:
HideCaret(hwnd);
DestroyCaret();
return 0;
case WM_PAINT :
hdc = BeginPaint (hwnd, &ps) ;
TextOut(hdc,0,0,szBuff,strlen(szBuff));
EndPaint (hwnd, &ps) ;
return 0 ;
case WM_CHAR:
code=wParam;
szBuff[pos]=code;
pos++;
szBuff[pos]=0x00;
if(pos>=80)
pos=0;
hdc=GetDC(hwnd);
GetTextExtentPoint(hdc,szBuff,pos,&size);
SetCaretPos(size.cx,0);
wsprintf(temp,"char:%03d",wParam);
TextOut(hdc,0,20,temp,strlen(temp));
ReleaseDC(hwnd,hdc);
rgn=CreateRectRgn(0,0,500,20);
InvalidateRgn(hwnd,rgn,TRUE);
DeleteObject(rgn);
break;
case WM_KEYDOWN:
//한글인지 영문인지를 알아본다
//현재 윈도우의 IMM 핸들러을 얻는다.
data=ImmGetContext(hwnd);
//한영상태를 얻는다.
//hanFlag가 1이면 한글 0이면 영문임
hanFlag=ImmGetOpenStatus(data);
//IMM핸들을 해제 한다.
ImmReleaseContext(hwnd,data);
if(hanFlag)
{
wsprintf(temp,"한글");
hdc=GetDC(hwnd);
TextOut(hdc,0,40,temp,strlen(temp));
ReleaseDC(hwnd,hdc);
}
else
{
wsprintf(temp,"영문");
hdc=GetDC(hwnd);
TextOut(hdc,0,40,temp,strlen(temp));
ReleaseDC(hwnd,hdc);
}
break;
//한글 조합 시작
case WM_IME_STARTCOMPOSITION:
wsprintf(temp,"조합상태");
hdc=GetDC(hwnd);
TextOut(hdc,0,60,temp,strlen(temp));
ReleaseDC(hwnd,hdc);
break;
//한글 조합중
case WM_IME_COMPOSITION:
hdc=GetDC(hwnd);
GetTextExtentPoint(hdc,szBuff,pos,&size);
//비완성된 문자를 Han에 받는다
Han[0] = (BYTE)(wParam / 256);
Han[1] = (BYTE)(wParam % 256);
Han[2]=0x00;
//화면에 Han를 출력한다.
TextOut(hdc,size.cx,0,Han,strlen(Han));
ReleaseDC(hwnd,hdc);
break;
//한글 조합 완료 즉 영문모드로 전환했을경우
case WM_IME_ENDCOMPOSITION:
wsprintf(temp,"영문");
hdc=GetDC(hwnd);
TextOut(hdc,0,40,temp,strlen(temp));
ReleaseDC(hwnd,hdc);
break;
case WM_LBUTTONDOWN:
hanFlag=1-hanFlag;//한영 플러그 전환
//핸들러 얻기
data=ImmGetContext(hwnd);
//한영 전환을 한다.
ImmSetConversionStatus(data,hanFlag,0);
//핸들러 해제
ImmReleaseContext(hwnd,data);
if(hanFlag)
{
wsprintf(temp,"한글");
hdc=GetDC(hwnd);
TextOut(hdc,0,40,temp,strlen(temp));
ReleaseDC(hwnd,hdc);
}
else
{
wsprintf(temp,"영문");
hdc=GetDC(hwnd);
TextOut(hdc,0,40,temp,strlen(temp));
ReleaseDC(hwnd,hdc);
}
break;
case WM_DESTROY :
PostQuitMessage (0) ;
return 0 ;
}
return DefWindowProc (hwnd, iMsg, wParam, lParam) ;
}
(프로그램 소스끝)
그림 5는 HanType2 의 출력 결과입니다.
(그림 5) HanType2출력 결과
(잔소리)
지금까지 C언어를 배우고 윈도우에서 기본적인 내용을 공부하셨습니다. 혹이 근심된 마음에서 말씀드리는데 지금까지 공부한내용이 조금은 알겠는데 확실히 모르겠다 하시는 분들이 계신다면 주저 없이 다음으로 넘어가주시기를 바랍니다. “에이 처음부터 다시 확실하게 공부하고 넘어가자” 라고 다시 처음으로 가지 말아주시기 바랍니다. 컴퓨터 프로그램은 절대 줄그어 가면서 한 Chapter 한 Chapter이해해가면서 완벽을 기하는 방법으로 공부하시면 절대 안됩니다. 그냥 페인트 칠하는 기법으로 한번 쫘악 보시고 다시 필요한 부분을 또한 번 보고 이렇게 함으로써 이해가 되는 것입니다. 지금까지 공부하실 때 “어떤 내용은 어느 Chapter에 있더라” 이것만 알아도 됩니다. 이렇게 알면서 뒤의것을 공부하시면 앞에 것을 또한번 이해할수 있습니다. 그리고 책을 다보신 후에(3편까지만 보아도 됩니다) 자신이 한번 프로그램을 만들어 보세요. 그리고 막히는 부분이 있다면 “아! 책 어느부분에 있구나” 하시고 그부분을 다시 보시던지 그래도 모르면 그냥 예제를 부분 카피 하셔서 사용해도 됩니다. 윈도우 프로그래밍은 사실 언어만 이해하면 이론적으로 공부하는 부분은 작고 그냥 시키는대로 하는게 더많습니다. 즉 어떻게 해야 하는가가 많다는 이야기입니다. 그것은 기술력에 해당되지 않습니다. 노우하우 (어떤면에서는 노우웨어 (Know Where)에 해당되는 것입니다. 앞으로 본책의 뒤로가면서 이론적 부분이 점점 사라지고 “이렇게 하면 됩니다” 라는 말이 많이 나올것입니다. 이게 사실이기 때문입니다. 왜냐면 마이크로 소프트사에서 주는 라이브러리를 사용하여 프로그램을 짜기 때문에 그들이 제공하는 라이브러리 함수를 순서에 맞게 설정하는 것 외에는 그렇다할 알고리듬이 없기 때문입니다. SDK라이브러리에 어떤 기술력이 있다고 괸히 어려운 함수 쓸려고 노력하시는 분들에게 한마디 하고 싶습니다. “그시간에 알고리듬 공부를 더하세요!”
(잔소리끝)
[출처] [펌] 키보드 이벤트 처리하기|작성자 PeterFish153
'Computer > C' 카테고리의 다른 글
멀티바이트와 유니코드 (0) | 2013.07.27 |
---|---|
BroadCasting 참고 코드 (0) | 2013.07.09 |
Virtual Key Code (0) | 2013.07.06 |
Visual Studio 2012 단축키 (0) | 2013.07.05 |
소켓통신 (0) | 2013.07.05 |