본문 바로가기
그래픽스 API/DirectX 11

[DirectX 11 in Windows 10, 11] 2. 프레임워크와 윈도우 만들기

by 니키티스 2023. 8. 3.

원문

빠재의 노트 :: DirectX11 Tutorial 2 - 프레임워크와 윈도우 만들기 (tistory.com)

 

DirectX11 Tutorial 2 - 프레임워크와 윈도우 만들기

Tutorial 2: Creating a Framework and Window 원문: http://www.rastertek.com/dx11tut02.html 저는 우선 DirectX 11 코딩을 시작하기보다는 간단한 코드 프레임워크를 만들어 두는 것을 추천합니다. 이 프레임워크는 간단

blog.nullbus.net

Tutorial 2: Creating a Framework and Window (rastertek.com)

 

Tutorial 2: Creating a Framework and Window

Tutorial 2: Creating a Framework and Window Before starting to code with DirectX 11 I recommend building a simple code framework. This framework will handle the basic windows functionality and provide an easy way to expand the code in an organized and read

www.rastertek.com

프레임 워크

우선 DX11(DirectX 11)을 코딩하기 전에 코드 프레임 워크를 만들어놓으면 편하다.

윈도우 기능을 제어할 때에 더 도움이 된다.

간단한 프레임워크 예시를 기반으로 코딩해 보자.

전체 구조

  • App의 시작점인 WinMain
  • 전체 애플리케이션을 캡슐화하는 SystemClass
  • 유저의 입력을 처리하는 InputClass
  • DX 그래픽 코드를 처리하는 GraphicsClass

main.cpp

왜? 이런 구조를?

///////////////////////////////////
// Filename: main.cpp
///////////////////////////////////
#include "systemclass.h"

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR pScmdline, int iCmdshow)
{
    SystemClass* System;
    bool result;

    // system 객체를 생성한다.
    System = new SystemClass;
    if (!System)
    {
        return 0;
    }

    // system 객체를 초기화하고 run을 호출한다.
    result = System->Initialize();
    if (result)
    {
        System->Run();
    }

    // system 객체를 종료하고 메모리를 반환한다.
    System->Shutdown();
    delete System;
    System = 0;

    return 0;
}

main.cpp는 전체 프레임워크의 구조를 보여준다.

main.cpp에서는 System 클래스를 생성하고 Run을 실행하고, Shutdown 함수를 호출하는 역할만 한다.

왜 소멸자 대신 Shutdown을 호출할까? 이렇게 단순한 구조면 굳이 main.cpp라는 파일이 필요 없을 것 같아 보인다.

이렇게 작성한 이유는 윈도우 프로그래밍에서는 제대로 파괴자를 호출하지 않는 함수가 몇몇 있기 때문이다.

  • EX: ExitThread()

그래서 호출 타이밍이 가변적인 소멸자(파괴자)에 메모리 정리를 위임하는 대신, Shutdown 함수에 메모리 정리를 위임하고 Shutdown을 확실하게 호출할 수 있게 프레임워크를 구성한다.

Systemclass

헤더 파일: systemclass.h

실제 시스템이 굴러가는 헤더 파일을 보자.

systemclass.h

////////////////////////////////////////////////////////////////////////////////
// Filename: systemclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _SYSTEMCLASS_H_
#define _SYSTEMCLASS_H_

///////////////////////////////
// PRE-PROCESSING DIRECTIVES //
///////////////////////////////
#define WIN32_LEAN_AND_MEAN

//////////////
// INCLUDES //
//////////////
#include <windows.h>

///////////////////////
// MY CLASS INCLUDES //
///////////////////////
#include "inputclass.h"
#include "applicationclass.h"

////////////////////////////////////////////////////////////////////////////////
// Class name: SystemClass
////////////////////////////////////////////////////////////////////////////////
class SystemClass
{
public:
    SystemClass();
    SystemClass(const SystemClass&);
    ~SystemClass();

    bool Initialize();
    void Shutdown();
    void Run();

private:
    bool Frame();
    void InitializeWindows(int&, int&);
    void ShutdownWindows();

private:
    LPCWSTR m_applicationName;
    HINSTANCE m_hinstance;
    HWND m_hwnd;

