1. Visual Studio 프로젝트 설정

  • Visual Studio 프로젝트 설정의 Linker > System에서 SubSystem을 Windows (/SUBSYSTEM:WINDOWS)로 변경

  • 기존 설정(Console (/SUBSYSTEM:CONSOLE))은 진입 함수가 main()으로, 일반적인 CLI 프로그램이지만 이를 Windows로 바꾸면 진입 함수가 WinMain()로 바뀌면서 GUI 애플리케이션 진입 모델로 바뀜

  • 이는 프로젝트를 콘솔 창 없는 순수 GUI 앱 개발 형태로 바꾸어주며, 메시지 루프(Message Loop)가 메인 프로그램 구조가 됨

2. 진입 함수

int WINAPI WinMain(
    HINSTANCE hInstance,
    HINSTANCE hPrevInstance,
    LPSTR lpCmdLine,
    int nShowCmd
)
{
    return 0;
}
  1. WINAPI: Windows API가 기대하는 함수 호출 규약

  2. HINSTANCE hInstance: 현재 실행 중인 프로그램(또는 모듈)의 핸들

  3. HINSTANCE hPrevInstance: 이전 인스턴스의 핸들

    • 과거 16bit Windows 시절 유물이라 현대 Windows에서는 항상 nullptr
  4. LPSTR lpCmdLine: 명령행 인자의 문자열

    • int main(int argc, char** argv)argv와 대응되는 개념으로, 프로그램 이름(.exe 파일 경로)은 제외된 문자열

    • 파싱은 직접 해야함

  5. int nShowCmd: 윈도우를 처음 띄울 때의 표시 상태

    • SW_SHOW: 일반 표시

    • SW_SHOWMAXIMIZED: 최대화

    • SW_SHOWMINIMIZED: 최소화

    • SW_HIDE: 숨김

    • 사용자가 바로 전에 창을 특정 상태로 종료한다면 다음 실행 시 그 상태를 OS가 기억하여 전달함

3. Window Procedure(메시지 처리기)

  • Win32 GUI 프로그램에서 가장 핵심적인 콜백 함수

    • OS가 이 창에 무슨 일이 생겼다고 알려줄 때마다 호출되는, 이벤트가 들어오는 공식 경로

    • 키보드, 마우스, 창 크기 변경, 종료 요청 등 모든 윈도우 이벤트가 메시지로 전달되며, Win32 프로그램에서 이 함수가 없으면 창은 사실상 아무 것도 못 함

LRESULT CALLBACK WndProc(
    HWND hWnd,
    UINT message,
    WPARAM wParam,
    LPARAM lParam
)
{
    ...;
}
  1. LRESULT: 메시지 처리 결과를 OS에 돌려주는 반환 값

    • 프로그램에서 처리하면 보통 0

    • 기본 처리 맡기면 DefWindowProc()(Windows 기본 처리 루틴)의 반환값

  2. CALLBACK: Windows API가 기대하는 호출 규약

  3. HWND hWnd: 메시지가 발생한 윈도우의 핸들

    • 멀티 윈도우 앱이면 어떤 창인지 구분하는 용도
  4. UINT message: 무슨 일이 일어났는지 나타내는 메시지 ID

    • WM_PAINT: 다시 그려라

    • WM_KEYDOWN: 키가 눌렸다

    • WM_SIZE: 창 크기 변경됨

    • WM_DESTROY: 창이 파괴됨

  5. WPARAM wParam: 메시지에 따른 부가 정보(작은 의미 값)

    • WM_KEYDOWN의 경우 눌린 키의 코드

    • WM_SIZE의 경우 최소화 / 최대화 여부

  6. LPARAM lParam: 메시지에 따른 부가 정보(구체적인 데이터)

    • WM_MOUSEMOVE: 마우스 좌표

    • WM_SIZE: 새 가로 / 세로 크기

  • 메시지 처리 분기 내

    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    
    • 우리가 처리하지 않은 메시지는 Windows 기본 처리 루틴에게 위임

