WinAPI로 엔진 만들기 #1

Window 데스크톱 애플리케이션 만들기

Visual Studio 2022 열기! → 새 프로젝트 만들기  Window Desktop Application(.exe) 형식

 

이후 필요에 따라 GIT 연결과 레포지토리 만들기 등을 실시한다!

 

Win Main

헤더 파일

도스에서는 사용하는 함수에 따라 여러개의 헤더 파일을 포함하지만 윈도우즈에서는 하나의 헤더 파일에 모든 API

함수들의 원형과 사용하는 상수들을 죄다 정의하고 있기 때문에 windos.h만 포함해주면 된다.

windows.h 헤더파일은 기본적인 데이터 타입, 함수 원형등을 정의하며 그 외 필요한 헤더 파일을 포함하고 있다.

그래서 윈도우즈 프로그램의 첫 줄은 거의 항상 include <windows.h>로 시작된다.

 

int APIENTRY wWinMain(_In_ HINSTANCE hInstance, // 프로그램의 인스턴스 핸들.
                     _In_opt_ HINSTANCE hPrevInstance, // 바로 앞에 실행된 현재 프로그램의 인스턴스 핸들, 없을 경우 NULL.
                     _In_ LPWSTR    lpCmdLine, // 명령행으로 입력된 프로그램 인수.
                     _In_ int       nCmdShow) // 프로그램이 실행될 형태, 보통 모양 정보 등이 전달된다.

 

또 dos 프로그램과의 차이는 시작점인 entry point가 main함수가 아니라 WinMain이라는 점이다.

윈도우즈 프로그램의 시작점은 main이 아닌 WinMain이다.

 

각 인수의 의미

더보기
hInstance : 프로그램의 인스턴스 핸들(프로세스ID).
hPrevInstance : 바로 앞에 실행된 현재 프로그램의 인스턴스 핸들, 없을 경우 NULL.
WIN32에서는 항상 NULL이다. 호환성을 위해서만 존재하는 인수이므로 신경쓰지 않아도 된다
lpCmdLine : 명령행으로 입력된 프로그램 인수.
nCmdShow : 프로그램이 실행될 형태. 최소화, 보통 모양 정보 등이 전달된다.

 

하지만 hInstance 외에는 잘 사용되지 않는다. 인스턴스라는 말은 클래스가 실제 메모리에 구현된 실체를 의미한다.

윈도우즈용 프로그램은 여러 개의 프로그램이 동시에 실행되는 멀티태스킹 시스템일 뿐만 아니라 하나의 프로그램이 여러 번 실행될 수도 있다. 이때 실행되고 있는 각각의 프로그램을 프로그램 인스턴스라고 하며 간단히 줄여서 인스턴스라고 한다.

 

예를 들어 메모장이 두 번 실행되어 있다고 해보자.

 

이 때 두 프로그램은 모두 메모장이지만 운영체제는 각각 다른 메모리를 사용하는 다른 프로그램으로 인식한다.

이때 각 메모장은 서로 다른 인스턴스 핸들을 가지며 운영체제는 이 인스턴스 핸들값으로 두개의 메모장을 서로 구별한다.

hInstance란 프로그램 자체를 일컫는 정수값이며 API함수에서 수시로 사용된다.

lpszClass라는 전역 문자열이 정의되어 있는데, 이 문자열은 윈도우 클래스를 정의하는데 사용된다.

 

메세지 처리 함수

이 프로그램을 자세히 보면 두개의 함수만 있다. 하나는 프로그램의 시작점인 winMain이며 나머지 하나는 WndProc이다.

 

도스에서는 main 함수만으로도 프로그램을 작성할 수 있지만,

윈도우즈에서는 아주 특별한 경우를 제외하고는 이 두개의 함수가 모두 있어야 한다.

 

winMain에서는 윈도우를 만들고 화면에 출력하기만 한다. 즉, 프로그램을 시작시키기는 역할만 맡는다.

그러므로 대부분의 일(실질적인 처리)은 WndProc에서 이루어 진다.

또한, winMain 은 대체적으로 항상 일정한 코드로 되어있지만 wndProc은 프로그램에 따라서 천차만별로 달라진다.

 

