API

[팁] 일반 윈도우에서 다이얼로그박스의 편리한 키보드 인터페이스 구현하기

디버그정 2009. 10. 15. 18:31
[팁] 일반 윈도우에서 다이얼로그박스의 편리한 키보드 인터페이스 구현하기

작성자 강성엽 (IP:220.78.3.178)
작성일 2004년 11월08일 20시 10분(조회수:5067)
내용 다이얼로그(특히 모달형 다이얼로그)박스를 만들어보신 분이라면
그 편리한 키보드 인터페이스에 대해 부러움을 가져보셨을 겁니다.

TAB 키를 누르면 컨트롤들 사이에서 포커스를 전환할 수 있고,
캐럿을 가지지 않는 컨트롤이라면 방향키로도 포커스 전환이 되고,
ShortCut을 이용해 한 번에 포커스를 옮길 수 도 있으며,
Default버튼은 포커스를 위치시킬 필요 없이 Enter 키만 눌러도
클릭이 되고, ESC 키를 누르면 WM_COMMAND 메시지와함께
IDCANCEL 코드가 넘어와서, 대화상자를 닫는 등의 작업을
시킬 수 있으니 정말 배가 아플 정도로 부럽지요.

그런데, 우리가 직접 CreateWindow로 만든 윈도우랑 컨트롤들은
어떻습니까? 암만 WS_TABSTOP이나 BS_DEFPUSHBUTTON 스타일을
줘도 TAB키, 방향키, Enter 키, ESC 키 꿈쩍도 하지 않지요.
물론 이들 기능을 일일이 WndProc 안에다 구현해 넣으면
되겠지만, 보통 힘든 일이 아닌데다가, 차일드 컨트롤의 수가 많아지면
많아질수록 여간 코드가 길어지고 성가신 일이 아닐 수 없지요.

직접 만든 윈도에서도 이와같은 편리한 키보드 인터페이스를 별도의
코딩 없이 구현하기위해 이리저리 찾아 헤맨 끝에 드디어 좋은 방법을
발견했습니다. 바로 while(GetMessage()) { } 루프 내에다 IsDialogMessage()
함수를 사용해서 키보드 메시지를 다이얼로그 인터페이스용 메시지로
바꾸어주는 것이지요.