    InputClass* m_Input;
    ApplicationClass* m_Application;    
};

/////////////////////////
// FUNCTION PROTOTYPES //
/////////////////////////
// handle messages from windows for the application.
static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

/////////////
// GLOBALS //
/////////////
static SystemClass* ApplicationHandle = 0;

    #endif

우선, 쓸데없는 Win32 헤더 파일을 제외하기 위해 WIN32_LEAN_AND_MEAN을 정의해 주게 된다.

💡 #define WIN32_LEAN_AND_MEAN?

생각이 없는 프로그래밍 :: #define WIN32_LEAN_AND_MEAN (tistory.com)

 

#define WIN32_LEAN_AND_MEAN

빌드 시간을 단축할 수 있도록 Visual C++는 자주 사용하지 않는 API의 일부를 제외하여 Win32 헤더 파일의 크기를 줄이기 위해 WIN32_LEAN_AND_MEAN 같은 매크로를 제공한다. MFC가 아닌 응용 프로그램의 경

kksuny.tistory.com

 

빌드 시간을 단축할 수 있도록 Visual C++는 자주 사용하지 않는 API의 일부를 제외하여 Win32 헤더 파일의 크기를 줄이기 위해 WIN32_LEAN_AND_MEAN 같은 매크로를 제공한다.

MFC가 아닌 응용 프로그램의 경우에는 WIN32_LEAN_AND_MEAN 매크로를 정의하여 빌드 시간을 단축할 수 있다.

SysteClass 클래스가 정의되는데, Initialize, Shutdown, Run과 같이 WinMain 함수에서 호출되는 간단한 함수를 포함한다. MessageHandler 함수를 포함하여 애플리케이션이 실행 중일 때 애플리케이션으로 날아오는 윈도우 시스템의 메시지를 핸들링한다. 마지막으로 m_Input, m_Application으로 그래픽스 렌더링을 핸들링한다.

맨 아래에 WndProc 함수와 ApplicationHandle 포인터를 포함시켜서 윈도우 시스템 메시지를 직접 만든 MessageHandler 함수에 넣어줄 수 있게 만들었다.

 

⌨️ LPCWSTR

위 헤더파일을 잘 보면 LPCWSTR m_applicationName; 라는 변수 정의를 볼 수 있다. 이를 알기 위해서는 wchar_t부터 알아야 한다.

wchar_t는 와이드 문자(wide character)를 저장하기 위한 자료형이다. 2바이트 이상인 유니코드를 표현하기 위해 사용한다. VS에서는 unsigned short로, GCC에서는 4바이트로 정의되어 있다.

리터럴 상수인 문자와 문자열*은 *따옴표 앞에 L을 붙여 표현한다.

wchar_t wc1 = L’a’; 또는 wchar_t *ws1 = L"안녕하세요."; 와 같이 표현한다.

wchar_t가 아니라 LPCWSTR, PCWSTR (CONST)로 표현할 수도 있다. 단, CONST이냐 UNALIGNED이냐 기본이냐에 따라 다양한 케이스가 있으니 winnt.h 파일의 선언을 잘 살펴보자.

wchar_t 문자열은 w가 붙은 함수(wchar.h에 선언됨)로 문자열을 출력/입력(ex: wprintf, wscanf, swprintf 등)하고 문자열 처리는 str 대신 wcs로 시작한다(ex: strcpy  wcscpy)

단, 리눅스에서 wprintf로 wchar_t를 출력할 땐 %s 대신 %S를 쓴다고 한다.

참고 자료: C 언어 코딩 도장: 85.22 wchar_t 사용하기 (dojang.io)

 

💡 Windows에서 오는 메시지의 형태

Step 7. Handle Window Messages - Win32 apps | Microsoft Learn

MSDN을 참고하면, 다음과 같은 꼴로 메시지가 온다. 순서대로 윈도우 HWND, 메시지 타입 UINT, 파라미터 WPARAM, LPARAM이 들어온다. WndProc 함수가 정의된 것은 이 윈도우 메시지의 형태와 동일하다.

BOOL CGrayProp::OnReceiveMessage(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)

본격적인 systemclass의 시작

생성자/소멸자