그래서 소스를 분석 할 때 주의깊게 봐야 할 부분은 main이 아니라 proc이다!!!

 

윈도우 클래스

winMain함수에서 가장 중요한 일은 윈도우를 만드는 일이다.

윈도우가 있어야 사용자로부터 입력을 받을 수 있고 출력을 보여줄 수도 있기 때문이다. 윈도우를 만드려면 윈도우

클래스를 먼저 등록한 후 CreateWindow함수를 호출해야 한다. 모든 윈도우는 윈도우 클래스를 기반으로 만들어지며

윈도우 클래스는 만들어질 윈도우의 여러가지 특성을 정의한다.

typedef struct tagWNDCLASS {
    UINT        style;
    WNDPROC     lpfnWndProc;
    int         cbClsExtra;
    int         cbWndExtra;
    HINSTANCE   hInstance;
    HICON       hIcon;
    HCURSOR     hCursor;
    HBRUSH      hbrBackground;
    LPCSTR      lpszMenuName;
    LPCSTR      lpszClassName;
} WNDCLASS

 

style

더보기

윈도우의 스타일을 정의한다. 즉 윈도우가 어떤 형태를 가질 것인가를 지정하는 멤버이다. 이 멤버가 가질 수 있는 값은 무수히 많지만 가장 많이 사용하는 값이 CS_HREDRAW와 CS_VREDRAW이다. 이 두 값을 OR 연산자(|)로 연결하여 사용한다. 이 값들의 의미는 윈도우의 수직(또는 수평) 크기가 변할 경우 윈도우를 다시 그린다는 뜻이다. 이 밖에도 많은 값이 올 수 있다.

lpfnWndProc

더보기

이 멤버는 윈도우의 메시지 처리 함수를 지정한다. 메시지가 발생할 때마다 여기서 지정한 함수가 호출되며 이 함수가 모든 메시지를 처리한다. 메시지 처리 함수의 이름은 물론 마음대로 정할 수 있지만 거의 WndProc으로 정해져 있는 편이다.

cbClsExtra, cbWndExtra

더보기

일종의 예약 영역이다. 윈도우즈가 내부적으로 사용하며 아주 특수한 목적에 사용되는 여분의 공간이다. 예약 영역을 사용하지 않을 경우는 0으로 지정한다.

hInstance

더보기

이 윈도우 클래스를 사용하는 프로그램의 번호이며 이 값은 WinMain의 인수로 전달된 hInstance값을 그대로 대입해주면 된다.

hIcon, hCursor

더보기

이 윈도우가 사용할 마우스 커서와 최소화되었을 경우 출력될 아이콘을 지정한다. LoadCursor 함수와 LoadIcon 함수를 사용하여 지정한다. 사용자가 직접 아이콘과 커서를 만들어 사용할 수도 있지만 여기서는 윈도우즈가 디폴트로 제공하는 아이콘과 커서를 사용하고 있다. 커서는 좌측으로 기울어진 화살표 모양이며 아이콘은 기본창 아이콘 모양을 가진다.

hbrBackground

더보기

윈도우의 배경 색상을 지정한다. 좀 더 정확하게 표현하면 윈도우의 배경 색상을 채색할 브러시를 지정하는 멤버이다. GetStockObject라는 함수를 사용하여 윈도우에서 기본적으로 제공하는 브러시를 지정한다. 지정할 수 있는 브러시에는 여러 가지 종류가 있지만 가장 일반적인 흰색 배경(WHITE_BRUSH)이 많이 사용된다.

lpszMenuName

더보기

이 프로그램이 사용할 메뉴를 지정한다. 메뉴는 프로그램 코드에서 만드는 것이 아니라 리소스 에디터에 의해 별도로 만들어진 후 링크 시에 같이 합쳐진다. 메뉴를 사용하지 않을 경우 이 멤버에 NULL을 대입해주면 된다.

lpszClassName

더보기