이 함수의 사용법은 TranslateAccelerator()와 마찬가지로 TRUE가
리턴됐을 경우에는 나머지 메시지 처리(Translate, Disptch)를 하지
않고 다시 GetMessage로 돌아가야 합니다.
while(GetMessage(&msg, NULL, 0, 0)) {
if (!IsDialogMessage(hWnd, &msg)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}

 
그런데 메시지 처리 루프에 IsDialogMessage()를 넣는 것 만으로는
부족한 것이 있습니다. 위와 같이 하면 TAB키, 방향키, ESC 키, Enter 키,
ShortCut은 잘 작동하지만, Default Pushbutton 스타일이 유지가 되지
않아서 디폴트 버튼일지라도 포커스를 뺐기면 더이상 디폴트 버튼으로서의
기능을 상실하게 됩니다. 따라서 Enter 키가 의미 없게 되어 버리지요.

완벽한 다이얼로그박스를 흉내내려면 WndProc의 디폴트 메시지 처리
함수를 DefWindowProc()에서 DefDlgProc()으로 바꾸어줍니다.
인수는 DefWindowProc와 완전히 동일합니다.
단, 이 함수를 WndProc 내에 넣을 때 몇 가지 주의해야 할 것이 있습니다.
이 함수는 윈도의 여분 메모리를 이용해서 작동하므로 클래스 선언할 때
반드시 cbWndExtra 멤버에 DLGWINDOWEXTRA 값을 넣어주어야 합니다.
(확인해보니 이 값은 30이더군요. 그냥 DLGWINDOWEXTRA 상수 쓰셔도 되구요)
또, 이 함수는 WM_CLOSE 메시지를 처리해주지 않으므로 사용자가 직접
WM_CLOSE 메시지에 대한 코드를 작성해주어야 합니다.
보통 DestroyWindow(hWnd);를 호출하면 되지요.

만약 기존에 하던대로 cbWndExtra를 0으로 방치해두면 잘못된 연산 수행
오류가 발생합니다.

다음으로, 만약 디폴트 버튼을 두고 싶으면 상식적으로는 해당 버튼에
BS_DEFPUSHBUTTON 스타일을 주면 되겠거니 생각하겠지만,
이 스타일을 주지 말고, 대신 메인 윈도우로 DM_SETDEFID 메시지를
디폴트 버튼을 삼을 버튼의 ID와 함께 넘겨주면 DefDlgProc()이 알아서
디폴트 버튼으로 만들어주며, 이 버튼이 포커스를 뺐기더라도 디폴트버튼
스타일을 유지시켜줍니다.

여기까지 마치면 완벽하게 DialogBox와 동일한 키보드 인터페이스를 갖는
자신만의 Custom DialogBox가 만들어지는 것입니다.
Custom DialogBox의 또 하나의 장점(경우에 따라서는 단점이 될 수도
있지만, 확실히 대부분의 이들에게는 장점으로 작용하리라 봅니다)은
좌표 단위가 일반 윈도우의 좌표인 Pixel 그대로 유지되므로 DialogBox에서
골치아프게 계산/변환하던 DLU에 대해서 신경쓰지 않아도 되는 것입니다.

아래에 간단한 예제 소스를 첨부합니다.
기존에 프로그램하던 방식과 거의 같습니다만 위에서 언급한 약간의 차이가
있지요. 눈여겨 봐둘만한 소스가 될겁니다.

---------------------------------------------------------------------
#include <windows.h>

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
TCHAR szClsName[] = "DLGTST",
szAppName[] = "Dialog Test";

int APIENTRY WinMain(HINSTANCE hInst, HINSTANCE hPrevInst, LPSTR CmdLine, int CmdShow) {
HWND hWnd;
MSG msg;
WNDCLASSEX wc;

g_hInst = hInst;
wc.cbSize = sizeof(WNDCLASSEX);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 30;
wc.hInstance = hInst;
wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor(NULL, IDC_ARROW);
wc.hbrBackground = (HBRUSH) (COLOR_BTNFACE + 1);
wc.lpszMenuName = NULL;
wc.lpszClassName = szClsName;
wc.hIconSm = NULL;
RegisterClassEx(&wc);

hWnd = CreateWindowEx(NULL, szClsName, szAppName, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 320, 120,
NULL, NULL, hInst, NULL);
ShowWindow(hWnd, CmdShow);
UpdateWindow(hWnd);

while(GetMessage(&msg, NULL, 0, 0)) {
if (!IsDialogMessage(hWnd, &msg)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}

return msg.wParam;
}

LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch(uMsg) {
case WM_CREATE:
// Edit 1
CreateWindowEx(NULL, "STATIC", "&Edit1", WS_CHILD | WS_VISIBLE,
8, 8, 240, 21, hWnd, (HMENU) -1, g_hInst, NULL);
CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", "", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
64, 8, 240, 21, hWnd, (HMENU) 8000, g_hInst, NULL);

// Edit 2
CreateWindowEx(NULL, "STATIC", "E&dit2", WS_CHILD | WS_VISIBLE,
8, 36, 240, 21, hWnd, (HMENU) -1, g_hInst, NULL);
CreateWindowEx(WS_EX_CLIENTEDGE, "EDIT", "", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
64, 36, 240, 21, hWnd, (HMENU) 8001, g_hInst, NULL);

// Button 1
CreateWindowEx(NULL, "BUTTON", "&Button1", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
94, 64, 100, 21, hWnd, (HMENU) 8002, g_hInst, NULL);
// Button 2
CreateWindowEx(NULL, "BUTTON", "B&utton2", WS_CHILD | WS_VISIBLE | WS_TABSTOP,
204, 64, 100, 21, hWnd, (HMENU) 8003, g_hInst, NULL);

// Button 1을 Default button으로...
SendMessage(hWnd, DM_SETDEFID, 8002, 0);
// Edit 1에 Focus를...
SetFocus(GetDlgItem(hWnd, 8000));
return 0;

case WM_CLOSE:
DestroyWindow(hWnd);
return 0;


case WM_DESTROY:
PostQuitMessage(0);
return 0;

case WM_COMMAND:
switch(LOWORD(wParam)) {
case 8002:
MessageBox(hWnd, "Button1", szAppName, MB_ICONINFORMATION);
break;
case 8003:
MessageBox(hWnd, "Button2", szAppName, MB_ICONINFORMATION);
break;
case IDCANCEL:
MessageBox(hWnd, "ESC key pressed", szAppName, MB_ICONINFORMATION);
break;
}
return 0;
}

return DefDlgProc(hWnd, uMsg, wParam, lParam);
}



winapi.co.kr의 자유강좌란에서 퍼왔다.
와우 정말 굿굿굿 팁~~~~