생성자에서는 별 다른 일을 하지 않고, m_Input과 m_Application 포인터를 초기화해주도록 한다.

초기화에 실패했을 경우 Shutdown 함수에서 오브젝트를 제거해주어야 하기 때문에, 처음에 포인터에 할당된 오브젝트가 없을 땐 null로 만들어주는 것이 좋은 습관이다.

소멸자는 일부러 비워둔다. Shutdown 함수에서 구현할 것이기 때문이다. 반드시 소멸자가 제대로 호출되리란 법이 없다. ExitThread 같은 함수를 호출하면 소멸자가 호출되지 않아 메모리 누수가 발생한다.

개인적으로는 유니크 포인터로 바꾸어도 될 것 같지만, 소멸자가 제대로 호출되리라는 보장이 없어 Shutdown 함수에서 처리할수 있게 원시 포인터를 사용한 것으로 보인다.

💡 SystemClass 클래스 파괴자에서 아무런 객체 정리도 하지 않는다. 대신에 모든 정리 작업을 아래에 있는 Shutdown 함수에서 한다. 그 이유는 저자가 파괴자의 호출이 올바로 되지 않는다고 생각하기 때문.

“ExitThread() 와 같은 일부 윈도우 함수는 파괴자를 호출하지 않아 메모리 누수를 발생시키는 것으로 알려져 있습니다. 물론 더 안전한 버전의 함수를 사용할 수 있지만 저는 윈도우에서 프로그래밍을 할 때에는 상당히 조심스럽습니다.”

return으로 쓰레드를 종료하는 것이 베스트이다. 하지만 그렇게 할 수 없다면, 윈도우 프로그래밍을 할 때에는 파괴자 내지 소멸자가 제대로 동작하지 않는다고 가정하고 프로그래밍해야 한다.

참고 자료: 12 장. 쓰레드의 생성과 소멸 (velog.io)

아래에 있는 것은 SystemClass의 생성자, 복사 생성자 및 소멸자이다.

SystemClass::SystemClass()
{
    m_Input = 0;
    m_Application = 0;
}

SystemClass::SystemClass(const SystemClass& other)
{
}

SystemClass::~SystemClass()
{
}

(m_Input, m_Application에 0 대신 nullptr을 넣어도 됨)

SystemClass::Initalize()

여러 옵션을 초기화하는데, 유저 입력을 관리할 입력(Input)과 화면에그래픽을 렌더링할 앱(Application) 오브젝트도 초기화한다.

다만 앱 오브젝트의 초기화에 실패할 경우 초기화 실패로 간주하는 것으로 보인다.

bool SystemClass::Initialize()
{
    int screenWidth, screenHeight;
    bool result;

    // Initialize the width and height of the screen to zero before sending the variables into the function.
    screenWidth = 0;
    screenHeight = 0;

    // Initialize the windows api.
    InitializeWindows(screenWidth, screenHeight);

    // Create and initialize the input object.  This object will be used to handle reading the keyboard input from the user.
    m_Input = new InputClass;

    m_Input->Initialize();

    // Create and initialize the application class object.  This object will handle rendering all the graphics for this application.
    m_Application = new ApplicationClass;

    result = m_Application->Initialize(screenWidth, screenHeight, m_hwnd);
    if(!result)
    {
        return false;
    }

    return true;
}

SystemClass::Shutdown

해당 함수는 소멸자가 하는 일을 한다고 볼 수 있다. 어플리케이션 해제나 윈도우 닫기, 관련 핸들을 깨끗하게 정리해준다.

void SystemClass::Shutdown()
{
    // Release the application class object.
    if(m_Application)
    {
        m_Application->Shutdown();
        delete m_Application;
        m_Application = 0;
    }

    // Release the input object.
    if(m_Input)
    {
        delete m_Input;
        m_Input = 0;
    }

    // Shutdown the window.
    ShutdownWindows();

    return;
}

SystemClass::Run 함수

프로그램이 종료될 때까지 루프를 돌면서 어플리케이션의 모든 작업을 처리한다. 어플리케이션의 모든 작업은 매 루프마다 불리는 Frame 함수에서 수행된다.

의사 코드는 다음과 같다.

