Direct3D-11 렌더링 엔진 만들기: 2-1 창(Window) 만들기

 

Direct3D-11 렌더링 엔진 만들기: 2-1 창(Window) 만들기

 

지난 포스팅을 통해 프로젝트를 구성했습니다.

이번에는 앞으로 Direct3D를 활용해 그림을 그릴 창을 생성합니다.

 

Application 프로젝트에 Main.cpp 파일 추가

먼저 실행 파일이 생성될 Application 프로젝트에 cpp 파일을 추가합니다.

추가하는 파일의 이름은 Main.cpp라고 지정합니다.

 

윈도우즈에서 C++로 창을 만들 때는 Win32 API를 사용합니다.

본 포스팅에서는 Win32 API에 대해서 자세히 다루지 않습니다. Direct3D에 집중하기 위해서 입니다.

Win32 API에 대해 궁금하시다면 MSDN 문서를 참고하시거나 “윈도우즈 시스템 프로그래밍” 관련 서적을 참고하시면 됩니다.

 

Win32 API를 사용해 창을 생성하는 기본적인 코드는 MSDN 문서에 잘 나와있습니다.

본 포스팅에서도 이를 기반으로 필요한 내용만 수정/추가하도록 하겠습니다.

 

구글에서 “msdn create window”라고 검색합니다.

검색하면 아래 링크와 같은 페이지를 볼 수 있습니다.

https://docs.microsoft.com/en-us/windows/win32/learnwin32/creating-a-window

글 마지막 부분에 최종 코드가 있습니다. 이 코드를 기본으로 약간 수정해 사용하겠습니다.

 

// Register the window class.
const wchar_t CLASS_NAME[]  = L"Sample Window Class";

WNDCLASS wc = { };

wc.lpfnWndProc   = WindowProc;
wc.hInstance     = hInstance;
wc.lpszClassName = CLASS_NAME;

RegisterClass(&wc);

// Create the window.

HWND hwnd = CreateWindowEx(
    0,                              // Optional window styles.
    CLASS_NAME,                     // Window class
    L"Learn to Program Windows",    // Window text
    WS_OVERLAPPEDWINDOW,            // Window style

    // Size and position
    CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,

    NULL,       // Parent window    
    NULL,       // Menu
    hInstance,  // Instance handle
    NULL        // Additional application data
    );

if (hwnd == NULL)
{
    return 0;
}

ShowWindow(hwnd, nCmdShow);

 

앞서 생성한 Main.cpp 파일을 선택하고 아래와 같이 코드를 작성합니다.

// Win32 API를 사용하기 위해 include.
#include <Windows.h>

// 창에서 메시지가 발생하면 호출될 함수.
LRESULT WINAPI WindowProc(HWND hwnd, unsigned int message, WPARAM wParam, LPARAM lParam)
{
    // 나머지 메시지는 윈도우 기본 설정으로 처리.
    return DefWindowProc(hwnd, message, wParam, lParam);
}

// 메인 함수(Entry Point).
int WINAPI WinMain(
    _In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPSTR lpCmdLine,
    _In_ int nShowCmd)
{
    // Window 클래스 등록.
    // 클래스 등록에 사용할 클래스 이름.
    const wchar_t* className = L"Sample Window Class";
    
    // 창 제목에 보여줄 타이틀.
    const wchar_t* windowTitle = L"Toy Render Engine";

    // 창 크기(가로/세로)
    unsigned int width = 1280;
    unsigned int height = 800;

    // 윈도우 클래스 구조체 설정.
    WNDCLASS wc = { };
    wc.lpfnWndProc = WindowProc;    // 창에서 발생한 메시지를 처리할 때 호출될 함수.
    wc.hInstance = hInstance;       // 프로그램 핸들 값 전달.
    wc.lpszClassName = className;   // 클래스 이름 설정.

    // 클래스 등록.
    if (RegisterClass(&wc) == false)
    {
        // 클래스 등록에 실패하면 프로그램 종료. (오류 코드 -1).
        exit(-1);
    }

    // 창 크기 조정.
    // 창을 생성할 때 타이틀 바 등이 차지하는 영역을 제외한 
    // 실제 그려지는 영역의 크기를 제대로 구하기 위한 코드.
    RECT rect = { 0, 0, static_cast<long>(width), static_cast<long>(height) };
    AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, false);
    
    // 실제 Direct3D를 활용해 그리는 공간을 1280/800으로 확보한 실제 창 크기 다시 구하기.
    int windowWidth = rect.right - rect.left;
    int windowHeight = rect.bottom - rect.top;

    // 창 위치 구하기.
    // 모니터의 가운데 위치 구하기.
    int xPosition = (GetSystemMetrics(SM_CXSCREEN) - windowWidth) / 2;
    int yPosition = (GetSystemMetrics(SM_CYSCREEN) - windowHeight) / 2;

    // 창 생성.
    HWND hwnd = CreateWindowEx(
        0,           // 윈도우 스타일(옵션).
        className,   // 윈도우 클래스 이름.
        windowTitle, // 제목에 보여줄 타이틀.
        WS_OVERLAPPEDWINDOW, // 창에 적용할 스타일.

        // 위치.
        xPosition, yPosition, 
        // 크기.
        windowWidth, windowHeight,

        nullptr,    // 부모 창.
        nullptr,    // 메뉴.
        hInstance,  // 프로그램 핸들 값.
        nullptr     // 추가로 전달할 어플리케이션 데이터.
    );

    // 창 생성에 실패한 경우, 프로그램 종료. (오류 코드 -1).
    if (hwnd == nullptr)
    {
        exit(-1);
    }

    // 창 보여주기.
    ShowWindow(hwnd, nShowCmd);

    // 업데이트 반영.
    UpdateWindow(hwnd);
}

 