윈도우 클래스의 이름을 정의한다. 여기서 지정한 이름은 CreateWindow 함수에 전달되어지며 CreateWindow 함수는 윈도우 클래스에서 정의한 특성값을 참조하여 윈도우를 만든다. 윈도우 클래스의 이름은 보통 실행 파일의 이름과 일치시켜 작성하며 이 예제의 경우 lpszClass 문자열에 "First"를 대입한 후 이 문자열을 윈도우 클래스 이름으로 사용하였다.

멤버의 수가 너무 많아 한번에 다 익히기 힘들겠지만 이 중에 제일 중요한 멤버는 윈도우 클래스의 이름을 정의하는 lpszClassName과 메시지 처리 함수를 지정하는 lpfnWndProc이다. 윈도우 클래스를 정의한 후 RegisterClass 함수를 호출하여 윈도우 클래스를 등록한다.

 

ATOM MyRegisterClass(HINSTANCE hInstance)
{
    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_EDITORWINDOW));
    wcex.hCursor        = LoadCursor(nullptr, IDC_ARROW);
    wcex.hbrBackground  = (HBRUSH)(COLOR_WINDOW+1);
    wcex.lpszMenuName   = MAKEINTRESOURCEW(IDC_EDITORWINDOW);
    wcex.lpszClassName  = szWindowClass;
    wcex.hIconSm        = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));

    return RegisterClassExW(&wcex);
}

 

RegisterClass 함수의 인수로 wndClass 구조체의 번지를 넘겨주면 된다.

이런 특성을 가진 윈도우를 앞으로 사용하겠다는 등록 과정이다. 그리고 난후에 등록된 윈도우로 윈도우를 생성해주어야 한다.

 

BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
   hInst = hInstance; // 인스턴스 핸들을 전역 변수에 저장합니다.

   // 윈도우 창 만들기
   HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
      CW_USEDEFAULT, 0, 1600, 900, nullptr, nullptr, hInstance, nullptr);

   if (!hWnd)
   {
      return FALSE;
   }

   ShowWindow(hWnd, nCmdShow);
   UpdateWindow(hWnd);

   return TRUE;
}

 

lpszClassName

더보기

생성하고자 하는 윈도우의 클래스를 지정하는 문자열이다. 앞에서 정의한 WndClass구조체의 lpszClassName 멤버의 이름을 여기에 기입해 준다. 우리의 예제에서는 lpszClass 문자열에 윈도우 클래스 이름을 기억시켜 두었으므로 이 문자열을 그대로 넘겨주면 된다.

lpszWindowName

더보기

윈도우의 타이틀 바에 나타날 문자열이다. 여기서 지정한 문자열이 윈도우의 타이틀 바에 나타난다. 프로그래머가 마음대로 지정할 수 있는데, 예제에서는 프로젝트명(lpszClass 전역 문자열)을 타이틀 바에 나타내고 있다.

dwStyle

더보기

만들고자 하는 윈도우의 형태를 지정하는 인수이다. 일종의 비트 필드값이며 거의 수십개를 헤아리는 매크로 상수들이 정의되어 있고 이 상수들을 OR연산자로 연결하여 윈도우의 다양한 형태를 지정한다. 윈도우가 경계선을 가질 것인가, 타이틀 바를 가질 것인가 또는 스크롤 바의 유무 등을 세세하게 지정해 줄 수 있다. 가능한 스타일값에 관한 자세한 내용은 레퍼런스를 참조하되 WS_OVERLAPPEDWINDOW를 사용하면 가장 무난한 윈도우 설정 상태가 된다. 즉 시스템 메뉴, 최대 최소 버튼, 타이틀 바, 경계선을 가진 윈도우를 만들어 준다.

X, Y, nWidth, nHeight

더보기

인수의 이름이 의미하듯이 윈도우의 크기와 위치를 지정하며 픽셀 단위를 사용한다. x, y좌표는 메인 윈도우의 경우는 전체 화면을 기준으로 하며 차일드 윈도우는 부모 윈도우의 좌상단을 기준으로 한다. 정수값을 바로 지정해도 되며 CW_USEDEFAULT를 사용하면 윈도우즈가 알아서 적당한 크기와 위치를 설정해 준다. 예제에서는 모두 CW_USEDEFAULT를 사용하였다.

hWndParent

더보기