while 종료되지 않은 동안
    윈도우의 시스템 메시지를 확인
    메시지를 처리
    어플리케이션의 작업
    유저가 작업중 프로그램의 종료를 원하는지 확인

간단하게 보면, 윈도우든 유저든 프로그램 종료 메시지가 올 때까진 무한루프를 돌린다.

SystemClass::Frame() 함수

매 프레임 호출되면서 어플리케이션이 끝났는지 체크하는 함수이다.

지금은 간단하게, ESC가 눌리면 나갈 수 있게 키를 체크한다. 유저가 어플리케이션을 끄려고 하는 게 아니면 Application 클래스 객체로 해당 프레임을 위해 그래픽을 렌더링한다.

bool SystemClass::Frame()
{
    bool result;

    // Check if the user pressed escape and wants to exit the application.
    if(m_Input->IsKeyDown(VK_ESCAPE))
    {
        return false;
    }

    // Do the frame processing for the application class object.
    result = m_Application->Frame();
    if(!result)
    {
        return false;
    }

    return true;
}

SystemClass::MessageHandler 함수

메시지 핸들러 함수는 윈도우 시스템이 메시지를 보내도록 우리가 지정한 곳이라 볼 수 있다. 이러한 방식으로 우리가 관심 있는 정보를 수신할 수 있는 셈이다.

지금은 단순하게, 키가 눌리거나 떼어지면 Input 객체에 입력 정보를 전달하는 일을 한다. 그 이외의 정보에 관해서는 모두 윈도우 기본 메시지 핸들러가 처리하도록 한다.

LRESULT CALLBACK SystemClass::MessageHandler(HWND hwnd, UINT umsg, WPARAM wparam, LPARAM lparam)
{
    switch(umsg)
    {
        // Check if a key has been pressed on the keyboard.
        case WM_KEYDOWN:
        {
            // If a key is pressed send it to the input object so it can record that state.
            m_Input->KeyDown((unsigned int)wparam);
            return 0;
        }

        // Check if a key has been released on the keyboard.
        case WM_KEYUP:
        {
            // If a key is released then send it to the input object so it can unset the state for that key.
            m_Input->KeyUp((unsigned int)wparam);
            return 0;
        }

        // Any other messages send to the default message handler as our application won't make use of them.
        default:
        {
            return DefWindowProc(hwnd, umsg, wparam, lparam);
        }
    }
}

DefWindowProc은 WinUser.h에 정의된 함수로 일반적으로 애플리케이션에서 처리하지 않는 모든 윈도우 메시지를 처리하는 데에 사용된다(DefWindowProcW 함수(winuser.h) - Win32 apps | Microsoft Learn).

잘 보면, MSDN에서 처리하는 형태와 동일하다.

Step 7. Handle Window Messages - Win32 apps | Microsoft Learn

💡 lparam과 wparam?

win32 프로그래밍 - lParam 와 wParam 사용 : 네이버 블로그 (naver.com)

두 개는 윈도우에서 메시지를 처리할 때 나오는 파라미터 값(param)인데, 쉽게 말해서 wparam은 word parameter(UINT_PTR), lparam은 long word parameter(LONG_PTR)를 가리킨다. 일반적으로는 lparam을 포인터를 사용할 때 쓰는 것으로 보인다.

win32 프로그래밍 - lParam 와 wParam 사용 : 네이버 블로그 (naver.com)

lparam은 long parameter를 가리키기 때문에, 예를 들어 마우스 입력을 받을 땐 x, y 좌표를 동시에 받는다. 이때 앞에 있는 상위 2바이트는 x 좌표의 값을, 뒤에 있는 하위 2바이트는 y 좌표의 값을 받는다. 쉬프트 연산도 가능하지만, 보통 다음과 같은 매크로를 사용한다.

x = LOWORD(lparam);
y = HIWORD(lparam);

반면, wparam은 여러 개의 키가 동시에 눌렸을 때 어떤 키가 눌렸는지 구별하는 역할을 해준다. 비트 플래그를 통해서 컨트롤 키, 쉬프트 키 등 중 무엇이 눌렸는지를 확인할 수 있으며 동시에 눌린 경우에는 & 연산을 통해 각 키가 눌렸는지 아닌지를 체크할 수 있다.

