기본적인 Windows 응용 프로그램

WanJu Kim·2023년 1월 1일

Direct3D

목록 보기
2/29

Windows 응용 프로그램을 만들려면 visual studio를 실행 후 'Windows 데스크톱 어플리케이션'을 선택해야 한다.

그리고 앞으로 x86에서만 실행할 예정이니, 미리 구성 관리자를 통해 다른 모드를 없앤다.



곧바로 실행(F5)하면 창이 하나 뜨는데, 코드 하나하나 알아보도록 하자. 프로그램이 돌아가는 순서를 외우는 게 좋은 것 같아서 옛 코드 쓰기로 했다.

// Windows 헤더 파일을 포함시킨다. 여기에 Windows 프로그래밍에 필요한 모든
// Win32 API 구조체, 형식, 함수 선언이 들어 있다.
#include <windows.h>

// 주된 창의 핸들. 생성된 창을 식별하는 용도로 쓰인다.
HWND ghMainWnd = 0;

// Windows 응용 프로그램의 초기화에 필요한 코드를 감싼 함수.
// 초기화에 성공하면 true, 그렇지 않으면 false 반환.
bool InitWindowsApp(HINSTANCE instanceHandle, int show);

// 메시지 루프 코드를 담은 함수.
int Run();

// 주 창이 받은 사건들을 처리하는 창 프로시저 함수.
LRESULT CALLBACK
WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

// Windows 응용 프로그램의 주 진입점. 콘솔 프로그램의 main()에 해당한다.
int WINAPI
WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR pCmdLine, int nShowCmd)
{
	// 우선 응용 프로그램의 주 창을 초기화하기 위해, hInstance와 nShowCmd를 인수로 하여
    // 초기화 함수(InitWindowsApp)를 호출한다.
    if (!InitWindowsApp(hInstance, nShowCmd))
    	return 0;
        
	// 응용 프로그램이 성공적으로 생성, 초기화되었다면 메시지 루프로 진입한다.
    // 그 루프는 응용 프로그램이 종료되어야 함을 뜻하는 WM_QUIT 메시지를 받을 때까지 계속 돌아간다.
    return Run();
}

bool InitWindowsApp(HINSTANCE instanceHandle, int show)
{
	// 창을 생성할 때 가장 먼저 할 일은 창의 몇몇 특성들을 서술하는 WNDCLASS 구조체를 채우는 것이다.
    WNDCLASS wc;
    wc.style = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc = WndProc;
    wc.cbClsExtra = 0;
    wc.cbWndExtra = 0;
    wc.hInstance = instanceHandle;
    wc.hIcon = LoadIcon(0, IDI_APPLICATION);
    wc.hCursor = LoadCursor(0, IDC_ARROW);
    wc.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
    wc.lpszMenuName = 0;
    wc.lpszClassName = L"BasicWndClass";
    
    // 다음으로는 이 WNDCLASS 인스턴스를 Windows에 등록한다.
    // 그래야 다음 단계에서 이 창 클래스에 기초해서 창을 생성할 수 있다.
    
    if (!RegisterClass(&wc))
    {
    	MessageBox(0, L"RegisterClass FAILED", 0, 0);
        return false;
    }
    
    // WNDCLASS 인스턴스가 성공적으로 등록되었다면 CreateWindow 함수로 창을 생성한다.
	// 이 함수는 생성된 창의 핸들 (HWND 형식의 값)을 돌려준다.
    // 생성이 실패하면 값이 0인 핸들을 돌려준다. 창 핸들은 Windows가 내부적으로 관리하는 창을 지칭하는 수단이다.
    // 창을 다루는 Win32 API 함수들 중에는 자신이 작업할 창을 식별하기 위해 이 HWND 값을 받는 것들이 많다.
    
    ghMainWnd = CreateWindow(
    	L"BasicWndClass",	// 사용할 창 클래스의 이름(앞에서 등록했음).
        L"Win32Basic",	// 창의 제목.
        WS_OVERLAPPEDWINDOW,	// 스타일 플래그들.
        CW_USEDEFAULT,	// x 좌표.
        CW_USEDEFAULT,	// y 좌표.
        CW_USEDEFAULT,	// 너비.
        CW_USEDEFAULT,	// 높이.
        0,	// 부모 창.
        0,	// 메뉴 핸들.
        instanceHandle,	// 응용 프로그램 인스턴스.
        0);		// 추가적인 생성 플래그들
        
	if (ghMainWnd == 0)
    {
    	MessageBox(0, L"CreateWindow FAILED", 0, 0);
        return false;
    }
    
    // 창이 성공적으로 생성되었다고 해도 저절로 화면에 나타나는 것이 아니다.
    // 따라서, 마지막 단계는 방금 생성한 창을 표시하고 갱신하는 것이다.
    // 이를 위해 두 개의 함수 호출이 필요하다. 이들은 갱신할 창을, 주어진 핸들을 이용해 알아챈다.
    ShowWindow(ghMainWnd, show);
    UpdateWindow(ghMainWnd);
    
    return true;
}

