목표
저번 포스팅에 이어 WIN32 API의 기본구조에 대해서 이해하는 시간을 갖겠습니다.
개요
저번 포스팅에서 Win32 API 프로젝트를 생성하는 방법과 코드 일부분을 살펴보았습니다. 헤더와 전역 변수 및 함수에 대해서 알아보았습니다. 이어서 이번 포스팅에서는 Win32 API의 핵심인 WinMain과 WndProc 함수에 대해서 알아보도록 하겠습니다.
WinMain 함수
모든 프로그램에는 Main 함수가 존재합니다. Main 함수의 역할은 여러가지가 있겠지만, 가장 큰 역할은 프로그램의 진입점을 잡아주는 것이라고 생각합니다. 마찬가지로 Win32 API 프로그램도 프로그램의 진입점을 잡아줄 Main 함수가 필요합니다. 그리고 Main문에서 윈도우를 디자인하고 생성하는 역할을 합니다. 코드로 확인하기 전에 WinMain 함수의 역할을 확실하게 짚고 넘어가도록 하겠습니다.
WinMain 함수의 역할
WinMain 함수는 다음과 같이 5개의 기능을 합니다.
1. WndClass 정의
WndClass는 윈도우 클래스를 의미합니다. 윈도우의 기반이 되는 클래스를 윈도우 클래스라고 합니다. WndClass 정의는 만들고자 하는 윈도우의 속성을 정의하는 것을 의미합니다. 윈도우의 배경색은 무엇인지, 아이콘은 어떤 것을 쓸지, 아이콘은 어떻게 생겼는지, 윈도우 스타일은 어떤 것인지와 같은 속성을 정의 할 수 있습니다. 위와 같은 디자인뿐만 아니라, 윈도우에서 발생하는 메세지는 어떤 함수에서 처리할지, 해당 인스턴스(프로그램)의 핸들은 무엇인지와 같은 정보들을 정의할 수 있습니다.
2. WndClass 등록
윈도우의 속성을 정의하고 나면 윈도우 클래스를 등록해야 합니다. 정의된 윈도우를 메모리에 올리려면 윈도우 클래스가 등록되어 있어야 합니다. 그럼, 메모리에 등록하는 것도 아니고 어디에 등록하는 것일까요? 바로 커널에 등록하는 것입니다. 운영체제가 윈도우 클래스를 인지할 수 있도록 등록하는 과정을 거치는 것입니다.
3. 윈도우 생성
윈도우 클래스가 커널에 등록되면 메모리에 윈도우 클래스를 올릴 수 있습니다. 메모리에 윈도우를 만드는 것을 "윈도우 생성"이라고 합니다. 메모리에 윈도우가 생성되면 이에 대한 핸들값을 알 수 있습니다.
4. 윈도우 출력
윈도우가 커널에 등록되고 메모리에 생성되면 이제 사용자에게 보여줘야 합니다. 시각화(화면에 출력)하는 것도 WinMain 함수의 역할입니다.
5. 메세지 루프
화면에 출력되고 사용자가 사용하기 시작하면 여러가지 메세지(이벤트)가 발생됩니다. 메세지가 언제 들어올지 알 수 없기 때문에 컴퓨터는 메세지 루프를 돌면서 메세지가 발생하는 것을 기다리고 있습니다. WinMain에서 메세지가 발생할 때까지 메세지 루프를 돌다가, 메세지가 발생하면 해당 메세지를 메세지 처리 전용 함수(윈도우 프로시저, WndProc)로 메세지를 전달합니다. 메세지 큐에 쌓여있는 메세지를 꺼내와 윈도우 프로시저에 전달하게 됩니다.
WinMain의 파라미터와 변수 선언
WinMain의 역할에 대해서 알아보았습니다. 이제, 코드로 확인해보도록 하겠습니다. 파라미터와 변수 선언까지 알아보고 위에서 알아본 WinMain의 역할에 맞춰 코드로 확인해보겠습니다.
APIENTRY
조금 낯선 키워드가 보입니다. 반환형과 함수 이름 사이에 APIENTRY라는 키워드가 있습니다. APIENTRY는 윈도우의 표준 호출 규약인 _stdcall을 사용한다는 의미입니다. _stdcall의 특징은 파라미터의 처리를 호출된 쪽에서 한다는 것입니다. 원래 우리가 함수를 사용할 때, 인수를 포함한 함수를 사용할 때 처리된 값을 넣어서 함수를 호출하게 됩니다. 반면, _stdcall을 사용하는 WinMain은 발생한 메세지를 전달만 하고 메세지에 대한 처리는 메세지 처리 전용 함수인 윈도우 프로시저에서 하게 되는 것입니다.
인수
메세지에 대한 얘기는 윈도우 프로시저로 미루고, 파라미터(매개변수)로 넘어가도록 하겠습니다. WinMain의 파라미터에 대해 얘기하기 전에, 또 낯선 키워드가 보입니다. 바로 _In_과 _In_opt_ 입니다. 이것은 Visual Studio가 2019로 넘어오면서 파라미터에 접두사를 추가해서 작성하는 것을 권장하기 때문에 사용하는 것입니다. 마치 scanf를 scanf_s로 사용하는 것과 같습니다. 각설하고, WinMain은 다음과 같이 4개의 파라미터를 갖습니다. 첫번째 파라미터를 제외한 3개는 거의 사용되지 않습니다.
1. HINSTANCE hInstance
프로그램의 인스턴스(프로그램) 핸들입니다. 코드의 4번째 줄을 보시면 g_hInst = hInstance; 와 같은 구문이 있습니다. 이전 포스팅에서 인스턴스의 핸들(g_hInst)을 전역 변수로 선언한 것을 확인했습니다. 인스턴스의 핸들을 WinMain에서 선언해버리면 WinMain에서만 사용할 수 있습니다.(지역 변수) 하지만, 많은 API 함수들이 인스턴스 핸들값이 필요한 경우가 많습니다. 그래서 전역 변수로 만들어 hInstance(인스턴스 핸들)값을 넘겨주는 것입니다.
정리하면, 파라미터로 받은 인스턴스의 핸들 값을 전역 변수에 전달해서 다른 함수들이 사용할 수 있도록 하는 것입니다.
참고-[Windows API] 윈도우 프로젝트 생성과 WIN32 API의 기본 구조-(1)
2. HINSTANCE hPrevInstance
hPrevInstance는 이전에 실행된 프로그램의 인스턴스 핸들입니다. 이전에 실행된 프로그램이 없을 경우 NULL을 갖습니다. 지금은 사용되지 않는 파라미터입니다. 호환성을 위해서 존재하는 파라미터이므로 신경쓰지 않아도 됩니다. 따라서, WIN32에서는 항상 NULL값입니다.
3. int lpCmdLine
명령행(도스,터미널)으로 입력된 프로그램 인수입니다. 도스의 argv 인수에 해당됩니다. 마찬가지로 거의 사용되지 않습니다.
4. int nCmdShow
프로그램이 실행될 형태입니다. 최소화, 보통 상태일 때 모양이 전달됩니다.
파라미터에 대해서 알아보았습니다. 다음은 변수 선언입니다.
HWND hWnd; // 윈도우의 핸들을 선언합니다. 윈도우 핸들 구조체에 관련 정보가 담겨있습니다.
MSG Message; // 메세지 구조체를 선언합니다. 메세지에 대한 정보가 구조체에 담겨 있습니다.
WNDCLASS WndClass; // 윈도우 클래스를 선언합니다. 다음이 이 윈도우 클래스를 정의하는 부분입니다.
g_hInst = hInstance; // 파라미터로 받은 인스턴스의 핸들을 전역 변수에 저장하는 것입니다.
아직까지 구체적인 내용을 다룰 단계는 아닌 것 같아서 이정도로 마무리 하도록 하겠습니다. 추후에 수준이 더 올라오면 구체적으로 다루는 시간을 가지도록 하겠습니다.
윈도우 클래스 정의
위에서 윈도우 클래스를 선언했습니다. (WNDCLASS WndClass;) 이제 이 윈도우의 속성을 정의해야합니다.
10개의 속성을 정의하는 부분입니다. WNDCLASS 구조체의 멤버변수에 값을 저장하는 모습을 볼 수 있습니다.
표의 형태로 확인해보도록 하겠습니다.
배경 색상, 아이콘, 커서, 메뉴에 대한 세부사항은 앞으로 직접 만들어보면서 다룰 예정이므로 표를 통해 간단하게 이해하고 넘어가겠습니다.그 외에는 짚고 넘어가도록 하겠습니다. 먼저, hInstance는 윈도우 클래스를 등록하는 프로그램의 번호로, 운영체제는 윈도우 클래스를 누가 등록했는지 기억하고 있다가 프로그램이 종료될 때 등록을 취소합니다. lpfnWndProc은 만들어지는 윈도우에서 발생하는 모든 메세지를 처리하는 전용 함수를 지정하는 것입니다. 마지막으로 lpszClassName은 윈도우 클래스의 이름을 정의하는 것입니다. 위에서 전역변수에 lpszClass에 문자열을 지정해줬는데요. 코드를 보시면 해당 문자열을 그대로 이름으로 쓰는 것을 알 수 있습니다. 주의할 점은, 윈도우 클래스의 이름과 생성한 프로젝트의 이름이 겹치면 안된다는 것입니다. 추후 컨트롤에 대해 다룰 때 다시 한 번 짚도록 하겠습니다.
윈도우 클래스 등록
위에서 정의한 윈도우 클래스를 커널에 등록하는 과정을 거칠 차례입니다.
RegisterClass 함수를 호출해서 등록을 할 수 있습니다. 매개변수로 생성한 윈도우 클래스 구조체의 주소를 넘겨줍니다. 이렇게 등록된 윈도우 클래스는 메모리에 올려서 사용할 수 있습니다.
윈도우 생성
등록된 윈도우를 메모리에 생성하는 과정입니다.
CreateWindow 함수를 통해서 윈도우를 생성할 수 있습니다. 함수의 결과를 미리 선언되어 있던 hWnd(윈도우 핸들)에 저장합니다. 함수의 매개변수가 조금 많은데, 원형을 확인하고 한 개씩 확인해보도록 하겠습니다.
CreateWindow 함수의 원형
HWND CreateWindow(①lpszClassName, ②lpszWindowName, ③dwStyle,
④x, ⑤y, ⑥nWidth, ⑦nHeight, ⑧hWndParent, ⑨hMenu, ⑩hInst, ⑪lpvParam)
①lpszClassName
생성하는 윈도우 클래스를 지정하는 문자열입니다. 이전에 전역 변수에 클래스 이름을 lpszClass에 넣었습니다. 이걸 매개변수로 전달합니다.
②lpszWindowName
생성되는 윈도우의 타이틀 바에 나타나는 문자열입니다. 마찬가지로 타이틀바에 클래스 이름을 출력하는 예제이므로 lpszClass를 넣습니다.
③dwStyle
윈도우의 형태를 지정하는 것입니다. 현재 지정되어 있는 WS_OVERLAPPEDWINDOW는 메모장과 유사한 형태로 가장 무난한 윈도우 스타일입니다. 이렇게 윈도우 스타일은 접두사로 WS_가 붙는 모습을 볼 수 있습니다.
④x, ⑤y, ⑥nWidth, ⑦nHeight
생성되는 윈도우의 x, y 좌표와 넓이와 높이입니다. 현재 CW_USEREFAULT라는 값이 들어가 있는데, 이는 운영체제가 판단하길 가장 적합한 위치와 크기를 맞춰서 출력하는 것입니다.
⑧hWndParent
생성되는 윈도우의 부모 윈도우입니다. 지금은 메인 윈도우를 생성하고 있기 때문에 부모 윈도우가 존재하지 않아 NULL값을 전달합니다. 하지만, 컨트롤과 같은 부모 윈도우를 가지는 차일드 윈도우를 생성할 때는 부모 윈도우를 넣어주게 됩니다.
⑨hMenu
윈도우에서 사용할 메뉴의 핸들입니다. 메뉴의 핸들값을 HMENU로 형변환해서 넣어주게 됩니다. 예제는 NULL값이 들어가있는데, 이는 윈도우 클래스의 속성을 정의할 때 정한 메뉴를 사용하는 것입니다.
⑩hInst
윈도우를 만들어주는 주체인 프로그램의 핸들을 지정하는 것입니다. WinMain함수의 매개변수로 받은 hInstance를 넣었습니다.
⑪lpvParam
CREATESTRUCT라는 구조체의 주소로, 특별한 목적이 있을 때만 사용합니다. 보통 NULL값을 사용합니다.
윈도우 정보 전달 및 윈도우 출력
순서대로 가면 윈도우를 화면에 출력해야 합니다. 그 전에 할 일이 있습니다. 우리는 메인 윈도우의 핸들을 전역변수로 선언했습니다. 그리고 WinMain에서 윈도우를 정의하고 등록하는 과정을 거쳤습니다. 메인 윈도우의 핸들을 전역변수로 선언한 이유는 다른 API 함수들도 윈도우 핸들의 정보가 필요한 경우가 있기 때문입니다.
따라서, 우리는 생성된 윈도우의 정보를 전역변수에 전달해줘야합니다. 그 과정은 다음과 같습니다.
hWndMain = hWnd;
이렇게 전역 변수에 윈도우 핸들을 전달하고 나면 화면에 출력할 차례입니다. 이 과정도 한 개의 함수로 해결할 수 있습니다.
ShowWindow(hWnd, nCmdShow);
두 개의 인수가 들어가게 되는데, 첫 번째 인수는 hWnd로, 출력될 윈도우의 핸들입니다. CreateWindow를 통해서 생성된 윈도우의 핸들을 넘겨주면 됩니다. 두 번째 인수는 nCmdShow입니다. WinMain의 네 번째 파라미터로 받았던 것 기억 나시나요? 이 변수는 윈도우를 화면에 출력하는 방법을 지정하는 역할을 합니다. 다양한 매크로 상수들이 정의되어 있습니다. 예를 들면, SW_HIDE(윈도우를 숨김), SW_SHOW(윈도우를 활성화해 보여줌), SW_MINIMIZE(윈도우 최소화 및 활성화하지 않음) 과 같이 ShowWindow와 관련된 매크로 상수들이 정의되어 있습니다.
메세지 루프
WinMain의 마지막 역할인 메세지 루프입니다. 윈도우가 생성되고, 등록되고, 출력되면 사용자가 사용할 수 있습니다. 이 때 발생하는 메세지(이벤트)를 받기 위해 루프를 계속 돌고, 메세지가 발생하면 윈도우 프로시저로 전달합니다. 해당 과정은 다음과 같습니다.
메세지 루프와 함수의 끝인 리턴입니다. 메세지 루프에 진입해서 전달하는 과정까지 살펴보도록 하겠습니다.
메세지 루프 진입
먼저, 반복문의 조건을 보면 GetMessage라는 함수가 보입니다. 해당 함수는 메세지 큐에서 메세지를 읽어들여서 첫번째 인수가 지정하는 메세지 구조체에 저장하게 됩니다. 우리는 위에서 MSG라는 구조체의 Message를 선언했습니다. 이것의 주소를 첫번째 인수로 넘겨주게 됩니다. 메세지를 읽었을 때, WM_QUIT라는 메세지를 읽게 되면 False를 리턴하게 되고 메세지 루프가 종료됩니다. 그 외의 메세지들은 True를 반환해서 메세지 루프를 돌게됩니다. 그 외에 두번째 인수는 메세지를 전달 받을 윈도우의 정보이고, 3~4번째 인수는 메세지 필터와 관련된 정보입니다. 첫 번째 인수 외에는 잘 사용되지 않는 경향이 있습니다.
메세지 전달
메세지 루프에 들어오게 되면 두 개의 함수가 보입니다. 먼저, TranslateMessage는 문자열이 들어오면 문자열 처리를 위한 메세지를 발생시키는 역할을 합니다. 이 정도로만 알고, 이후 문자열에 관련된 내용을 다룰 때 더욱 심도있게 다루도록 하겠습니다. 다음 DispatchMessage입니다. 들어온 메세지를 윈도우 프로시저에 전달하는 역할을 합니다. TranslateMessage는 꼭 필요한 함수는 아니지만 DispatchMessage는 꼭 필요한 함수입니다.
정리
이렇게 WinMain 함수에 대해서 자세히 알아보았습니다. Win32 API는 처음 기본 구조를 잡는 것이 중요합니다. 가장 주요한 역할을 하는 WinMain, WndProc 함수에 대해서 자세히 알고 넘어가야 이후 추가적인 내용들을 배울 때 구조적으로 이해할 수 있습니다. 다음 포스팅에서는 전달받은 메세지를 처리하는 WndProc(윈도우 프로시저)함수에 대해서 알아보도록 하겠습니다.
'개발 > Win32 API Programming' 카테고리의 다른 글
[Windows API] Device Context란?, Win32 API를 활용해 문자열 출력하기 (0) | 2021.12.31 |
---|---|
[Windows API] Win32 API의 기본구조, 윈도우 프로시저 (0) | 2021.12.26 |
[Windows API] 윈도우 프로젝트 생성과 WIN32 API의 기본 구조-(1) (0) | 2021.11.03 |
[Windows API] Win32 API에서 제공하는 자료형(데이터 타입) 모음과 핸들(HANDLE) (0) | 2021.10.07 |
[Windows API] API, MFC란, 윈도우 프로그래밍 동작 방식 (1) | 2021.09.28 |