SystemClass::InitializeWindows() 윈도우 초기화 함수

InitializeWindows 함수는 우리가 렌더링할 윈도우를 만드는 코드를 넣을 곳이다.

윈도우의 폭과 높이를 반환하여 어플리케이션 전반적으로 사용할 수 있게 한다. 윈도우는 검정 화면에 테두리가 없도록 초기화된 기본 세팅을 사용하도록 할 것이다. 함수는 전역변수 FULL_SCREEN의 설정에 따라 작은 윈도우나 전체 화면으로 된 윈도우를 만들 수 있다. 전역변수 값이 true이면 전체 화면, 아니면 단순히 800x600 크기의 윈도우를 화면 가운데에 띄운다. 해당 변수는 applicationclass.h의 최상단에 쓸 예정이다.

굳이 전역 변수가 아니라 해당 헤더 파일의 상단에 놓을 수도 있지 않느냐, 라고 할 수 있는데 이러한 결정을 한 이유는 이후에 하다 보면 이해할 수 있을 것이라고 한다.

해당 글에서 가장 긴 파트이다.

더보기
void SystemClass::InitializeWindows(int& screenWidth, int& screenHeight)
{
	WNDCLASSEX wc;
	DEVMODE dmScreenSettings;
	int posX, posY;


	// Get an external pointer to this object.
	ApplicationHandle = this;

	// Get the instance of this application.
	m_hinstance = GetModuleHandle(NULL);

	// Give the application a name.
	m_applicationName = L"Engine";

	// Setup the windows class with default settings.
	wc.style         = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
	wc.lpfnWndProc   = WndProc;
	wc.cbClsExtra    = 0;
	wc.cbWndExtra    = 0;
	wc.hInstance     = m_hinstance;
	wc.hIcon         = LoadIcon(NULL, IDI_WINLOGO);
	wc.hIconSm       = wc.hIcon;
	wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
	wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
	wc.lpszMenuName  = NULL;
	wc.lpszClassName = m_applicationName;
	wc.cbSize        = sizeof(WNDCLASSEX);

	// Register the window class.
	RegisterClassEx(&wc);

	// Determine the resolution of the clients desktop screen.
	screenWidth  = GetSystemMetrics(SM_CXSCREEN);
	screenHeight = GetSystemMetrics(SM_CYSCREEN);

	// Setup the screen settings depending on whether it is running in full screen or in windowed mode.
	if(FULL_SCREEN)
	{
		// If full screen set the screen to maximum size of the users desktop and 32bit.
		memset(&dmScreenSettings, 0, sizeof(dmScreenSettings));
		dmScreenSettings.dmSize       = sizeof(dmScreenSettings);
		dmScreenSettings.dmPelsWidth  = (unsigned long)screenWidth;
		dmScreenSettings.dmPelsHeight = (unsigned long)screenHeight;
		dmScreenSettings.dmBitsPerPel = 32;
		dmScreenSettings.dmFields     = DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT;

		// Change the display settings to full screen.
		ChangeDisplaySettings(&dmScreenSettings, CDS_FULLSCREEN);

		// Set the position of the window to the top left corner.
		posX = posY = 0;
	}
	else
	{
		// If windowed then set it to 800x600 resolution.
		screenWidth  = 800;
		screenHeight = 600;

		// Place the window in the middle of the screen.
		posX = (GetSystemMetrics(SM_CXSCREEN) - screenWidth)  / 2;
		posY = (GetSystemMetrics(SM_CYSCREEN) - screenHeight) / 2;
	}

	// Create the window with the screen settings and get the handle to it.
	m_hwnd = CreateWindowEx(WS_EX_APPWINDOW, m_applicationName, m_applicationName,
				WS_CLIPSIBLINGS | WS_CLIPCHILDREN | WS_POPUP,
				posX, posY, screenWidth, screenHeight, NULL, NULL, m_hinstance, NULL);

	// Bring the window up on the screen and set it as main focus.
	ShowWindow(m_hwnd, SW_SHOW);
	SetForegroundWindow(m_hwnd);
	SetFocus(m_hwnd);

	// Hide the mouse cursor.
	ShowCursor(false);

	return;
}

