지금까지 만든 내용들을 사용하기 편하도록 프레임워크의 형태로 만들어보자.
우선 winapi 헤더를 직접 define하는 대신 별도의 헤더를 정의해 거기서 window 헤더를 define해주고 대신 그 헤더를 define해주자.
사실 이런 방식은 다른 프로젝트에서도 흔히 볼 수 있는 방식인데, WinApi를 사용할 때 특히 이 부분이 중요한 이유는, 윈도우api가 자체적으로 정의하는 요소들이 프로젝트 내부에서 문제를 일으킬 수 있기 때문이다.
한가지 예시로, 아래 헤더에서 #define NOMINMAX는 windows minmax 사용해제 옵션 매크로를 켜준 것으로, 이걸 켜주지 않으면 std::min std::max 등과 충돌해 예기치 못한(잡기도 힘든) 버그들이 발생할 수 있다.
추가로, 이 헤더를 정의한 이후에도 뭔가 winAPI관련 문제가 발생할 수도 있는데 이때 이 매크로들을 잘 살펴보면 힌트를 얻을지도 모른다.
// ChiliWin.h
#pragma once
// target Windows 7 or later
#define _WIN32_WINNT 0x0601
#include <sdkddkver.h>
// The following #defines disable a bunch of unused windows stuff. If you
// get weird errors when trying to do some windows stuff, try removing some
// (or all) of these defines (it will increase build time though).
// 매크로가 Windows.h define 전에 정의되는 게 아주 중요하다. 그래야 적용이 된다.
#define WIN32_LEAN_AND_MEAN
#define NOGDICAPMASKS
#define NOSYSMETRICS
#define NOMENUS
#define NOICONS
#define NOSYSCOMMANDS
#define NORASTEROPS
#define OEMRESOURCE
#define NOATOM
#define NOCLIPBOARD
#define NOCOLOR
#define NOCTLMGR
#define NODRAWTEXT
#define NOKERNEL
#define NONLS
#define NOMEMMGR
#define NOMETAFILE
#define NOMINMAX
#define NOOPENFILE
#define NOSCROLL
#define NOSERVICE
#define NOSOUND
#define NOTEXTMETRIC
#define NOWH
#define NOCOMM
#define NOKANJI
#define NOHELP
#define NOPROFILER
#define NODEFERWINDOWPOS
#define NOMCX
#define NORPC
#define NOPROXYSTUB
#define NOIMAGE
#define NOTAPE
#define STRICT
#include <Windows.h>
이번에는 창(윈도우), 그리고 창 인스턴스 클래스를 정의해보자.
창은 Window, 창 인스턴스(복사본)은 WindowClass로 정의한다.
창과 창 인스턴스의 차이에 대해서는 파트1의 앞선 글들을 참고하자.
WindowClass는 윈도우 운영체제에서 사용하는 창 인스턴스를 나타내는 클래스로(말이 헷갈릴 수 있으니 주의), 내부에 HINSTANCE 타입의 창 하나에 대응하는 멤버변수를 저장하고 있다. 창 생성, 소멸, 창 이름 등을 관리한다.
정의할 때 한가지 주의해야 할 점은 1) 창 인스턴스는 하나만 존재해야 하므로(하나의 창을 가리키니 당연하다) 클래스를 싱글톤으로 정의한다. 2) WindowClass를 윈도우에 등록하고, 사용 후 프로그램 종료시 등록 해제해주는 게 권장된다. (이는 윈도우 API 자체에서 창을 등록하고 해제해주는 것을 권장하기 때문이다)
세부적인 내용은 코드로 살펴보자.
#pragma once
#include "ChiliWin.h"
class Window
{
private:
// singleton manages registration/cleanup of window class
class WindowClass
{
public:
static const char* GetName() noexcept;
static HINSTANCE GetInstance() noexcept;
private:
WindowClass() noexcept;
~WindowClass();
WindowClass( const WindowClass& ) = delete;
WindowClass& operator=( const WindowClass& ) = delete;
static constexpr const char* wndClassName = "Chili Direct3D Engine Window";
static WindowClass wndClass;
HINSTANCE hInst;
};
public:
Window( int width,int height,const char* name ) noexcept;
~Window();
Window( const Window& ) = delete;
Window& operator=( const Window& ) = delete;
private:
/*
프레임워크의 핵심적인 부분을 담당하는 static 함수들이다.
아래 cpp 구현부에서 자세히 살펴보자.
*/
static LRESULT CALLBACK HandleMsgSetup( HWND hWnd,UINT msg,WPARAM wParam,LPARAM lParam ) noexcept;
static LRESULT CALLBACK HandleMsgThunk( HWND hWnd,UINT msg,WPARAM wParam,LPARAM lParam ) noexcept;
LRESULT HandleMsg( HWND hWnd,UINT msg,WPARAM wParam,LPARAM lParam ) noexcept;
private:
int width;
int height;
HWND hWnd;
};
#include "Window.h"
// Window Class Stuff
Window::WindowClass Window::WindowClass::wndClass;
Window::WindowClass::WindowClass() noexcept
:
hInst( GetModuleHandle( nullptr ) )
{
WNDCLASSEX wc = { 0 };
wc.cbSize = sizeof( wc );
wc.style = CS_OWNDC;
wc.lpfnWndProc = HandleMsgSetup; // 후술할 내용에서 다룸
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = GetInstance();
wc.hIcon = nullptr;
wc.hCursor = nullptr;
wc.hbrBackground = nullptr;
wc.lpszMenuName = nullptr;
wc.lpszClassName = GetName();
wc.hIconSm = nullptr;
RegisterClassEx( &wc );
}
Window::WindowClass::~WindowClass()
{
UnregisterClass( wndClassName,GetInstance() ); // WinAPI함수
}
const char* Window::WindowClass::GetName() noexcept
{
return wndClassName;
}
HINSTANCE Window::WindowClass::GetInstance() noexcept
{
return wndClass.hInst;
}
// Window Stuff
Window::Window( int width,int height,const char* name ) noexcept
{
// calculate window size based on desired client region size
RECT wr;
wr.left = 100;
wr.right = width + wr.left;
wr.top = 100;
wr.bottom = height + wr.top;
AdjustWindowRect( &wr,WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU,FALSE ); // winapi 함수로, 클라이언트(즉 우리가 윈도우 객체를 띄우려는 실제 버튼이 딸린 윈도우 창)의 크기에 맞게 입력값을 조절해준다.
// --------------
// 핵심 코드
// create window & get hWnd
hWnd = CreateWindow(
WindowClass::GetName(),name,
WS_CAPTION | WS_MINIMIZEBOX | WS_SYSMENU,
CW_USEDEFAULT,CW_USEDEFAULT,wr.right - wr.left,wr.bottom - wr.top,
nullptr,nullptr,WindowClass::GetInstance(),
this // this로 자신(Window)의 레퍼런스를 전달하는 이 부분이 중요한데, 추후 다시 언급하겠다
);
// ---------------
// show window
ShowWindow( hWnd,SW_SHOWDEFAULT );
}
Window::~Window()
{
DestroyWindow( hWnd );
}
/*
사실상 이번 내용의 핵심인 코드 부분이자, 약간 이해하기가 까다로울 수 있다.
hacky한 내용으로, 윈도우 api를 보다 c++ 환경에 맞게 조작하기 위해 일종의 꼼수 아닌 꼼수를 쓴다.
우선 목표는 다음과 같다.
현재 우리가 정의한 Window 클래스는 내부에 hWnd를 저장하고는 있으나, window api 입장에서는 우리가 이렇게 커스텀으로 정의한 Window를 쓰는 지 알 길이 없다.
그러다보니 winapi에서는 Window 클래스 내부의 멤버 변수, 메소드 등에 접근할 방법이 없고, 이걸 개통해주는 게 우리의 목표이다.
그럼 어떻게 구현할까?
시작에 앞서, 4. Message loop에서 다룬 내용 중 윈도우(창)의 메세지를 처리하는 함수를 가리키는 wc.lpfnWndProc 인자에 대한 내용을 간략하게라도 살펴보고 오는 것을 권장한다.
우선 우리는 윗쪽 Window::WindowClass::WindowClass() noexcept 구현에서 볼 수 있듯이 wc.lpfnWndProc을 HandleMsgSetup으로 지정해둔 상태다. 즉 HandleMsgSetup을 메세지를 처리하는 함수로 사용하겠다고 선언한 것이다.
따라서 이 윈도우(창)에 메세지가 전달되면 HandleMsgSetup가 실행되는데, HandleMsgSetup에서 앞으로 올 모든 메세지들에 대한 처리가 이뤄지냐? 하면 그건 아니다. HandleMsgSetup에서는 오직 WM_NCCREATE라는 메세지(처음 윈도우(창)이 생성될 때 전송되는 메세지)에 대해서만 처리를 해주고, 그 외의 경우 그냥 DefWindowProc, 즉 기본값 함수가 메세지를 처리하도록 해준 것을 볼 수 있다.
왜 이렇게 했는지는 잠시 뒤로 하자.
WM_NCCREATE 메세지 구조체의 lParam은 CREATESTRUCT라는 구조체의 포인터를 저장하고 있고, 이 CREATESTRUCT 구조체의 내부에는 lpCreateParams라는 변수가 저장된다. (MSDN 참고)
아래 구현부를 보면 HandleMsgSetup()에서 파라미터로 받은 lParam에서 CREATESTRUCTW와 LPTR을 추출해 최종적으로 해당 메세지가 생성한 Window에 대한 포인터를 얻는 것을 볼 수 있다.
자, 그럼 이 Window에 대한 포인터는 대체 언제 이 메세지(WM_NCCREATE)에게 전달되었던 것일까?
여기서 핵심이 되는 함수는 winapi에서 제공하는 SetWindowLongPtr 함수이다.
(https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setwindowlongptra)
SetWindowLongPtr을 이용하면 우리는 hWnd라는 WinAPI에서의 윈도우 클래스 내부에 다양한 값들을 저장할 수 있는데, 여기서 GWLP_USERDATA라는, 일종의 우리같은 사용자단에게 허용된 데이터 공간 내부에 우리가 정의한 class Window를 포인터로 저장하는 것으로 hWnd(WINAPI) 와 Window(사용자 커스텀)의 연결을 성사시킨 것이다!
즉 이제 WinAPI측에서 우리 class Window에 대한 접근이 가능해진 것이다!
이 일회성 작업을 하는 것이 HandleMsgSetup()의 유일한 목적이며, 이 작업이 끝나면 윈도우(창) 구조체의 .lpfnWndProc은 HandleMsgSetup에서 HandleMsgThunk로 변경해주는 것을 볼 수 있다. (이 변경 또한 SetWindowLongPtr를 통해 이루어진다)
이제 메세지는 HandleMsgThunk()에서 처리된다.
그럼 여기서 끝이냐?
하면 또 그건 아닌데, HandleMsgThunk()도 사실 직접 메세지를 처리한다기 보다는 일종의 wrapper 느낌에 가깝다. HandleMsgThunk로 전달되는 인자중 hWnd로부터 Window class 포인터를 추출해내어 Window class 내부의 다양한 멤버변수와 메소드를 활용할 수 있게 해주는 역할을 하는 것이다.
코드에서도 보면 알 수 있지만 실질적인 메세지의 처리는 Window::HandleMsgThunk가 아닌 Window::HandleMsg에서 처리되는 것을 볼 수 있다.
*/
LRESULT CALLBACK Window::HandleMsgSetup( HWND hWnd,UINT msg,WPARAM wParam,LPARAM lParam ) noexcept
{
// use create parameter passed in from CreateWindow() to store window class pointer at WinAPI side
if( msg == WM_NCCREATE )
{
// extract ptr to window class from creation data
const CREATESTRUCTW* const pCreate = reinterpret_cast<CREATESTRUCTW*>(lParam);
Window* const pWnd = static_cast<Window*>(pCreate->lpCreateParams);
// set WinAPI-managed user data to store ptr to window class
SetWindowLongPtr( hWnd,GWLP_USERDATA,reinterpret_cast<LONG_PTR>(pWnd) );
// set message proc to normal (non-setup) handler now that setup is finished
SetWindowLongPtr( hWnd,GWLP_WNDPROC,reinterpret_cast<LONG_PTR>(&Window::HandleMsgThunk) );
// forward message to window class handler
return pWnd->HandleMsg( hWnd,msg,wParam,lParam );
}
// if we get a message before the WM_NCCREATE message, handle with default handler
return DefWindowProc( hWnd,msg,wParam,lParam );
}
LRESULT CALLBACK Window::HandleMsgThunk( HWND hWnd,UINT msg,WPARAM wParam,LPARAM lParam ) noexcept
{
// retrieve ptr to window class
Window* const pWnd = reinterpret_cast<Window*>(GetWindowLongPtr( hWnd,GWLP_USERDATA ));
// forward message to window class handler
return pWnd->HandleMsg( hWnd,msg,wParam,lParam );
}
LRESULT Window::HandleMsg( HWND hWnd,UINT msg,WPARAM wParam,LPARAM lParam ) noexcept
{
switch( msg )
{
// we don't want the DefProc to handle this message because
// we want our destructor to destroy the window, so return 0 instead of break
case WM_CLOSE:
// 여기서 WM_CLOSE만 예외처리를 해준 이유는 ~Window에서 winapi 윈도우(창)에대한 소멸 처리를 해주니까 굳이 winAPI에서의 소멸처리를 두번 해주는 것을 막게 하기 위함이다.
PostQuitMessage( 0 );
return 0;
}
return DefWindowProc( hWnd,msg,wParam,lParam );
}
이제 실제 사용 예시를 살펴보자.
#include "Window.h"
int CALLBACK WinMain( // 그냥 참고: CALLBACK과 WINAPI 매크로는 둘다 __stdcall을 의미한다.
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow )
{
Window wnd( 800,300,"테스트" ); // 우리가 정의한 클래스를 사용해 윈도우 제작이 가능해졌다!
// Window wnd2(640,480,"테스트2"); // 창 여러개 사용도 쉽게 가능하다!
MSG msg;
BOOL gResult;
while( (gResult = GetMessage( &msg,nullptr,0,0 )) > 0 )
{
// TranslateMessage will post auxilliary WM_CHAR messages from key msgs
TranslateMessage( &msg );
DispatchMessage( &msg );
}
// check if GetMessage call itself borked
if( gResult == -1 )
{
return -1;
}
// wParam here is the value passed to PostQuitMessage
return msg.wParam;
}
'computer graphics > DirectX' 카테고리의 다른 글
[D3D Engine] 8.Error handling, Custom icon (0) | 2023.03.19 |
---|---|
[D3D Engine] 6. WM_CHAR, Mouse (0) | 2023.03.19 |
[D3D Engine] 5. Window Messages (1) | 2023.03.19 |
[D3D Engine] 4. Message loop, WinProc (0) | 2023.03.19 |
[D3D Engine] 3. Window Creation (0) | 2023.03.19 |