본문 바로가기
게임 공부/Windows API

[윈도우즈 API 정복] 1. 윈도우즈 프로그래밍

by woohyeon 2023. 5. 23.
반응형

Windows와 Windows API

다 알겠지만 Windows는 마이크로소프트에서 개발한 운영체제이다.
윈도우즈 이전엔 주로 MS-DOS(Disk Operating System)가 사용 되었으며, 가장 표면적인 차이점은 윈도우즈 OS는 사용자 입장에서 직관적인 GUI 환경의 운영체제라는 점이다.

운영체제는 응용 프로그램이 하드웨어 위에서 잘 동작할 수 있도록 도와주는 시스템 소프트웨어이다.
응용 프로그램이 디스크에 존재하는 파일을 읽고 싶다면 하드웨어에게 명령을 내려야 하는데, 이를 운영체제가 도와준다.

단순한 파일 읽기라도 내부적으론 많은 복잡한 과정(명령)을 거치게 되는데, 윈도우즈는 이러한 복잡한 명령 집합을 랩핑하여 간단한(상대적으로) 인터페이스를 구현해 놓았고, 우리는 이를 통해 파일을 쉽게 읽을 수 있다.

이처럼 하드웨어와의 소통을 위해 윈도우즈가 제공하는 인터페이스(함수) 들의 집합을 Windows API라 한다.
윈도우즈 환경에서 실행되는 프로그램은 하드웨어에 명령을 내리기 위해 당연히 내부적으로 WinAPI를 사용해야 하며, 이를 위해선 윈도우즈 환경의 특징, 구조 등을 잘 이해하고 기본적인 API에 대한 이해가 필요하다.
-> 더 효율적이고 성능 좋은 프로그램을 위해서라면..



윈도우즈 프로그램의 진입점

일반적인 콘솔 프로그램의 진입점(엔트리 포인트)은 main 함수이다.
윈도우즈 프로그램은 콘솔 기반이 아닌 GUI 기반의 프로그램이기 때문에 진입점이 다르며, 다음과 같은 WinMain이란 이름의 함수에서 프로그램이 시작된다.

int APIENTRY WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow)
{
    ...
}

아래에서 보겠지만 윈도우즈는 메시지 기반 프로그램이기에 WinMain에 주요 로직이 있진 않다.

윈도우 메시지 관련해서 이해가 필요한데, 이에 대해선 다음 링크에 포스팅이 되어 있다.

https://woo-dev.tistory.com/121

 

윈도우 클래스

윈도우는 프로그램에 따라 여러 특성을 가질 수 있다.
예를 들면 윈도우의 이름, 크기, 위치, 메뉴 등이 있는데 이러한 특성을 윈도우 클래스란 구조체에 저장하고 등록해야 한다.
윈도우 생성 시 등록한 윈도우 클래스를 지정하면 해당 클래스를 참고하여 윈도우를 생성하게 된다.

윈도우 클래스 등록을 위해선, 먼저 다음과 같은 구조체 변수를 하나 선언하고 데이터를 채워야 한다.

struct WNDCLASS {
    UINT        style;          

    WNDPROC     lpfnWndProc;    // 해당 윈도우 클래스로 생성한 윈도우의 메시지 처리 함수이다.
                                // 우리가 선언하고 구현해야 하는 함수이다.

    int         cbClsExtra;     // 예약 영역이며 항상 0

    int         cbWndExtra;     // 예약 영역이며 항상 0

    HINSTANCE   hInstance;      // 이 윈도우 클래스를 등록하는 프로그램의 번호로 운영체제가 이를 기억해두고, 
                                // 부모 프로그램 종료 시 이 윈도우 클래스의 등록을 취소한다.
                                // 보통 WinMain의 인자로 전달된 hInstance를 그대로 이용

    HICON       hIcon;          // 해당 윈도우가 사용할 기본 아이콘

    HCURSOR     hCursor;        // 해당 윈도우가 사용할 기본 마우스 커서

    HBRUSH      hbrBackground;  // 윈도우의 배경색에 사용할 브러쉬이다. 
                                // 예를 들면 하얀색 브러쉬를 지정하면 하얀 배경의 윈도우가 만들어진다.

    LPCWSTR     lpszMenuName;   // 메뉴를 지정한다고 하는데, 사용하지 않을 경우 NULL

    LPCWSTR     lpszClassName;  // 해당 윈도우 클래스의 이름이다.
                                // 윈도우 생성 시 어떤 윈도우 클래스를 참조하여 만들지 결정하는데, 이때 해당 값을 통해 
                                // 윈도우 클래스를 식별한다.
}

데이터가 되게 많지만 단순 세팅을 위한 데이터가 대부분이다.
중요한 데이터로는 lpfnWndProc과 lpszClassName이 있다.



윈도우 생성

윈도우 클래스를 등록했다면 이제 윈도우를 생성할 수 있다.

윈도우 생성은 다음과 같은 함수를 통해 수행한다.