아래에는 InitializeWindows에는 Win32 및 MFC 관련 내용을 간단하게 정리한 것이다. 저자의 말에 따르면, 당장 이해하기 힘들면 넘어가는 걸 권장한다.

더보기

윈도우의 정보를 가리키는 WNDCLASSEX 클래스

더보기

RegisterClassEX

각 프로세스는 자체 윈도우 클래스를 등록해야 합니다. 애플리케이션 로컬 클래스를 등록하려면 RegisterClassEx 함수를 사용합니다. 윈도우 프로시저를 정의하고 WNDCLASSEX 구조체의 멤버를 채운 다음 구조체에 대한 포인터를 RegisterClassEx 함수에 전달해야 합니다. (창 클래스 사용 - Win32 apps | Microsoft Learn)
  • 자체적으로 만든 윈도우 클래스를 사용할 때, 이를 운영체제(윈도우8, 10, 11, …)에 등록해야 하므로 윈도우 클래스를 생성하는 과정의 끝에는 반드시 RegisterClassEX 함수를 호출해야 한다. 해당 함수는 이후에 사용할 윈도우 클래스를 등록한다.
  • RegisterClassExW 함수(winuser.h) - Win32 apps | Microsoft Learn
더보기

GetSystemMetrics

// Determine the resolution of the clients desktop screen.
screenWidth  = GetSystemMetrics(SM_CXSCREEN);
screenHeight = GetSystemMetrics(SM_CYSCREEN);

위 코드는 화면의 크기를 가져온다. 이때, GetSystemMetrics 함수는 해상도와 같이 컴퓨터에서 하드웨어에 따라 달라지거나 사용자가 재설정할 수 있는 정보들을 가져오는 함수이다.

실제 가져올 수 있는 정보는 아래 글을 들어가보면 알겠지만 해상도나 아래쪽 작업표시창의 높이, 현재 사용중인 네트워크처럼 단순한 정보밖에 없다. 따라서 심화된 정보는 다른 클래스를 참고하자.

GetSystemMetrics 함수에 대하여 : 네이버 블로그 (naver.com)

더보기

 DEVMODE 관련

dmSize

구조체의 공용 멤버를 따를 수 있는 프라이빗 드라이버 관련 데이터를 포함하지 않고 DEVMODE 구조체의 크기(바이트)를 지정합니다. 사용 중인 DEVMODE 구조체의 버전을 나타내려면 이 멤버 sizeof (DEVMODE) 를 로 설정합니다.

dmPelsWidth

표시되는 디바이스 표면의 너비를 픽셀 단위로 지정합니다. 예를 들어 표시 드라이버는 ChangeDisplaySettings 함수에서 이 멤버를 사용합니다. 프린터 드라이버는 이 멤버를 사용하지 않습니다.

dmPelsHeight

표시되는 디바이스 표면의 높이를 픽셀 단위로 지정합니다. 예를 들어 표시 드라이버는 ChangeDisplaySettings 함수에서 이 멤버를 사용합니다. 프린터 드라이버는 이 멤버를 사용하지 않습니다.

dmBitsPerPel

디스플레이 디바이스의 색 해상도를 픽셀당 비트 단위로 지정합니다(예: 16가지 색의 경우 4비트, 256색의 경우 8비트 또는 65,536색의 경우 16비트). 예를 들어 표시 드라이버는 ChangeDisplaySettings 함수에서 이 멤버를 사용합니다. 프린터 드라이버는 이 멤버를 사용하지 않습니다.

dmFields

DEVMODE 구조체의 특정 멤버가 초기화되었는지 여부를 지정합니다. 멤버가 초기화되면 해당 비트가 설정되고, 그렇지 않으면 비트가 명확합니다. 드라이버는 프린터 또는 디스플레이 기술에 적합한 DEVMODE 멤버만 지원합니다.

(MSDN에 더 많은 dmField 플래그의 종류가 존재함)

(DEVMODEA(wingdi.h) - Win32 apps | Microsoft Learn)

DEVMODE 구조체의 멤버 변수로, 표시되는 디바이스 크기를 설정할 수 있다. 여기서 디바이스는 윈도우를 말하는듯. pel은 픽셀 또는 픽셀의 수를 가리키는 것으로 보인다.