부모 윈도우가 있을 경우 부모 윈도우의 핸들을 지정해준다. MDI 프로그램이나 팝업 윈도우는 윈도우끼리 수직적인 상하관계를 가져 부자(parent-child) 관계가 성립되는데 이 관계를 지정해 주는 인수이다. 부모 윈도우가 없을 경우는 이 값을 NULL로 지정하면 된다.

hmenu

더보기

윈도우에서 사용할 메뉴의 핸들을 지정한다. WndClass에도 메뉴를 지정하는 멤버가 있는데 윈도우 클래스의 메뉴는 그 윈도우 클래스를 기반으로 하는 모든 윈도우에서 사용되는 반면 이 인수로 지정된 메뉴는 현재 CreateWindow 함수로 만들어지는 윈도우에서만 사용된다. 만약 WndClass에서 지정한 메뉴를 그대로 사용하려면 이 인수를 NULL로 지정하면 되며 WndClass에서 지정한 메뉴 대신 다른 메뉴를 사용하려면 이 인수에 원하는 메뉴 핸들을 주면 된다. First 예제의 경우 WndClass에도 메뉴가 지정되어 있지 않고 CreateWindow 함수에서도 메뉴를 지정하지 않았으므로 메뉴없는 프로그램이 만들어진다.

hinst

더보기

윈도우를 만드는 주체, 즉 프로그램의 핸들을 지정한다. WinMain의 인수로 전달된 hInstance를 대입해 주면 된다.

lpvParam

더보기

CREATESTRUCT라는 구조체의 번지이며 특수한 목적에 사용된다. 보통은 NULL값을 사용한다.

 

물론 CreateWindow의 모든 인수를 다 외우려고 할 필요까지는 없다!!!

예제에서 어떤 값이 사용되었는가와 그 의미가 무엇인가만 대충 보고 가도록 하자.

 

CreateWindow 이해하기!

  1. CreateWindow 함수는 윈도우에 관한 모든 정보를 메모리에 만든 후 윈도우 핸들을 리턴값으로 넘겨준다.
  2. 넘겨지는 윈도우 핸들은 hWnd라는 지역 변수에 저장되었다가 윈도우를 참조하는 모든 함수의 인수로 사용된다.
  3. CreateWindow 함수로 만든 윈도우는 어디까지나 메모리상에서만 있을 뿐이며 아직까지 화면에 출력되지는 않았다.
  4. 메모리에 만들어진 윈도우를 화면으로 보이게 하려면 다음 함수를 사용해야 한다. → BOOL ShowWindow(hWnd, nCmdShow);
  5. hWnd 인수는 화면으로 출력하고자 하는 윈도우의 핸들이며 CreateWindow 함수가 리턴한 핸들을 그대로 넘겨주면 된다.
  6. nCmdShow는 윈도우를 화면에 출력하는 방법을 지정하며 다음과 같은 매크로 상수들이 정의되어 있다.
더보기

SW_HIDE : 윈도우를 숨긴다.

SW_MINIMIZE : 윈도우를 최소화시키고 활성화시키지 않는다.

SW_RESTORE : 윈도우를 활성화시킨다.

SW_SHOW : 윈도우를 활성화시켜 보여준다.

SW_SHOWNORMAL : 윈도우를 활성화시켜 보여준다.

 

nCmdShow 인수에 어떤 값을 넘겨줄 것인가는 전혀 고민할 필요가 없으며 WinMain 함수의 인수로 전달된 nCmdShow를 그대로 넘겨주기만 하면 된다. 그래서 'ShowWindow(hWnd,nCmdShow);' 와 같이 거의 호출 형식이 정해져 있는 셈이다.

 

윈도우를 만들고 화면에 나타내는 코드는 다음과 같다.

// 윈도우 창 만들기
HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
   CW_USEDEFAULT, 0, 1600, 900, nullptr, nullptr, hInstance, nullptr);

if (!hWnd)
{
   return FALSE;
}

ShowWindow(hWnd, nCmdShow);

 

여기까지 실행하면 화면에 윈도우가 출력된다. 이후부터는 메시지 루프가 시작되며 프로그램이 사용자와 윈도우즈, 그리고 다른 프로그램과 상호 정보를 교환하며 실행된다. 여기까지 윈도우를 만드는 과정을 간단하게 정리해 보도록 하자.

 