4. Window Class(윈도우 클래스)

  • Win32에서 창을 하나 만들 때 OS는 각종 정보를 필요로 함

    • 이 창의 메시지를 누가 처리할 것인가(WndProc())

    • 기본 배경색

    • 기본 커서

    • 아이콘

    • 창의 종류

  • 이런 정보들을 윈도우 클래스라는 단위로 한 번 정의

    WNDCLASSW wndclass = {
        0, // style
        WndProc, // lpfnWndProc
        0, // cbClsExtra
        0, // cbWndExtra
        0, // hInstance
        0, // hIcon
        0, // hCursor
        0, // hbrBackground
        0, // lpszMenuName
        WindowClass // lpszClassName
    };
    
  • 이런 형태의 창이 있음을 등록: RegisterClassW(&wndclass)

  • 이후 윈도우 클래스를 이용하여 창 생성

    HWND CreateWindowExW( // 창 생성에 성공하면 유효한 윈도우 핸들 반환
        DWORD dwExStyle, // 확장(Ex 함수) 윈도우 스타일
        LPCWSTR lpClassName, // 사용할 윈도우 클래스 이름
        LPCWSTR lpWindowName, // 타이틀바에 표시될 문자열
        DWORD dwStyle, // 창의 모양과 동작을 정의하는 플래그
        int X, // 창 가로 위치
        int Y, // 창 세로 위치
        int nWidth, // 초기 클라이언트 영역 너비
        int nHeight, // 초기 클라이언트 영역 높이
        HWND hWndParent, // 부모 윈도우
        HMENU hMenu, // 메뉴 핸들
        HINSTANCE hInstance, // 프로그램 인스턴스 핸들(WinMain에서 받은 것)
        LPVOID lpParam // 추가 사용자 데이터
    );
    

유니코드 문자열

  • 코드 내에 사용되는 WCHAR나 함수 이름 뒤의 W는 유니코드(Wide, 16bit)를 사용함을 의미하며, 여기에 문자열을 사용할 때는 일반적인 문자열을 사용하면 안되고, 유니코드 문자열 리터럴(L"...")을 사용해야 함

  • 반면 ANSI 버전(A가 붙음)은 그냥 문자열로 써도 되지만, 왠만하면 W 형태로 통일하는 것이 좋음

5. Message Loop(메시지 루프)

  • 보통 메인 루프 내에서 동작
while (bIsExit == false) // 메인 루프
{
    MSG msg;

    while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) // 메시지 루프
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);

        if (msg.message == WM_QUIT)
        {
            bIsExit = true;
            break;
        }
    }

    // 매번 실행되는 로직
}

// 소멸 로직
  1. PeekMessage(): 윈도우 메시지가 있으면 가져오고(MSG msg에 저장), 없으면 즉시 false 반환

    • 게임 / 그래픽 앱에서 보통 사용하며, 전통적인 GUI 앱에서 사용하는 GetMessage()는 메시지가 올 때까지 프로세스를 블록함

    • 마지막 인자의 PM_REMOVE는 메시지를 읽은 후 큐에서 제거한다는 의미이며, 없으면 루프에서 같은 메시지를 계속 읽는 무한 루프에 빠짐

  2. TranslateMessage(): 키보드 메시지를 문자 메시지로 변환

    • 키보드 입력 메시지를 가공하여 WM_CHAR 생성

    • 텍스트 입력이 필요한 경우에는 필수이며, 마우스 메시지에는 영향 없음

  3. DispatchMessage(): 해당 윈도우의 WndProc(hWnd, msg.message, msg.wParam, msg.lParam)을 호출

WM_QUIT

  • PostQuitMessage()로 생성되는 윈도우 메시지

  • WndProc로 전달되지 않으며, 메시지 큐에서만 처리되기 때문에 이 윈도우 메시지를 통해 메인 루프를 해제할 수 있음