안녕하세요. 언제나휴일입니다.
이번에는 윈도우 클래스를 등록하여 윈도우 개체를 생성하는 형태의 윈도우즈 응용 프로그램을 만들어 보기로 합시다. 이번 프로그램을 통해 윈도우즈 프로그램의 기본적인 동작 흐름을 파악할 수 있습니다.
윈도우즈 프로그램에서 자신의 원하는 형태로 창을 만들기 위해서는 윈도우 클래스를 등록한 후에 등록한 윈도우 클래스 형태의 인스턴스를 생성하는 것이 필요합니다. 윈도우즈 API에서는 비슷한 동작을 하는 버튼이나 리스트 박스 등을 만들 때 미리 등록해 놓은 윈도우 클래스를 이용하여 만들게 하고 있습니다.
다음은 이번에 작성할 윈도우즈 프로그램의 기본 흐름입니다.
제일 먼저 윈도우 클래스 속성을 설정합니다. 윈도우 클래스 속성은 WNDCLASS 형식을 이용합니다.
struct WNDCLASS{ UINT style; //윈도우 클래스 스타일 WNDPROC lpfnWndProc; //콜백 프로시저 int cbClsExtra; //윈도우 클래스의 여분 메모리 int cbWndExtra; //윈도우 인스턴스의 여분 메모리 HINSTANCE hInstance; //윈도우 클래스를 등록하는 모듈의 인스턴스 핸들 HICON hIcon; //아이콘 핸들 HCURSOR hCursor; //커서 핸들 HBRUSH hbrBackground; //배경 브러쉬 LPCWSTR lpszMenuName; //메뉴 이름 LPCWSTR lpszClassName; //클래스 이름 };
WNDCLASS 형식의 주요 멤버를 살펴봅시다.
- UINT style; //윈도우 클래스 스타일
WNDCLASS의 style은 윈도우 클래스의 스타일을 지정하는 멤버입니다. 창의 폭과 넓이가 변할 때 화면 영역을 모두 다시 그리게 설정하는 CS_VREDRAW, CS_HREDRAW와 마우스 더블 클릭 메시지를 발생할 수 있게 설정하는 CS_DBLCLKS 등이 있습니다.
#define CS_VREDRAW 0x0001 #define CS_HREDRAW 0x0002 #define CS_DBLCLKS 0x0008 #define CS_OWNDC 0x0020 #define CS_CLASSDC 0x0040 #define CS_PARENTDC 0x0080 #define CS_NOCLOSE 0x0200 #define CS_SAVEBITS 0x0800 #define CS_BYTEALIGNCLIENT 0x1000 #define CS_BYTEALIGNWINDOW 0x2000 #define CS_GLOBALCLASS 0x4000
- WNDPROClpfnWndProc; //콜백 프로시저
lpfnWndProc 멤버는 윈도우 인스턴스에 발생한 메시지를 처리할 윈도우 콜백 프로시저입니다. 마우스 클릭, 키보드 클릭 등의 사건을 발생하면 운영체제는 메시지를 처리할 프로세스의 응용 메시지 큐에 보내거나 윈도우 콜백 프로시저를 호출합니다. 따라서 윈도우 인스턴스에 관한 메시지 처리를 위한 프로시저를 윈도우 클래스를 등록할 때 WNDCLASS의 멤버로 정보를 설정해야 합니다. 다음은 WinUser.h 파일에 윈도우 콜백 프로시저의 함수 포인터 형식을 타입 재지정한 구문입니다.
- typedef LRESULT (CALLBACK* WNDPROC)(HWND, UINT, WPARAM, LPARAM);
윈도우 프로시저는 입력 매개 변수가 4개입니다. 첫 번째 인자는 처리할 윈도우 메시지와 관련있는 윈도우 인스턴스의 핸들입니다. 두 번째 인자는 발생한 윈도우 메시지 ID입니다. 세 번째 인자와 네 번째 인자는 메시지 처리에 필요한 메타 정보이며 윈도우 메시지 종류에 따라 실제 전달하는 정보는 다릅니다.
- HINSTANCEhInstance; //윈도우 클래스를 등록하는 모듈의 인스턴스 핸들
hInstance는 윈도우 클래스를 등록하는 모듈의 인스턴스 핸들입니다. 운영체제에서는 누가 등록하였는지 기억하고 있다가 해당 모듈이 사라질 때 등록한 윈도우 클래스도 해제합니다.
- HICONhIcon; //아이콘 핸들
hIcon 멤버는 윈도우 인스턴스의 좌측 상단 모서리에 보이는 이미지에 관한 핸들입니다.
- HCURSORhCursor; //커서 핸들
hCursor 멤버는 윈도우 인스턴스에 마우스 좌표가 위치할 때 보이는 커서에 관한 핸들입니다.
- HBRUSHhbrBackground; //배경 브러쉬
hbrBackground 멤버는 윈도우 인스턴스의 배경을 채우는 브러쉬 핸들입니다.
- LPCWSTRlpszMenuName; //메뉴 이름
lpszMenuName은 메뉴 핸들을 문자열로 변형한 값입니다.
- LPCWSTRlpszClassName; //클래스 이름
등록한 윈도우 클래스의 구분자인 클래스 이름입니다.
WNDCLASS wndclass={0}; wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);//흰색 브러쉬 핸들 wndclass.hCursor = LoadCursor(0,IDC_ARROW); //마우스 커서 핸들 wndclass.hIcon = LoadIcon(0,IDI_APPLICATION); //아이콘 핸들 wndclass.hInstance = GetModuleHandle(0);//자신 모듈의 인스턴스 핸들 wndclass.lpfnWndProc = MyWndProc;//윈도우 콜백 프로시저 wndclass.lpszClassName = MY_FIRST_WND;//클래스 이름 - 클래스 구분자 wndclass.style = CS_DBLCLKS;//클래스 종류
이와 같이 윈도우 클래스를 설정한 후에 등록할 때는 RegisterClass를 호출합니다.
ATOM WINAPI RegisterClass(WNDCLASS *lpWndClass);
입력 인자로 윈도우 클래스의 속성을 설정한 WNDCLASS 형식 변수의 주소를 전달하면 결과를 반환합니다. 성공하면 0이 아닌 값을 반환하고 실패하면 0을 반환합니다.
윈도우 인스턴스를 생성할 때는 등록 상태의 윈도우 클래스를 이용합니다. 여기에서 말하는 윈도우 인스턴스는 화면에 보이는 사각 창인 윈도우와 이를 위해 프로그램에서 관리해야 하는 정보를 총괄하는 의미입니다. Windows API에서는 CreateWindow 함수를 호출하여 윈도우 인스턴스를 생성할 수 있습니다.
HWND WINAPI CreateWindow(LPCWSTR lpClassName,//윈도우 클래스 이름 LPCWSTR lpWindowName,//윈도우 인스턴스 캡션 명(타이틀) DWORD dwStyle, //윈도우 스타일 int X, //좌상단 x좌표 int Y, //좌상단 y좌표 int nWidth, //너비 int nHeight, //높이 HWND hWndParent, //부모 윈도우 핸들 HMENU hMenu, //메뉴 핸들 HINSTANCE hInstance, //모듈의 인스턴스 핸들 LPVOID lpParam);//생성할 때 전달할 인자
첫 번째 인자로 윈도우 클래스 이름을 사용하며 두 번재 인자로 창의 타이틀 부분에 출력할 캡션 명을 전달합니다. 세 번째 인자로 윈도우 스타일을 전달합니다. 그리고 창의 좌상단 x,y 좌표와 창의 너비와 높이를 전달합니다. 이 외에 버튼 같은 컨트롤처럼 다른 윈도우의 클라이언트 영역에 배치할 때는 부모 윈도우 핸들을 전달하며 이 외에 메뉴 핸들과 모듈의 인스턴스 핸들, 생성할 때 전달할 인자가 있습니다.
모듈의 인스턴스 핸들은 프로세스 자신의 인스턴스 핸들로 운영체제에게 누가 생성을 요청하는 것인지 구분하기 위해 전달하는 것입니다.
그런데 이렇게 윈도우 인스턴스를 생성한다고 바로 화면에 보이는 것은 아닙니다. 화면에 시각화하려면 ShowWindow 호출이 필요합니다.
ShowWindow(hWnd,nShow);//윈도우 인스턴스 시각화, SW_SHOW(시각화), SW_HIDE(비시각화)
윈도우즈 프로그램은 최종 사용자에 의해 컨트롤이나 마우스 누름, 키 누름 등의 동작에 따라 동작합니다. 이러한 동작을 하면 운영체제는 이를 감지하여 프로세스의 응용 메시지 큐에 메시지를 전달합니다. 응용에서는 응용 메시지 큐에 도착한 메시지를 하나씩 꺼내어 콜백 프로시저에서 처리하는 구조입니다.
MSG Message ; while(GetMessage(&Message,0,0,0))//메시지 루프에서 메시지 꺼냄(WM_QUIT이면 FALSE 반환) { TranslateMessage(&Message);//WM_KEYDOWN이고 키가 문자 키일 때 WM_CHAR 발생 DispatchMessage(&Message);//콜백 프로시저가 수행할 수 있게 디스패치 시킴 }
먼저 윈도우 메시지 형식인 MSG 구조체를 살펴봅시다.
struct MSG { HWND hwnd;//사건과 관련있는 윈도우 핸들 UINT message;//메시지 구분자 WPARAM wParam; //메시지 처리에 필요한 인자1 LPARAM lParam; //메시지 처리에 필요한 인자2 DWORD time; //사건이 발생한 시각 POINT pt; //사건이 발생할 때의 마우스 좌표 };
MSG 구조체에는 사건과 관련있는 윈도우 핸들과 메시지 구분자가 있습니다. 그리고 메시지 처리에 필요한 인자가 wParam과 lParam으로 있고 이 외에 사건이 발생한 시각과 마우스 좌표를 갖고 있습니다.
메시지 루프에서 제일 먼저 할 일은 메시지 큐에 도착한 메시지를 꺼내는 작업입니다. GetMessage 호출로 메시지 큐에 도착한 메시지를 꺼낼 수 있습니다.
BOOL WINAPI GetMessage(LPMSG lpMsg,HWND hWnd,UINT wMsgFilterMin,UINT wMsgFilterMax);
GetMessage함수에서는 첫 번째 인자로 전달한 주소에 메시지를 꺼내어 설정합니다. 그리고 두 번째 인자부터 네 번째 인자는 원하는 메시지를 필터링할 때 사용합니다. 특정 윈도우에 발생한 메시지만 꺼내기를 원하면 두 번째 인자를 유효한 윈도우 핸들을 전달하세요. 그리고 특정 메시지만 꺼내기를 원하면 세 번째와 네 번째 인자로 꺼내고자 하는 메시지 번호의 구간을 전달합니다.
주의할 사항으로는 GetMessage는 메시지 큐에 메시지가 없을 때 FALSE를 반환하는 것이 아니라 WM_QUIT 메시지를 꺼낼 때 FALSE를 반환한다는 것입니다. 윈도우즈 프로그램에서 처리할 메시지가 없다고 프로세스를 종료하는 것이 아니라 종료 메시지를 수신하였을 때 종료하게 처리합니다.
그리고 프로그램 방식으로 WM_QUIT 메시지를 메시지 큐에 전달할 때는 PostQuitMessage를 호출합니다.
다음은 앞에서 설명한 내용으로 비어있는 창을 띄우는 예제 코드입니다.
#include <Windows.h> #define MY_FIRST_WND (TEXT("myfirstwnd")) void RegWindowClass(); void MessageLoop(); INT APIENTRY WinMain(HINSTANCE hIns, HINSTANCE hPrev, LPSTR cmd, INT nShow) { RegWindowClass();//윈도우 클래스 속성 설정 및 등록 //윈도우 인스턴스 생성 HWND hWnd = CreateWindow(MY_FIRST_WND,//클래스 이름 TEXT("테스트"), //캡션 명 WS_OVERLAPPEDWINDOW, //윈도우 스타일 10,10,1000,800,//좌,상,폭,높이 0,//부모 윈도우 핸들 0,//메뉴 핸들 hIns,//인스턴스 핸들 0);//생성 시 인자 ShowWindow(hWnd,nShow);//윈도우 인스턴스 시각화, SW_SHOW(시각화), SW_HIDE(비시각화) MessageLoop();//메시지 루프 return 0; } void OnDestroy(HWND hWnd) { PostQuitMessage(0);//메시지 큐에 WM_QUIT 메시지를 붙임 } LRESULT CALLBACK MyWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) { switch(iMessage) { case WM_DESTROY: OnDestroy(hWnd); return 0; } return DefWindowProc(hWnd,iMessage,wParam,lParam); } void RegWindowClass() { WNDCLASS wndclass={0}; wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);//흰색 브러쉬 핸들 wndclass.hCursor = LoadCursor(0,IDC_ARROW); //마우스 커서 핸들 wndclass.hIcon = LoadIcon(0,IDI_APPLICATION); //아이콘 핸들 wndclass.hInstance = GetModuleHandle(0);//자신 모듈의 인스턴스 핸들 wndclass.lpfnWndProc = MyWndProc;//윈도우 콜백 프로시저 wndclass.lpszClassName = MY_FIRST_WND;//클래스 이름 - 클래스 구분자 wndclass.style = CS_DBLCLKS;//클래스 종류 RegisterClass(&wndclass); } void MessageLoop() { MSG Message; while(GetMessage(&Message,0,0,0))//메시지 루프에서 메시지 꺼냄(WM_QUIT이면 FALSE 반환) { TranslateMessage(&Message);//WM_KEYDOWN이고 키가 문자 키일 때 WM_CHAR 발생 DispatchMessage(&Message);//콜백 프로시저가 수행할 수 있게 디스패치 시킴 } }
다른 부분은 앞에서 설명한 내용을 코드로 반영한 것입니다. 여기에서는 콜백 부분을 설명할게요.
void OnDestroy(HWND hWnd) { PostQuitMessage(0);//메시지 큐에 WM_QUIT 메시지를 붙임 } LRESULT CALLBACK MyWndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) { switch(iMessage) { case WM_DESTROY: OnDestroy(hWnd); return 0; } return DefWindowProc(hWnd,iMessage,wParam,lParam); }
콜백으로 정의한 MyWndProc은 메시지 종류에 따라 각각의 메시지 처리기를 호출하게 구현하세요. 여기에서는 창을 닫을 때 응용 프로그램을 종료시킬 수 있게 WM_DESTROY 메시지 처리기를 만들어 사용하고 있습니다.
윈도우 메시지는 WM_으로 시작합니다. WM_DESTROY는 창을 닫을 때 응용 프로그램에서 창에 할당한 자원을 해제하거나 후처리 작업을 할 수 있게 전달하는 윈도우 메시지입니다. 메시지 종류에 따라 wParam, lParam의 의미는 다르며 새로운 윈도우 메시지를 접할 때마다 이를 기록해 두세요.
MyWndProc의 마지막에 DefWindowProc을 호출하는 이유는 공통적인 처리를 하기 위해서입니다. Windows API에서는 윈도우의 공통적인 동작을 처리하는 DefWindowProc 함수를 제공하고 있습니다. 창의 타이틀 바에 마우스를 클릭하여 드래그하여 이동하거나 창의 크기를 축소, 확대하는 등의 작업은 공통적인 작업입니다. 이런 작업을 모든 개발자가 코드를 구현하는 것은 비용 낭비이며 이를 위해 제공하는 것입니다.
참고로 대부분의 윈도우즈 응용에서는 클라이언트를 제외한 나머지 영역인 NC(Non Client, 비 클라이언트) 영역에 발생한 처리는 DefWindowProc에서 처리하는 것일 일반적입니다.