상세 컨텐츠

본문 제목

[WinAPI] Window Project (Window, Handle, Message)

Win32 API

by 이나시오- 2024. 8. 8. 02:17

본문

 Window는 Windows 운영체제의 GUI에서 가장 기본이 되는 개념이다. Windows에서 실행되는 프로세스는 window를 가질 수도 있고, 가지지 않을 수도 있다. Window를 가지는 경우 프로그램이 실행되면서 window가 켜지고, window를 끄면 프로그램이 종료되므로 프로그램 = window로 착각할 수 있지만, 실제로는 별개이고, 프로그램이 실행되면 window를 켜고 window를 끄면 프로그램이 종료되도록 설계된 프로그램일 뿐이다. 

 

1. Window 생성

 Window를 만들기 위해서는 window class를 등록해야 한다. Window class란 window의 정보를 의미하며 다음과 같은 멤버를 갖는 WNDCLASSEXW 타입의 구조체를 정의한다.

WNDCLASSEXW wcex{};		// 윈도우 클래스를 생성

wcex.cbSize = sizeof(WNDCLASSEX);

wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.cbClsExtra = 0;
wcex.cbWndExtra = 0;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_CLIENT));
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_CLIENT);
wcex.lpszClassName = L"WINDOW_KEY";
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

RegisterClassExW(&wcex);	// 윈도우 클래스를 등록

// 윈도우 생성
HWND hWnd = CreateWindowW(L"WINDOW_KEY", L"My Game", WS_OVERLAPPEDWINDOW,
        CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
        
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);

style : window의 스타일. CS_HREDRAW/CS_VREDRAW : 창의 너비/높이 변경 시 창을 다시 그림

lpfnWndProc : window에 등록될 프로시저 포인터 (window의 메세지 처리 함수)

hInstance : 인스턴스 핸들, 운영체제에서 애플리케이션에 부여한 고유 식별자를 의미. wWinMain함수가 실행될 때 전달받음.

hIcon : 아이콘 핸들, 아이콘 리소스 아이디로 접근해서 작업관리자에 뜨는 아이콘을 설정

hCursor : 커서 핸들, 커서 리소스 아이디로 접근해서 커서를 설정

hbrBackground : 기본적으로 윈도우를 채울 브러쉬(브러쉬 핸들)를 설정

lpszMenuName : 윈도우 제목 아래줄에 달린 메뉴바를 리소스 아이디를 받아서 설정함. nullptr면 메뉴바 사라짐

lpszClassName : 이후 윈도우 생성할 때 이 window class를 연결할 key값

hIconSm : 아이콘 핸들, 윈도우 제목 옆에 뜨는 작은 아이콘을 설정

 

 이후 RegisterClassExW()를 이용해서 window class를 등록하고, CreateWindowW()에 window class의 classname을 전달해서 등록한 window class를 찾아서 해당 정보를 이용해서 window를 생성한다. Window가 잘 생성되면 HWND 타입의 윈도우 핸들을 반환한다.

 

 ShowWindow는 윈도우 핸들을 이용해서 윈도우를 화면에 표시하고, UpdateWindow는 window의 화면(client) 영역을 업데이트하는 기능이다 (WM_PAINT).

 

2. Handle

 여기서 핸들이라는 개념을 짚고 넘어가야 할 것 같다. 핸들은 윈도우 운영체제에서 커널 오브젝트에 접근할 수 있도록 제공하는 아이디 값이라고 할 수 있다. 커널 오브젝트란, 운영체제와 관련된 오브젝트로, 사용자가 직접 이에 접근할 수 있으면 예기치 못한 오류를 발생시킬 수 있기 때문에 운영체제를 거쳐서만 다룰 수 있다. 따라서 WinAPI에서는 커널 오브젝트에 직접 접근할 수 없고, 핸들이라는 일종의 ID 구조체를 사용해서 다룰 수 있도록 기능을 제공한다. 다양한 커널 오브젝트에 대한 핸들이 존재하고, int 하나로 구성된 구조체이다. 따라서 다른 핸들로 형변환이 가능하고 특정 핸들들을 하나로 묶는 상위 핸들이 존재하기도 한다.

HWND, HINSTANCE와 같이 타입 앞에 H가 붙어있는 관례가 존재한다. HWND는 윈도우를 다룰 수 있는 핸들, HINSTANCE는 프로그램을 다룰 수 있는 핸들이라고 생각하면 된다.

 

3. Message

 위에서 WM_PAINT라는 말이 잠시 언급되었는데, WM은 Window Message의 줄임말이다. Window는 message 기반으로 작동하며, message는 window가 수행할 특정 동작을 의미한다. Window마다 message queue라는 것이 존재해서, 조건에 따라 window message가 message queue에 들어간다. 그리고 윈도우 main 함수에서 while문을 돌면서 message를 하나씩 꺼내면서 message가 의미하는 동작을 차례로 수행하게 된다.

 

 기본적으로 GetMessage() 구조로 프로젝트가 작성된다. GetMessage()는 내부적으로 while문을 돌며 message queue를 반복적으로 확인하는데, 메세지가 존재하지 않는 경우 while문을 무한히 돌면서 함수를 빠져나오지 않는다. 그리고 message가 존재하는 경우 message를 꺼내서 MSG 구조체에 전달하며 true를 리턴한다. GetMessage()가 false를 리턴하는 조건이 한 가지 존재하는데, message queue에서 WM_QUIT 메세지를 꺼낸 경우이다. WM_QUIT은 윈도우를 종료하는 메세지이다. 그러면 윈도우 main 함수에서 돌던 반복문이 종료되고, WM_QUIT 동작을 수행하면서 window가 닫히고, 프로그램이 종료된다.

MSG msg;

while (GetMessage(&msg, nullptr, 0, 0))
{
    TranslateMessage(&msg);
    DispatchMessage(&msg);
}

 

 하지만 GetMessage()는 프로그램 작동 중 message queue에 메세지가 없는 시간이 훨씬 많기 때문에 게임을 만들기에 적합하지 않다. CPU를 더 효율적으로 사용하기 위해 PeekMessage()를 사용해야 한다. PeekMessage()는 message queue에 메세지가 존재하지 않아도 리턴하는 함수이다. 메세지가 존재하면 true, 메세지가 존재하지 않으면 false를 리턴한다. 따라서 게임을 설계할 때, 메세지가 존재하면 메세지를 꺼내서 처리하고, 메세지가 존재하지 않을 때 게임에 관련된 코드를 수행하면 된다. 대신 PeekMessage()에서 메세지를 꺼내보았을 때 WM_QUIT이면 종료하도록 추가적으로 구조를 바꿔줘야 한다.

MSG msg;

while (true)
{
    if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
    {
    	if (msg.message == WM_QUIT)
            break;
            
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
    else
    {
        // 게임 코드
    }
}