int Run()
{
	MSG msg = {0};
    
    // WM_QUIT 메시지를 받을 때까지 루프를 돌린다.
    // GetMessage() 함수는 WM_QUIT 메시지를 받은 경우에만 0을 반환하고 루프를 종료한다.
    // GetMessage()는 메시지 수신 오류가 있었으면 -1을 반환한다.
	// 또한 GetMessage()를 호출하면 메시지가 도달할 때까지 응용 프로그램 스레드가 수면 상태가 된다.
    BOOL bRet = 1;
    while ((bRet = GetMessage(&msg, 0, 0, 0)) != 0)
    {
    	if (bRet == -1)
        {
        	MessageBox(0, L"GetMessage FAILED", L"Error", MB_OK);
            break;
        }
        else
        {
        	TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }
    
    return (int)msg.wParam;
}

LRESULT CALLBACK
WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
	// 몇 가지 구체적인 메시지들을 처리한다. 여기서 메시지를 처리했다면 반드시 0을 반환해야 함.
    switch(msg)
    {
    // 왼쪽 마우스 버튼이 눌렸으면 메시지 상자를 표시한다.
    case WM_LBUTTONDOWN:
    	MessageBox(0, L"Hello, World", L"Hello", MB_OK);
        return 0;
    // Esc 키가 눌렸으면 응용 프로그램 주 창을 파괴한다.
    case WM_KEYDOWN:
    	if (wParam == VK_ESCAPE)
        	DestroyWindow(ghMainWnd);
		return 0;
    // 파괴 메시지의 경우에는 종료 메시지를 보낸다. 
    // 그러면 결과적으로 메시지 루프가 종료된다.
    case WM_DESTROY:
    	PostQuitMessage(0);
		return 0;
    }
    // 여기서 명시적으로 처리하지 않은 다른 메시지들은 기본 창 프로시저,
    // 즉 DefWindowProc에게 넘겨준다. 지금 이 창 프로시저가 반드시
    // DefWindowProc의 반환값을 돌려주어야 함.
    return DefWindowProc(hWnd, msg, wParam, lParam);
}

설명이 좀 필요한 코드들을 다시 구체적으로 알아보자.

HWND

HWND 형식의 전역 변수는 'handle to a window', 즉 창의 핸들(window handle)을 의미한다. 무슨 역할을 하는가? 작업의 대상이 되는 창을 식별하기 위해 핸들을 요구하는 함수들이 많아서 그때 매개변수로 쓰인다.

WinMain

int main()의 역할과 비슷한 WinMain을 보자.

int WINAPI
WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
PSTR pCmdLine, int nShowCmd)
  1. hInstance : 현재 응용 프로그램 인스턴스의 핸들. 이 응용프로그램을 식별하고 지칭하는 수단으로 쓰인다.
  2. hPrevInstance : Win32 프로그래밍에는 쓰이지 않아서 0 지정.
  3. pCmdLine : 프로그램을 실행하는 데 쓰인 명령줄 인수 문자열.
  4. nCmdShow : 응용 프로그램의 표시 방식을 지정하는 '표시 명령'이다. 예로 최대 크기, 최소 크기 등이 있다.

WinMain 함수가 성공적으로 실행을 마쳤다면 반드시 WM_QUIT 메시지의 wParam 멤버를 반환해야 한다. 만일 메시지 루프에 진입하지도 못하고 함수를 마쳐야 한다면 0을 돌려주어야 한다. WinMain의 반환 형식은 다음과 같다.

#define WINAPI		__stdcall

이는 함수의 호출 규약(calling convention)을 지정하는 것으로, 호출 규약은 간단히 말하면 함수 인수들을 스택에 넣는 방식과 관련된 것이다.

WNDCLASS 구조체와 창 클래스 등록