dmSize로 크기를 정해주고, 멤버 변수를 정해준 뒤에 dmFields로 어떤 변수를 지정해주었는지 알려주는 식으로 사용하는 것으로 보인다.

더보기

ChangeDisplaySettings

  • ChangeDisplaySettings 를 사용하면 현재 디스플레이의 해상도를 변경할 수 있다. 파라미터로 DEVMODE* 변수를 요구한다.이를 되돌리기 위해서는 첫 파라미터 DEVMODEA *lpDevMode 에 NULL을 주면 된다.
  • CDS_FULLSCREEN 플래그를 주면 그래픽 모드의 변경이 일시적으로만 적용이 된다.
  • 여기서 중요한 건 윈도우가 아니라, 디스플레이 해상도를 바꾼다는 점이다!!!
  • ChangeDisplaySettingsA 함수(winuser.h) - Win32 apps | Microsoft Learn
더보기

CreateWindowEX 함수

 

SystemClass::ShutdownWindows

윈도우를 닫는 함수. 화면 설정을 원상복구하고 윈도우와 그와 연관된 핸들도 해제(release)한다.

void SystemClass::ShutdownWindows()
{
    // Show the mouse cursor.
    ShowCursor(true);

    // Fix the display settings if leaving full screen mode.
    if(FULL_SCREEN)
    {
        ChangeDisplaySettings(NULL, 0);
    }

    // Remove the window.
    DestroyWindow(m_hwnd);
    m_hwnd = NULL;

    // Remove the application instance.
    UnregisterClass(m_applicationName, m_hinstance);
    m_hinstance = NULL;

    // Release the pointer to this class.
    ApplicationHandle = NULL;

    return;
}

WndProc 콜백: 윈도우 메시지 처리

WndProc 함수는 윈도우가 전송한 메시지가 도달하는 함수이다. 윈도우 클래스를wc.lpfnWndProc=WndProc;로 초기화할 때(InitializeWindows 함수) 나와서 이미 WndProc 함수를 알고 있는 분들도 있을 것이다. 모든 메시지를 system class에 있는 MessageHandler 함수로 보내기 때문에, WndProc도 system class 안에서 선언하도록 한다. 이렇게 하면 메세지 기능을 아예 system 클래스로 묶어놓는 셈이 되어서 관리하기도 더 편해진다.

LRESULT CALLBACK WndProc(HWND hwnd, UINT umessage, WPARAM wparam, LPARAM lparam)
{
    switch(umessage)
    {
        // Check if the window is being destroyed.
        case WM_DESTROY:
        {
            PostQuitMessage(0);
            return 0;
        }

        // Check if the window is being closed.
        case WM_CLOSE:
        {
            PostQuitMessage(0);        
            return 0;
        }

        // All other messages pass to the message handler in the system class.
        default:
        {
            return ApplicationHandle->MessageHandler(hwnd, umessage, wparam, lparam);
        }
    }
}

InputClass

InputClass.h

튜토리얼의 간편성을 위해 해당 강좌에서는 윈도우 입력(the windows input)을 사용하도록 한다(나중에는 윈도우 입력보다 더 좋은 DirectInput을 쓰게 될 것이다).

InputClass는 유저로부터 키보드 입력을 받게 되는데, SystemClass::MessageHandler 함수로부터 입력을 전달받는다. input 객체는 키보드 배열에서 각 키의 상태를 저장한다. 키보드 상태에 대해 질의가 들어오면 주어진 키가 눌려져 있는지 호출 함수를 통해 정보를 알려준다.

////////////////////////////////////////////////////////////////////////////////
// Filename: inputclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _INPUTCLASS_H_
#define _INPUTCLASS_H_

////////////////////////////////////////////////////////////////////////////////
// Class name: InputClass
////////////////////////////////////////////////////////////////////////////////
class InputClass
{
public:
    InputClass();
    InputClass(const InputClass&);
    ~InputClass();

    void Initialize();

    void KeyDown(unsigned int);
    void KeyUp(unsigned int);

    bool IsKeyDown(unsigned int);

private:
    bool m_keys[256];
};