이렇게 작성한 후에 F5를 눌러 실행해보면, 잠깐 실행되는 듯 하다가 바로 종료됩니다.

창을 사용해 무언가 작업을 하려면 “메시지”를 처리하는 루프(Loop)를 작성해야 합니다.

이를 위한 루프 코드를 WinMain 함수 맨 마지막에 추가해줍니다.

// 메시지 루프.
MSG msg = { 0 };
while (msg.message != WM_QUIT)
{
    // 창에서 메시지가 발생하는 지 확인.
    if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
    {
        // 메시지 변환.
        TranslateMessage(&msg);
        // 메시지 전달.
        DispatchMessage(&msg);
    }
    else
    {

    }
}

// 프로그램이 종료되면 클래스 등록 해제.
UnregisterClass(className, hInstance);

 

아래는 메시지 루프 코드를 추가한 Main.cpp의 전체 코드입니다.

// Win32 API를 사용하기 위해 include.
#include <Windows.h>

// 창에서 메시지가 발생하면 호출될 함수.
LRESULT WINAPI WindowProc(HWND hwnd, unsigned int message, WPARAM wParam, LPARAM lParam)
{
    // 나머지 메시지는 윈도우 기본 설정으로 처리.
    return DefWindowProc(hwnd, message, wParam, lParam);
}

// 메인 함수(Entry Point).
int WINAPI WinMain(
    _In_ HINSTANCE hInstance,
    _In_opt_ HINSTANCE hPrevInstance,
    _In_ LPSTR lpCmdLine,
    _In_ int nShowCmd)
{
    // Window 클래스 등록.
    // 클래스 등록에 사용할 클래스 이름.
    const wchar_t* className = L"Sample Window Class";
    
    // 창 제목에 보여줄 타이틀.
    const wchar_t* windowTitle = L"Toy Render Engine";

    // 창 크기(가로/세로)
    unsigned int width = 1280;
    unsigned int height = 800;

    // 윈도우 클래스 구조체 설정.
    WNDCLASS wc = { };
    wc.lpfnWndProc = WindowProc;    // 창에서 발생한 메시지를 처리할 때 호출될 함수.
    wc.hInstance = hInstance;       // 프로그램 핸들 값 전달.
    wc.lpszClassName = className;   // 클래스 이름 설정.

    // 클래스 등록.
    if (RegisterClass(&wc) == false)
    {
        // 클래스 등록에 실패하면 프로그램 종료. (오류 코드 -1).
        exit(-1);
    }

    // 창 크기 조정.
    // 창을 생성할 때 타이틀 바 등이 차지하는 영역을 제외한 
    // 실제 그려지는 영역의 크기를 제대로 구하기 위한 코드.
    RECT rect = { 0, 0, static_cast<long>(width), static_cast<long>(height) };
    AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, false);
    
    // 실제 Direct3D를 활용해 그리는 공간을 1280/800으로 확보한 실제 창 크기 다시 구하기.
    int windowWidth = rect.right - rect.left;
    int windowHeight = rect.bottom - rect.top;

    // 창 위치 구하기.
    // 모니터의 가운데 위치 구하기.
    int xPosition = (GetSystemMetrics(SM_CXSCREEN) - windowWidth) / 2;
    int yPosition = (GetSystemMetrics(SM_CYSCREEN) - windowHeight) / 2;

    // 창 생성.
    HWND hwnd = CreateWindowEx(
        0,           // 윈도우 스타일(옵션).
        className,   // 윈도우 클래스 이름.
        windowTitle, // 제목에 보여줄 타이틀.
        WS_OVERLAPPEDWINDOW, // 창에 적용할 스타일.

        // 위치.
        xPosition, yPosition, 
        // 크기.
        windowWidth, windowHeight,

        nullptr,    // 부모 창.
        nullptr,    // 메뉴.
        hInstance,  // 프로그램 핸들 값.
        nullptr     // 추가로 전달할 어플리케이션 데이터.
    );

    // 창 생성에 실패한 경우, 프로그램 종료. (오류 코드 -1).
    if (hwnd == nullptr)
    {
        exit(-1);
    }

    // 창 보여주기.
    ShowWindow(hwnd, nShowCmd);

    // 업데이트 반영.
    UpdateWindow(hwnd);
        
    // 메시지 루프.
    MSG msg = { 0 };
    while (msg.message != WM_QUIT)
    {
        // 창에서 메시지가 발생하는 지 확인.
        if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
        {
            // 메시지 변환.
            TranslateMessage(&msg);
            // 메시지 전달.
            DispatchMessage(&msg);
        }
        else
        {

        }
    }

    // 프로그램이 종료되면 클래스 등록 해제.
    UnregisterClass(className, hInstance);
}

 

