-
[Win32] Windows application 만들기C++/미분류 2024. 1. 25. 19:35
Win32 API(이하 WinAPI)는 C++에서 GUI개발을 위해 사용되는 대표적인 라이브러리 중 하나입니다.
이번 글에서는 WinAPI를 이용해 간단한 Window하나를 만들어보도록 하겠습니다.
이번 글에서는 Visual Studio 2022 (v143)를 사용합니다.
1. 프로젝트 생성 및 초기설정
더보기위와 같이 새 프로젝트를 생성합니다.
이후, Visual Studio창에서 Alt + Enter 혹은 위와 같이 Project 우클릭으로 Property Page로 들어갑니다.
Property Page 의 Linker-System에서 SubSystem을 Windows로 변경합니다.
이는 exe파일의 진입점을 명시하는 작업입니다. (Console과 Windows는 main함수에 차이가 있습니다.)
2. 헤더 파일 작성
더보기헤더 파일을 작성합니다.
HelloWindows.h
#pragma once #include <Windows.h> class MyApp { public: HRESULT initialize(HINSTANCE hInstance); void runMessageLoop(); private: static LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam); private: HWND myHwnd = nullptr; };
헤더 파일의 코드는 위와 같으며, 각각 다음과 같은 용도로 사용됩니다.
#pragma once
컴파일 시, 헤더 파일이 컴파일러에 한 번만 포함되도록 지정합니다.
여러 파일에서 include 되어도 한 번만 처리되어 빌드 시간을 줄일 수 있습니다.
이번 글에서는 한 번만 include되므로, 불필요합니다.
#include <Windows.h>
WinAPI라이브러리를 담고 있는 헤더입니다.
위 헤더를 포함시켜야 WinAPI를 사용할 수 있습니다.
class MyClass { ... }
이번 글에서 사용될 Application객체입니다.
public: HRESULT initialize(HINSTANCE hInstance); void runMessageLoop();
initialize함수는 객체를 초기화하며, Window 클래스를 생성합니다.
runMessageLoop함수는 메시지 루프를 실행합니다.
private: static LRESULT WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam);
메시지 루프를 처리하는 윈도우 프로시저 함수입니다.
윈도우 생성 시점 (이번 글에서는 initialize함수입니다.)에 생성되는 객체의 lpfnWndProc필드에 지정되어야 합니다.
이를 위해 해당 함수는 클래스 멤버 함수가 아니거나, static 멤버 함수여야 합니다.
이번 글에서는 static으로 지정했습니다.
private: HWND myHwnd = nullptr;
윈도우 핸들 필드입니다.
생성되는 윈도우를 OS가 관리하고, 식별하기 위해 사용되는 객체이며, 정수 값으로 이루어져 있습니다.
3. 구현 (WinMain)
더보기이제 헤더에 작성한 함수와, WinMain함수를 구현해야 합니다.
HelloWindows.cpp파일을 생성하고, 헤더파일을 포함시키겠습니다.
#include "HelloWindows.h"
윈도우 프로그램의 main함수는 main이 아닌 WinMain이며, 매개변수가 다릅니다.
이번 글에서 사용할 WinMain함수는 다음과 같습니다.
int WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR lpCmdLine, int nShowCmd ) { MyApp* app = new MyApp(); if (SUCCEEDED(app->initialize(hInstance))) { app->runMessageLoop(); } return 0; }
우리가 정의한 MyApp객체를 하나 생성합니다.
이후 이 객체의 초기화를 수행하며, 초기화가 성공적일 경우 메시지 루프를 수행합니다.
이후 문단은 HelloWindows.h에 선언한 함수의 구현입니다.
3. 구현 (initialize)
더보기우선 윈도우를 초기화하는 intialize함수를 구현하겠습니다.
HRESULT MyApp::initialize(HINSTANCE hInstance) { HRESULT hr; WNDCLASSEX winCls = { sizeof(WNDCLASSEX) }; winCls.style = CS_HREDRAW | CS_VREDRAW; winCls.lpfnWndProc = MyApp::WndProc; winCls.cbClsExtra = 0; winCls.cbWndExtra = sizeof(LONG_PTR); winCls.hInstance = hInstance; winCls.hbrBackground = nullptr; winCls.lpszMenuName = nullptr; winCls.hCursor = LoadCursor(nullptr, IDI_APPLICATION); winCls.lpszClassName = L"MyApp"; RegisterClassEx(&winCls); myHwnd = CreateWindowW( L"MyApp", L"MyAppName", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 640, 480, nullptr, nullptr, hInstance, this ); hr = myHwnd ? S_OK : E_FAIL; if (SUCCEEDED(hr)) { ShowWindow(myHwnd, SW_SHOWNORMAL); UpdateWindow(myHwnd); } return hr; }
initialize함수는 윈도우 클래스를 생성하고, 이를 등록하는 역할을 수행합니다.
위 코드를 상세히 살펴보도록 하겠습니다.
HRESULT MyApp::initialize(HINSTANCE hInstance) { HRESULT hr; ... return hr; }
HRESULT는 함수의 결과로 사용되는 값 객체 입니다.
initialize로직을 수행한 결과를 반환하며, 위 반환 결과에 따른 동작을 구현할 수 있습니다.
WNDCLASSEX winCls = { sizeof(WNDCLASSEX) }; winCls.style = CS_HREDRAW | CS_VREDRAW; winCls.lpfnWndProc = MyApp::WndProc; winCls.cbClsExtra = 0; winCls.cbWndExtra = sizeof(LONG_PTR); winCls.hInstance = hInstance; winCls.hbrBackground = nullptr; winCls.lpszMenuName = nullptr; winCls.hCursor = LoadCursor(nullptr, IDI_APPLICATION); winCls.lpszClassName = L"MyApp"; RegisterClassEx(&winCls);
윈도우 클래스를 만들고, 이것을 등록하는 코드입니다.
윈도우 클래스의 필드는 사용할 프로시저 함수, 아이콘 등의 윈도우가 가질 기본적인 정보로 이루어져 있습니다.
myHwnd = CreateWindowW( L"MyApp", L"MyAppName", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 640, 480, nullptr, nullptr, hInstance, this );
윈도우를 생성하는 코드입니다.
사용할 윈도우 클래스의 이름, 창의 이름, 창의 크기와 속성 등에 대한 값을 지정합니다.
이 중 클래스의 이름은 이전에 등록한 윈도우 클래스의 lpszClassName필드와 동일해야 합니다.
이 함수의 결과로 윈도우 핸들을 반환합니다.
hr = myHwnd ? S_OK : E_FAIL; if (SUCCEEDED(hr)) { ShowWindow(myHwnd, SW_SHOWNORMAL); UpdateWindow(myHwnd); }
윈도우 핸들에 정상적인 접근이 가능한지의 여부로 HRESULT의 값을 지정합니다.
값이 정상적일 경우 (S_OK일 경우) 윈도우를 화면에 출력합니다.
4. 구현 (runMessageLoop)
더보기WinAPI는 메시지에 의해 동작합니다.
사용자의 입력, 행동 등이 윈도우에 메시지 형태로 전달되며, 전달된 메시지는 메시지 큐에 쌓이게 됩니다.
쌓인 메시지 큐의 메시지를 윈도우 프로시저 함수로 전달, 이를 처리하는 것으로 프로그램이 실행됩니다.
runMessageLoop함수는 메시지 큐의 메시지를 윈도우 프로시저로 전달하는 역할을 수행합니다.
void MyApp::runMessageLoop() { MSG msg; BOOL bRet; while ( (bRet = GetMessage(&msg, nullptr, 0, 0)) != 0) { if (bRet == -1) { // Error handling } else { TranslateMessage(&msg); DispatchMessage(&msg); } } }
runMessageLoop는 메시지 큐의 메시지를 해석하여 전달하는 역할을 수행합니다.
BOOL bRet; while ( (bRet = GetMessage(&msg, nullptr, 0, 0)) != 0) { ... }
메시지 큐를 탐색합니다.
탐색한 메시지가 WM_QUIT(종료)가 아닐 경우 반환값은 0이 아닌 다른 값이 됩니다.
이 경우, 반복문 내부의 코드가 실행될 수 있습니다.
만약 오류가 있을 경우, 반환 값이 -1이 됩니다.
반복문 내부에 조건문을 추가하는 것으로 이에 대한 처리를 할 수 있습니다.
if (bRet == -1) { // ... }
반복문 내부의, GetMessage가 -1일 경우의 조건문입니다.
오류 대응 코드를 작성할 수 있습니다.
이번 글의 프로그램은 창을 띄우는 간단한 예제이므로 다루지 않도록 하겠습니다.
else { TranslateMessage(&msg); DispatchMessage(&msg); }
0, -1이 아닌 메시지에 대한 처리 코드입니다.
TranslateMessage는 입력 키를 메시지에 추가하는 작업을 수행합니다.
예를 들어 키보드 A가 입력되었을 경우, 메시지 큐는 WM_KEYDOWN, WM_KEYUP이 추가됩니다.
입력 키를 탐색할 수 있는 메시지는 WM_CHAR이며, 추가되지 않습니다.
이 WM_CHAR을 메시지에 추가하는 역할을 TranslateMessage가 수행합니다.
TranslateMessage는 위의 예제와 같이 다음 메시지에 대해 동작합니다.
WM_KEYDOWN WM_KEYUP WM_SYSKEYDOWN WM_SYSKEYUP
DispatchMessage는 메시지를 윈도우 프로시저에 전달하는 역할을 수행합니다.
5. 구현 (WndProc)
더보기윈도우 프로서지는 전달된 메시지를 처리하는 역할을 수행합니다.
LRESULT MyApp::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { LRESULT result = 0; if (message == WM_CREATE) { LPCREATESTRUCT cStruct = reinterpret_cast<LPCREATESTRUCT>(lParam); MyApp* myApp = static_cast<MyApp*>(cStruct->lpCreateParams); SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(myApp)); result = 1; } else { MyApp* myApp = reinterpret_cast<MyApp*>(GetWindowLongPtrW(hWnd, GWLP_USERDATA)); bool wasHandled = false; if (myApp) { switch (message) { case WM_DESTROY: PostQuitMessage(0); result = 1; wasHandled = true; } } if (!wasHandled) { result = DefWindowProc(hWnd, message, wParam, lParam); } } return result; }
위 함수의 기능은 다음과 같습니다.
LRESULT MyApp::WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { LRESULT result = 0; ... return result; }
initialize와 같이, 프로시저의 실행 결과를 반환합니다.
if (message == WM_CREATE) { LPCREATESTRUCT cStruct = reinterpret_cast<LPCREATESTRUCT>(lParam); MyApp* myApp = static_cast<MyApp*>(cStruct->lpCreateParams); SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(myApp)); result = 1; }
메시지가 WM_CREATE일 경우 동작하는 코드입니다.
WM_CREATE는 윈도우 생성 시 최초 1회에 한해 전달됩니다.
따라서 위 코드는 윈도우 생성 시 최초 1회만 실행되는 코드입니다.
WM_CREATE는 lParam으로 윈도우 클래스를 전달합니다.
이를 적절히 캐스팅 후, 프로그램의 윈도우 데이터 영역에 윈도우 클래스의 포인터를 저장합니다.
윈도우 프로시저 함수가 static이기 때문에, 우리가 만들 MyApp클래스의 포인터에 접근하기 위해 사용합니다.
else { MyApp* myApp = reinterpret_cast<MyApp*>(GetWindowLongPtrW(hWnd, GWLP_USERDATA)); bool wasHandled = false; if (myApp) { switch (message) { case WM_DESTROY: PostQuitMessage(0); result = 1; wasHandled = true; } } if (!wasHandled) { result = DefWindowProc(hWnd, message, wParam, lParam); } }
메시지가 WM_CREATE외의 다른 메시지일 경우 동작하는 코드입니다.
SetWindowLongPtr로 지정한 클래스 포인터를 우선적으로 받아옵니다.
이후 switch 로 메시지에 따른 적절한 동작 코드를 구현합니다.
이번 글에서는 다른 코드는 구현하지 않고, 프로그램이 종료될 때 전달되는 WM_DESTROY만 구현했습니다.
이후 wasHandled의 값에 따라 DefWindowProc이 실행됩니다.
wasHanedled변수는 switch의 처리 후 true로 지정되며, 기본 값은 false입니다.
DefWindowProc은 우리가 구현하지 않은, 기본 구현된 윈도우 프로시저입니다.
위와 같이 원하는 메시지에 대한 동작만 구현하고, 그 외의 경우는 기본 구현으로 처리를 이관할 수 있습니다.
간단한 Windows Application을 만들어봤습니다.
전체 소스 코드는 아래 Github 에서 확인이 가능합니다.
감사합니다.
'C++ > 미분류' 카테고리의 다른 글
[WinAPI] 로그 출력하기 (Console, TRACE) (0) 2024.02.16 [Direct2D] 도형 그려보기 (0) 2024.02.08 [C++] 랜덤 이벤트 (확률) 구현하기 (0) 2022.10.06 [Visual Studio] Code style 변경하기 (BSD, K&R) (0) 2022.09.14 [WinAPI] Edit Control에서 Enter키 입력 받기 (0) 2022.06.22