InitWindowsApp 함수 호출에서는, 응용 프로그램의 초기화를 수행한다. 이는 창의 기본적인 속성들을 서술하는 WNDCLASS 구조체의 인스턴스를 채운다. 정의는 다음과 같다.

typedef struct tagWNDCLASS {
    UINT        style;
    WNDPROC     lpfnWndProc;
    int         cbClsExtra;
    int         cbWndExtra;
    HINSTANCE   hInstance;
    HICON       hIcon;
    HCURSOR     hCursor;
    HBRUSH      hbrBackground;
    LPCWSTR     lpszMenuName;
    LPCWSTR     lpszClassName;
} WNDCLASS
  1. style : 창의 스타일을 지정한다. 자세한건 MSDN 라이브러리 참고.
  2. lpfnWndProc : 이 WNDCLASS 인스턴스에 연관될 창 프로시저 함수를 가리키는 포인터이다.
  3. cbClsExtre와 cbWndExtra : 이들은 응용 프로그램 고유의 목적으로 사용할 수 있는 추가적인 메모리 슬롯들이다. 지금 예제는 추가적인 공간을 사용하지 않으므로 둘 다 0을 지정한다.
  4. hInstance : 응용 프로그램 인스턴스의 핸들이다.
  5. hIcon : 생서된 창에 사용할 아이콘의 핸들이다. 종류들은 MSDN 라이브러리 참고.
  6. hCursor : 마우스 위치의 모양을 말한다. 종류들은 MSDN 라이브러리 참고.
  7. hbrBackground : 창의 클라이언트 영역의 배경색을 결정하는 브러시(brush, 붓)의 핸들을 지정한다. 종류들은 MSDN 라이브러리 참고.
  8. lpszMenuName : 창의 메뉴를 지정한다.
  9. lpszClassName : 창 클래스의 이름이다. 어떤 이름이든 가능.

WNDCLASS 구조체를 다 채웠으면, Windows에 등록해야 한다. 그제서야 바로 이 창 클래스를 기반으로 하여 창을 생성할 수 있다. 등록은 RegisterClass 함수로 하는데, 성공하면 WNDCLASS 구조체를 가리키는 포인터를 얻고, 실패시엔 0을 돌려준다.

창의 생성과 표시

창 클래스를 Windows에 등록했다면, 창을 생성할 수 있다. CreateWindow 함수를 이용한다.

HWND CreateWindow(
	LPCTSTR lpClassName,
    LPCTSTR lpWindowName,
    DWORD dwStyle,
    int x,
    int y,
    int nWidth,
    int nHeight,
    HWND hWndParent,
    HMENU hMenu,
    HANDLE hhInstance,
    LPVOID lpParam
);
  1. lpClassName : WNDCLASS 인스턴스를 통해 등록된 창 클래스의 이름.
  2. lpWindowName : 창의 이름. 창의 제목줄에도 나타난다.
  3. dwStyle : 창의 스타일을 지정한다. 좀 더 자세한 목록은 MSDN 라이브러리 참고.
  4. x : 창의 왼쪽 상단 모퉁이의 x 위치. CW_USEDEFAULT는 Windows가 알아서 적절한 기본값 선택.
  5. y : 창의 왼쪽 상단 모퉁이의 y 위치. CW_USEDEFAULT는 Windows가 알아서 적절한 기본값 선택.
  6. nWidth : 창의 너비로, 단위는 픽셀이다. CW_USEDEFAULT는 Windows가 알아서 적절한 기본값 선택.
  7. nHeight : 창의 높이로, 단위는 픽셀이다. CW_USEDEFAULT는 Windows가 알아서 적절한 기본값 선택.
  8. hWndParent : 이 창의 부모에 해당하는 창의 핸들이다. 지금은 관계 있는 창이 없으므로 0 지정.
  9. hMenu : 메뉴 핸들. 메뉴를 사용하지 않으면 0.
  10. hInstance : 이 창이 속한 응용 프로그램 인스턴스의 핸들이다.
  11. lpParam : 응용 프로그램 고유의 목적을 위한 사용자 정의 자료를 가리키는 포인터로, WM_CREATE 메시지에 포함되어서 메시지 처리부(창 프로시저)에 전달된다.

그 다음 두 함수는 창을 화면에 적절히 표시하기 위한 것이다.

ShowWindow(ghMainWnd, show);
UpdateWindow(ghMainWnd);