WndClass 정의 → CreateWindow ShowWindow 메시지 루프

 

이 과정은 거의 정형화된 과정이므로 계속 윈도우즈 프로그래밍을 공부할 생각이 있다면 암기할 만도 하다. 이 과정이 WinMain에서 반드시 해줘야 할 과정이며 그 이외의 처리는 할 필요가 거의 없다.

 

윈도우즈는 세가지 동적 연결 라이브러리 이루어져 있는데, 메모리를 관리하고 실행시키는 KERNEL, 유저 인터페이스와 윈도우를 관리하는 USER, 그리고 화면 처리와 그래픽을 담당하는 GDI이다.

 

출력을 하려면 우리는 GDI(Graphic Device Interface)모듈에 특별히 관심을 기울여야 한다. 화면으로 출력되는 모든 글자와 그림은 GDI를 통해야 하기 때문이다.

 

DC(Device Context)란, 출력에 필요한 모든 정보를 가지는 데이터 구조체이며 GDI모듈에 의해 관리된다. 어떤 폰트를 사용할 것인가, 선의 색상과 굵기, 채움 무늬와 색상, 출력방법 등이 모두 출력에 필요한 정보들이다.

 

화면 출력에 DC가 필요한 이유를 이해하기 위해 몇 가지 상황을 들어보자

 

상황1

더보기

우선 화면에 선을 긋는 LineTo라는 함수를 생각해 보자. 선을 긋기 위해서는 최소한 시작점과 끝점의 좌표가 필요하다는 것을 상식적으로 쉽게 이해할 수 있을 것이다. 그러면 LineTo(x1, y1, x2, y2)와 같은 식으로 함수를 호출하여 선을 그을 수 있을 것이다. 그러나 조금 더 생각 해보면 두 점의 좌표 외에도 여러가지 정보가 더 필요하다. 선의 색상, 굵기, 모양, 선을 그리는 방법, 좌표값을 해석하는 방법 등의 추가 정보가 있어야 비로소 완벽한 선을 그을 수 있다. 이런 정보들을 모두 인수로 넘겨준다면 LineTo함수는 다음과 같은 모양이 될것이다.

 

LineTo(StartX, StartY, EndX, EndY, Color, Width, Shape, ROP, mode,......)

 

이런 정보들을 일일이 인수로 전달할 것이 아니라 한 곳에 모아두고 그 값들을 사용하는 방법이 훨씬 더 편리하고 효율적이다. 그래서 이런 정보들을 모두 모아 DC라는 것을 만들고 그리기 함수에서는 DC의 핸들만을 넘겨받아 그리기에 필요한 추가 정보는 모두 DC에 정의 되어 있는 값을 사용한다. 이런 방식을 사용하면 LineTo 함수는 다음과 같이 간단해질수 있을 것이다.

 

LineTo(hdc, x, y)

 

상황2

더보기

DC가 필요한 또 다른 예는 윈도우즈는 여러 개의 프로그램이 동시에 실행되는 멀티태스킹 시스템이기 때문에 그리기 함수에 의해 실제 출력되는 모양은 주변 환경에 따라 다르다.

LineTo(hdc, 100, 100)을 호출 했을 때, 화면 상의 (100, 100)까지의 점을 찍는 것이 아니라 실제로 점이 찍혀야 할 부분은 현재 윈도우가 차지하고 있는 영역이 되어야 한다. 또한, 2개의 윈도우가 있을 때 화면에 그려져야 할 부분은 해당 코드가 실행된 윈도우 영역에서만 그림이 그려져야 한다.

 

이 외에도 DC가 필요한 상황은 여러가지가 존재한다!

 

 

 

 

참고) https://www.youtube.com/watch?v=l8awS35y43k&list=PLWKwcHKTXy5RSkINElI7wZOwn9z4RcJff&index=3

'WinAPI, DirectX' 카테고리의 다른 글

DirectX로 엔진 만들기 #2  (0) 2024.09.12
DirectX로 엔진 만들기 #1  (0) 2024.09.12