#endif

InputClass.cpp

////////////////////////////////////////////////////////////////////////////////
// Filename: inputclass.cpp
////////////////////////////////////////////////////////////////////////////////
#include "inputclass.h"

InputClass::InputClass()
{
}

InputClass::InputClass(const InputClass& other)
{
}

InputClass::~InputClass()
{
}

void InputClass::Initialize()
{
    int i;


    // Initialize all the keys to being released and not pressed.
    for(i=0; i<256; i++)
    {
        m_keys[i] = false;
    }

    return;
}

void InputClass::KeyDown(unsigned int input)
{
    // If a key is pressed then save that state in the key array.
    m_keys[input] = true;
    return;
}

void InputClass::KeyUp(unsigned int input)
{
    // If a key is released then clear that state in the key array.
    m_keys[input] = false;
    return;
}

bool InputClass::IsKeyDown(unsigned int key)
{
    // Return what state the key is in (pressed/not pressed).
    return m_keys[key];
}

ApplicationClass

applicationclass.h

어플리케이션 클래스는 시스템 클래스에 의해 만들어진 또 다른 객체이다. 이 어플리케이션에서 모든 그래픽스 기능은 해당 클래스 내에서 캡슐화된다. 전체화면이나 창모드 같이 변경할 수도 있는 전역 설정과 관련된 그래픽스 기능을 위해 해당 헤더 파일을 활용할 예정이다.

현재 해당 클래스는 비어있지만 이후 튜토리얼에서 이 클래스에 모든 그래픽스 객체를 담을 예정이다.

////////////////////////////////////////////////////////////////////////////////
// Filename: applicationclass.h
////////////////////////////////////////////////////////////////////////////////
#ifndef _APPLICATIONCLASS_H_
#define _APPLICATIONCLASS_H_

//////////////
// INCLUDES //
//////////////
#include <windows.h>

/////////////
// GLOBALS //
/////////////
const bool FULL_SCREEN = false;
const bool VSYNC_ENABLED = true;
const float SCREEN_DEPTH = 1000.0f;
const float SCREEN_NEAR = 0.3f;

////////////////////////////////////////////////////////////////////////////////
// Class name: ApplicationClass
////////////////////////////////////////////////////////////////////////////////
class ApplicationClass
{
public:
    ApplicationClass();
    ApplicationClass(const ApplicationClass&);
    ~ApplicationClass();

    bool Initialize(int, int, HWND);
    void Shutdown();
    bool Frame();

private:
    bool Render();

private:

};

#endif

정리

별 문제 없이 따라가면 잘 실행이 된다.

이렇게 해서 프레임워크를 완성해서 컴파일하면 화면에 검정 윈도우가 뜨는 걸 볼 수 있다. 이 프레임워크는 향후에 진행할 튜토리얼의 기반이 되니 프레임워크가 작동하는 방식을 이해하는 것이 꽤 중요하다고 한다. 다만 프레임워크가 이해가 안 되더라도 다음 튜토리얼로 넘어가도 괜찮다. 이후에 프레임워크를 더 채우다 보면 좀 더 이해할 수 있을 것이다.

 

추가로 공부할 만한 것들

WINAPI(WinMain), CALLBACK(SystemClass::MessageHandler, WndProc)과 같은 낯선 자료형이 등장하는데, WINAPI와 CALLBACK은 minwindef.h 파일에 따르면 __stdcall을 재정의한 것이다.

// minwindef.h
...
#define CALLBACK    __stdcall
#define WINAPI      __stdcall
...

__stdcall은 함수 호출 규약의 일종이다. 자세한 사항은 다음 글을 참고하자.

[C/C++] 함수 호출 규약 __stdcall과 __cdecl에 관하여 (tistory.com)

 

[C/C++] 함수 호출 규약 __stdcall과 __cdecl에 관하여

함수 호출 규약? 함수 호출 규약을 공부하게 된 계기 함수의 선언형은 반환형 + 함수명 + 매개변수 리스트 로 구성된다. 함수의 반환형은 단 하나이다. 그래서 int, double과 같은 기본형과 MyClass 같

dev-nicitis.tistory.com

 

댓글