실행 및 테스트

다시 F5키를 이용해 실행합니다.

이번에는 창이 나타나고 창 상단을 드래그하면 이동도 잘 되는 것을 볼 수 있습니다.

이제 창의 X 버튼을 눌러 창을 닫습니다.

그런데 이렇게 창을 닫았음에도 Visual Studio로 돌아가보면 프로그램이 종료되지 않은 것을 볼 수 있습니다.

 

일단 Visual Studio 상단에서 디버그 > 디버깅 중지 메뉴를 선택해 프로그램을 강제 종료합니다.

창을 닫아도 프로그램이 종료되지 않는 이유는 우리가 창을 닫았을 때 프로그램을 종료하라고 명령하지 않아서 입니다.

 

종료되지 않는 문제 해결하기

Win32 API는 메시지 기반으로 동작합니다.

위에서 While 루프로 구현한 메시지 루프에서 사용한 PeekMessage는 창에 어떤 메시지가 들어왔는지를 확인합니다.

메시지가 들어오면 메시지를 읽을 수 있는 형태로 변환(Translate)하고, 메시지 처리 함수에 전달(Dispatch)합니다.

창을 닫는다는 상황도 Win32는 “메시지”로 인식하고 이를 변환 후 WindowProc 함수에 전달합니다.

그런데 우리가 작성한 WindowProc 함수는 이에 대한 처리를 하지 않습니다.

 

WindowProc 함수를 찾아 아래와 같이 코드를 추가합니다.

// 창에서 메시지가 발생하면 호출될 함수.
LRESULT WINAPI WindowProc(HWND hwnd, unsigned int message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    // 창이 삭제되면 발생하는 메시지.
    case WM_DESTROY:
        // 프로그램 종료 메시지 발생시키기.
        PostQuitMessage(0);
        break;
    }

    // 나머지 메시지는 윈도우 기본 설정으로 처리.
    return DefWindowProc(hwnd, message, wParam, lParam);
}

 

먼저 메시지 타입 별로 구분해 처리하기 위해 switch 구문을 추가했습니다.

switch 구문에서는 WM_DESTROY 메시지에 대해 처리합니다.

WM_DESTROY 메시지는 창을 닫을 때 발생하는 메시지 타입입니다.

이때 PostQuitMessage(0);을 호출하고 있습니다.

PostQuitMessage 함수는 프로그램에 “종료”를 알리는 함수입니다.

파라미터로 전달된 0은 우리가 프로그램 종료할 때 return 0;을 주로 사용하는데 이때 return 문 뒤에 오는 숫자 값입니다.

 

코드 추가 후 다시 F5를 눌러 실행한 다음, 창을 닫아보면 Visual Studio에서 프로그램이 종료된 것을 확인할 수 있습니다.

실행과 종료 과정을 다음과 같이 정리할 수 있습니다.

——————————————————————————————————————–

F5키를 눌러 프로그램 실행 > 창 생성 > X 버튼 눌러서 창 닫기

> PeekMessage 함수에서 메시지 인식 > 메시지 변환 및 전달 > WindowProc 함수 호출

> WM_DESTROY 메시지 확인 > PostQuitMessage 함수 호출

> WM_QUIT 메시지 발생 > 메시지 루프(while 루프) 종료 > 프로그램 종료

——————————————————————————————————————–

앞으로도 창에서 발생한 특정 이벤트를 처리할 때는 WindowProc 함수에 메시지 처리 로직을 추가하는 과정을 진행할 것입니다.

이것으로 기본적인 창 생성 내용을 마무리하겠습니다.

다음 시간에는 창 생성 코드를 Engine 프로젝트에 클래스로 추가해 정리하는 과정을 진행해보겠습니다.

 

질문은 댓글로 남겨주세요.

긴 글 읽어주셔서 감사합니다.

 

RonnieJ

프리랜서 IT강사로 활동하고 있습니다. 게임 개발, C++/C#, 1인 기업에 관심이 많습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

Please turn AdBlock off

Notice for AdBlock users

Please turn AdBlock off