둘 다 창의 핸들을 매개변수로 받고, ShowWindow함수의 제2 매개변수로는 WinMain의 한 매개변수인 nShowCmd를 지정하는 것이 정석이다. 여기까지 초기화가 제대로 되면 true를 반환한다.

메시지 루프

초기화를 성공적으로 마쳤다면 프로그램의 심장부인 메시지 루프로 진입한다. 메시지 루프가 처음으로 하는 일은 MSG 형식의 변수 msg를 초기화하는 것이다. MSG는 Windows의 메시지 하나를 대표하는 구조체다.

typedef struct tagMSG {
    HWND        hwnd;
    UINT        message;
    WPARAM      wParam;
    LPARAM      lParam;
    DWORD       time;
    POINT       pt;
} MSG
  1. hwnd : 메시지를 받을 창 프로시저에 연관된 창의 핸들.
  2. message : 메시지의 종류를 나타내는 미리 정의된 상수 값.
  3. wParam : 메시지에 관한 추가 정보.
  4. lParam : 메시지에 관한 추가 정보.
  5. time : 메시지가 전송된 시간.
  6. pt : 메시지가 전송되었을 때의 마우스 커서의 (x,y) 좌표.

그런 다음에서야 실제로 메시지 루프를 시작한다. GetMessage 함수는 메시지 대기열에서 메시지 하나를 가져 와서, 그 메시지의 세부사항을 msg 인수에 채워 넣는다. 뭔가 오류가 발생하면 -1를 반환한다. 받은 메시지가 WM_QUIT이면 0을 반환하고, 그러면 메시지 루프가 종료된다. 그 이외의 값을 반환한다면, 메시지 루프는 두 개의 함수를 더 호출한다. TranslateMessage와 DispatchMessage가 바로 그것이다. TranslateMessage는 Windows가 모종의 키보드 변환을 수행하게 한다. 좀 더 구체적으로 말하면 가상 키 코드를 문자 메시지로 변환한다. DispatchMessage 함수를 호출하면 비로소 메시지가 해당 창 프로시저로 배분된다.

PeekMessage 함수

메시지 루프에서 메시지를 가져올 때 GetMesaage 함수를 사용했다. 하지만 사실 이보다 더 좋은 함수가 있다. PeekMessage이다. 왜? GetMessage 함수는 메시지 대기열에 메시지가 없으면 스레드를 수면 상태로 전환하는데, 대체로 게임은 메시지가 올 때까지 가만히 있는 게 아니라 끊임없이 갱신된다. PeekMessage는 메시지가 없으면 게임 자체의 코드를 실행한다.

코드 예시.

if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
	if (msg.message == WM_QUIT)
		break;
	TranslateMessage(&msg);
	DispatchMessage(&msg);
}
else	// 게임 자체 코드 실행.
{
	Graphics::Get()->Begin();	
	{
       	// 작업한다.
	}
	Graphics::Get()->End();
}

창 프로시저

창 프로시저는 특정 메시지에 반응해서 실행할 코드를 담은 함수이다. 원형은 이렇다.

LRESULT CALLBACK
WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam);

이 함수의 반환 형식인 LRESULT는 내부적으로 정수 형식으로 정의되고 함수의 성공 여부를 나타낸다. CALLBACK 지정자는 Windows가 이 함수를 호출할 것임을 나타낸다.

  1. hWnd : 메시지를 받는 창의 핸들.
  2. msg : 메시지의 종류를 나타내는 미리 정의된 상수 값. 예를 들어 WM_QUIT는 종류 메시지다. WM는 "Window Message"를 뜻한다. 자세한 사항은 MSDN 라이브러리 참고.
  3. wParam : 메시지에 관한 추가 정보로, 구체적인 사항은 메시지 종류에 따라 다를 수 있다.
  4. lParam : 메시지에 관한 추가 정보로, 구체적인 사항은 메시지 종류에 따라 다를 수 있다.

MessageBox 함수

MessageBox 함수는 다음과 같이 메시지를 띄워주는 박스를 생성해준다.

int MessageBox(
	HWND hWnd,	// 메시지 상자가 속한 창. NULL을 지정할 수도 있다.
    LPCTSTR lpText,	// 메시지 상자에 표시할 텍스트.
    KPCTSTR lpCaption,	// 메시지 상자의 제목으로 표시할 텍스트.
    UINT uType	// 메시지 상자의 스타일.
);
profile
Question, Think, Select

0개의 댓글