HWND CreateWindow(
    lpszClassName,          // 사용할 윈도우 클래스(등록한)의 이름을 지정한다.

    lpszWindowName,         // 해당 윈도우의 타이틀에 표현할 제목을 지정한다.

    dwStyle,                // 윈도우는 최소화, 최대화 버튼 등을 가질 수 있는데, 이러한 옵션이 플래그로 존재한다.
                            // 각 플래그를 조합하여 전달한다.

    x, y, nWidth, nHeight,  // 해당 윈도우의 위치 및 크기 정보이다.

    hWndParent,             // 부모 윈도우를 지정한다. 
                            // 예를 들면 메인 윈도우 위에 뜬 팝업 윈도우 생성 시 이 값을 메인 윈도우로 지정할 수 있다.
                            // NULL로 지정할 경우 데스크탑을 부모로 가진다. 

    hMenu,                  // 등록한 윈도 클래스에 지정한 메뉴말고 다른 메뉴를 지정하고 싶을 경우 지정해준다. (or NULL)
    hInstance,              // 윈도우 클래스에 등록한 hInstance와 동일한 의미를 가지는 매개변수이다.
    lpParam                 // 여러 개의 윈도우를 만들 때 각 윈도우에 전달할 파라미터라고 한다. 현재는 NULL 지정
)

윈도우는 위의 함수를 통해 메모리에 생성되며, 생성된 윈도우에 대한 식별자인 윈도우 핸들이 반환된다. 해당 함수는 메모리에 윈도우 정보를 올려놓기만 한 것이기 때문에 아직 화면에 보여지지 않는다. 따라서 다음의 함수를 통해 윈도우를 화면에 띄워야 한다.

BOOL ShowWindow(hWnd, nCmdShow)





메시지 루프

윈도우를 생성했다면 해당 윈도우에서 발생한 키 입력, 마우스 입력 등이 메시지 형태로 해당 윈도우로 전달된다.
이 메시지는 윈도우 클래스에 지정한 WndProc 함수에 의해 처리 되며, 각 메시지마다 수행할 동작은 사용자가 정의한다.

메시지 루프는 보통 다음과 같은 형태를 가진다. (슈도코드)

while(GetMessage(&Msg, NULL, 0, 0))
{
    TranslateMessage(&Msg);
    DispatchMessage(&Msg);
}

 

GetMessage

메시지는 메시지 큐란 곳에 쌓이게 되는데, GetMessage 함수는 메시지 큐에서 메시지 하나를 꺼내온다.
GetMessage는 WM_QUIT이라는 메시지를 꺼내왔을 때만 FALSE를 반환하며 이외의 모든 메시지에 대해선 TRUE를 반환한다.
즉 위 루프는 WM_QUIT이라는 메시지가 발생하면 루프를 종료하고 아니라면 계속해서 루프를 돌게 된다.
따라서 WM_QUIT은 프로그램 종료 메시지를 의미하며, 어디선가 PostQuitMessage 함수를 호출하면 WM_QUIT 메시지가 발생한다.

메시지 큐에서 메시지를 가져오는 또 다른 함수로는 PeekMessage가 있다. GetMessage의 경우 메시지 큐에 메시지가 없다면 메시지가 발생할 때까지 리턴하지 않고 기다린다. (블로킹, 동기)

반면 PeekMessage는 메시지 큐에 메시지가 없더라도 기다리지 않고 바로 리턴한다. (논블로킹, 비동기)
만약 메시지가 있다면 0이 아닌 값을 반환하고, 없다면 0을 반환한다.
따라서 루프 구조가 다음과 같이 살짝 달라진다.

while(true)
{
    if(PeekMessage(&Msg)) // 처리할 메시지가 있다면
    {
        if(Msg == WM_QUIT)
        {
            break;
        }

        TranslateMessage(&Msg);
        DispatchMessage(&Msg);
    }
    else // 처리할 메시지가 없다면
    {
        // 다른 작업 수행
    }
}

PeekMessage는 보통 게임과 같이 프로그램이 블로킹 되지 않고 계속해서 다른 작업을 수행해야 할 때 사용한다.

 

TranslateMessage

가져온 메시지가 키 입력일 경우 어떤 Key가 눌렸는지 알 수 있도록 가공해주는 동작이 필요한데, 이 함수가 이를 수행한다.
예를 들어 키가 눌렸을 때 WM_KEYDOWN 메시지가 발생하는데, 해당 함수를 만나면서 WM_CHAR 메시지를 메시지 큐에 추가한다.
따라서 다음 메시지를 가져올 때 WM_CHAR 메시지를 얻을 수 있으며, 이 WM_CHAR 메시지 안에 눌린 키의 문자 코드가 포함 된다.

 

DispatchMessage

메시지를 메시지 처리 함수인 WndProc(윈도우 프로시저)로 보낸다.
해당 함수는 메시지만 보내며, WndProc은 운영체제에 의해 호출된다.
그러나 일부 메시지는 메시지 큐에 들어가지 않고 바로 WndProc으로 전달되는데, 그런 경우 해당 함수에 의해 보내지지 않는다.

 

윈도우 프로시저(WndProc)

윈도우 메시지를 처리하는 함수이다.
DispatchMessage를 통해 받은 메시지를 처리하며, 운영체제에 의해 호출되는 콜백 함수이다.

해당 함수는 다음과 같은 시그니처를 가져야 하며 이름은 달라도 되지만 보통 WndProc이란 이름이 포함되는 것이 관례이다.

LRESULT WndProc(
  HWND   hWnd,
  UINT   Msg,
  WPARAM wParam,
  LPARAM lParam
)

hWnd는 이 메시지가 발생한 윈도우의 핸들이며, Msg는 처리할 메시지이다.
wParam과 lParam은 메시지 종류마다 다른데, 어떤 메시지의 경우 함께 전달하고 싶은 정보가 있을 수 있다.
그런 정보가 해당 인자에 저장된다. 예를 들어 키 입력 시 발생한 WM_CHAR 메시지는 wParam에 입력된 문자 코드가 담겨있다.




댓글