Windows API 기초 | 메시지 루프와 윈도우 프로시저 완벽 가이드
이 글의 핵심
Windows API 프로그래밍의 핵심은 메시지 기반 아키텍처입니다. 메시지 루프가 OS로부터 메시지를 받아 윈도우 프로시저로 전달하고, 윈도우 프로시저는 각 메시지를 처리합니다. 이 구조를 이해하면 모든 Windows 프로그램의 동작 원리를 파악할 수 있습니다.
들어가며
Windows API 프로그래밍은 1990년대부터 Windows 데스크톱 애플리케이션 개발의 기초였습니다. 2000년대 들어 MFC와 .NET이 주류가 되었지만, 순수 Win32 API는 여전히 저수준 제어가 필요한 곳에서 사용됩니다. 처음엔 복잡해 보이지만, 메시지 루프와 윈도우 프로시저의 개념을 이해하면 모든 것이 명확해집니다.
Windows API는 복잡해 보이지만, 핵심 원리는 단순합니다:
// 모든 Windows 프로그램의 골격
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg); // → 윈도우 프로시저 호출
}
이 3줄이 Windows 프로그래밍의 심장입니다.
왜 Windows API를 배워야 할까?
실무에서 마주치는 상황들:
- 레거시 코드 유지보수 - 많은 기업 솔루션이 여전히 Win32 API 기반
- 저수준 제어 - 성능 최적화, 시스템 리소스 직접 제어
- 다른 프레임워크 이해 - MFC, WPF, Qt 모두 Win32 API 위에 구축됨
- 디바이스 드라이버 연동 - 하드웨어 제어 시 필수
산업 현장에서는 Win32 API가 여전히 활발히 사용됩니다. 특히 산업용 모니터링 소프트웨어나 하드웨어 통신 프로그램에서 MFC로는 구현하기 어려운 저수준 USB/시리얼 통신을 Win32 API로 직접 제어하는 경우가 많습니다.
1. Windows API 프로그램의 구조
1.1 기본 구조 개요
#include <windows.h>
// 1. 윈도우 프로시저 선언
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
// 2. 진입점: WinMain
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR lpCmdLine, int nCmdShow)
{
// 3. 윈도우 클래스 등록
WNDCLASS wc = {};
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.lpszClassName = L"MyWindowClass";
RegisterClass(&wc);
// 4. 윈도우 생성
HWND hwnd = CreateWindow(
L"MyWindowClass", L"내 첫 윈도우",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 800, 600,
NULL, NULL, hInstance, NULL
);
ShowWindow(hwnd, nCmdShow);
// 5. 메시지 루프
MSG msg = {};
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int)msg.wParam;
}
// 6. 윈도우 프로시저 구현
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg) {
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
TextOut(hdc, 10, 10, L"Hello, Windows!", 15);
EndPaint(hwnd, &ps);
return 0;
}
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
이 60줄이 모든 Windows 프로그램의 뼈대입니다.
2. WinMain - Windows 프로그램의 진입점
2.1 WinMain 시그니처 분석
int WINAPI WinMain(
HINSTANCE hInstance, // 현재 프로그램 인스턴스 핸들
HINSTANCE hPrevInstance, // 이전 인스턴스 (16비트 시대 유물, 항상 NULL)
LPSTR lpCmdLine, // 명령줄 인자 (문자열)
int nCmdShow // 윈도우 표시 방식 (SW_SHOW, SW_HIDE 등)
)
실무 팁:
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)
{
// hPrevInstance, lpCmdLine은 무시 (사용 안 함)
// 명령줄 인자는 GetCommandLine()으로 받기
// 디버깅: 콘솔 창 띄우기
#ifdef _DEBUG
AllocConsole();
FILE* pFile;
freopen_s(&pFile, "CONOUT$", "w", stdout);
printf("Debug Mode: hInstance = %p\n", hInstance);
#endif
// ... 초기화 코드
}
2.2 HINSTANCE의 역할
// HINSTANCE는 프로그램의 "신분증"
// - 리소스(아이콘, 메뉴) 로드 시 필요
// - 윈도우 클래스 등록 시 필요
// - 여러 인스턴스 구분 (같은 프로그램 2개 실행 시)
HICON hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_MYICON));
HMENU hMenu = LoadMenu(hInstance, MAKEINTRESOURCE(IDR_MAINMENU));
실전 예제: 프로그램 중복 실행 방지
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)
{
// 뮤텍스로 중복 실행 방지
HANDLE hMutex = CreateMutex(NULL, TRUE, L"MyAppSingleInstance");
if (GetLastError() == ERROR_ALREADY_EXISTS) {
MessageBox(NULL, L"프로그램이 이미 실행 중입니다.", L"오류", MB_OK);
return 1;
}
// ... 메인 로직
ReleaseMutex(hMutex);
CloseHandle(hMutex);
return 0;
}
3. 윈도우 클래스 (WNDCLASS) 등록
3.1 WNDCLASS 구조체 완벽 분석
typedef struct tagWNDCLASS {
UINT style; // 클래스 스타일 (CS_HREDRAW, CS_VREDRAW 등)
WNDPROC lpfnWndProc; // 윈도우 프로시저 함수 포인터 ★★★
int cbClsExtra; // 클래스 추가 메모리 (보통 0)
int cbWndExtra; // 윈도우 추가 메모리 (보통 0)
HINSTANCE hInstance; // 프로그램 인스턴스
HICON hIcon; // 아이콘
HCURSOR hCursor; // 커서
HBRUSH hbrBackground; // 배경 브러시
LPCWSTR lpszMenuName; // 메뉴 이름
LPCWSTR lpszClassName; // 클래스 이름 (고유 식별자)
} WNDCLASS;
3.2 실전 윈도우 클래스 설정
WNDCLASSEX wcex = {}; // WNDCLASSEX는 WNDCLASS의 확장 버전 (권장)
wcex.cbSize = sizeof(WNDCLASSEX);
// ★ 스타일: 크기 변경 시 재그리기
wcex.style = CS_HREDRAW | CS_VREDRAW;
// ★★★ 윈도우 프로시저 (가장 중요!)
wcex.lpfnWndProc = WndProc;
wcex.hInstance = hInstance;
// 아이콘 (NULL이면 기본 아이콘)
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
wcex.hIconSm = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_APPLICATION));
// 커서 (화살표)
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
// 배경 (흰색)
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
// 메뉴 (없음)
wcex.lpszMenuName = NULL;
// 클래스 이름 (고유해야 함!)
wcex.lpszClassName = L"MyMainWindow";
// 등록
if (!RegisterClassEx(&wcex)) {
MessageBox(NULL, L"윈도우 클래스 등록 실패!", L"오류", MB_ICONERROR);
return 1;
}
3.3 클래스 스타일 옵션
// 자주 사용하는 클래스 스타일
wcex.style =
CS_HREDRAW | // 가로 크기 변경 시 재그리기
CS_VREDRAW | // 세로 크기 변경 시 재그리기
CS_DBLCLKS | // 더블클릭 메시지 받기
CS_OWNDC; // 각 윈도우마다 고유 DC (Device Context)
// 성능 최적화가 필요한 경우
wcex.style = 0; // 재그리기 최소화
// 게임/그래픽 앱
wcex.style = CS_OWNDC | CS_HREDRAW | CS_VREDRAW;
실무 활용 사례:
// 2000년대 실시간 차트 프로그램에서 자주 사용되던 기법
// CS_HREDRAW | CS_VREDRAW 사용 → 크기 조절 시 깜빡임 발생
// 해결: WM_SIZE에서 수동으로 InvalidateRect() 호출
wcex.style = 0; // 자동 재그리기 끄기
// WndProc에서:
case WM_SIZE:
// 필요한 영역만 무효화
RECT rect = {0, 0, LOWORD(lParam), 50}; // 상단 영역만
InvalidateRect(hwnd, &rect, FALSE);
return 0;
4. 윈도우 생성 (CreateWindow)
4.1 CreateWindow vs CreateWindowEx
// CreateWindow (구버전, 간단)
HWND hwnd = CreateWindow(
L"MyMainWindow", // 등록한 클래스 이름
L"Hello Windows!", // 제목 표시줄 텍스트
WS_OVERLAPPEDWINDOW, // 윈도우 스타일
CW_USEDEFAULT, // X 위치 (자동)
CW_USEDEFAULT, // Y 위치 (자동)
800, // 너비
600, // 높이
NULL, // 부모 윈도우 (없음)
NULL, // 메뉴 (없음)
hInstance, // 인스턴스
NULL // 추가 파라미터
);
// CreateWindowEx (확장 버전, 권장)
HWND hwnd = CreateWindowEx(
WS_EX_CLIENTEDGE, // 확장 스타일 (3D 테두리)
L"MyMainWindow",
L"Hello Windows!",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
800, 600,
NULL, NULL, hInstance, NULL
);
if (hwnd == NULL) {
DWORD err = GetLastError();
wchar_t buf[256];
swprintf_s(buf, L"CreateWindowEx 실패! 에러 코드: %d", err);
MessageBox(NULL, buf, L"오류", MB_ICONERROR);
return 1;
}
4.2 윈도우 스타일 완전 정복
// 기본 스타일 조합
WS_OVERLAPPEDWINDOW // = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU
// | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX
// 커스텀 조합 예제
// 1. 크기 조절 불가, 최대화 버튼 없음
DWORD style = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;
// 2. 테두리 없는 윈도우 (게임, 스플래시)
DWORD style = WS_POPUP;
// 3. 항상 위 (Always On Top)
CreateWindowEx(
WS_EX_TOPMOST, // 확장 스타일
L"MyClass", L"Title",
WS_OVERLAPPEDWINDOW,
...
);
// 4. 툴 윈도우 (작은 제목 표시줄)
CreateWindowEx(
WS_EX_TOOLWINDOW,
...
);
// 5. 투명 윈도우 (Windows 2000+)
CreateWindowEx(
WS_EX_LAYERED, // 레이어드 윈도우 활성화
...
);
// 투명도 설정
SetLayeredWindowAttributes(hwnd, 0, 200, LWA_ALPHA); // 0~255
4.3 윈도우 위치와 크기 계산
// 화면 중앙에 윈도우 띄우기
int screenWidth = GetSystemMetrics(SM_CXSCREEN);
int screenHeight = GetSystemMetrics(SM_CYSCREEN);
int windowWidth = 800;
int windowHeight = 600;
int x = (screenWidth - windowWidth) / 2;
int y = (screenHeight - windowHeight) / 2;
HWND hwnd = CreateWindow(
L"MyClass", L"Centered Window",
WS_OVERLAPPEDWINDOW,
x, y, // 계산된 위치
windowWidth, windowHeight,
...
);
// 클라이언트 영역 크기로 윈도우 크기 계산
// (제목 표시줄, 테두리 제외한 실제 그리기 영역)
RECT rect = {0, 0, 800, 600}; // 원하는 클라이언트 크기
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE);
int width = rect.right - rect.left;
int height = rect.bottom - rect.top;
HWND hwnd = CreateWindow(
L"MyClass", L"Title",
WS_OVERLAPPEDWINDOW,
x, y, width, height, // 계산된 크기
...
);
실전 팁: 윈도우 크기 제한
// WndProc에서:
case WM_GETMINMAXINFO: {
MINMAXINFO* pMMI = (MINMAXINFO*)lParam;
pMMI->ptMinTrackSize.x = 400; // 최소 너비
pMMI->ptMinTrackSize.y = 300; // 최소 높이
pMMI->ptMaxTrackSize.x = 1920; // 최대 너비
pMMI->ptMaxTrackSize.y = 1080; // 최대 높이
return 0;
}
5. 메시지 루프 - Windows 프로그램의 심장
5.1 메시지 루프의 역할
MSG msg = {};
// 메시지가 있으면 가져오고, WM_QUIT이면 종료
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg); // 키보드 메시지 변환 (WM_KEYDOWN → WM_CHAR)
DispatchMessage(&msg); // 윈도우 프로시저로 전달
}
return (int)msg.wParam; // 종료 코드 반환
내부 동작 과정:
[OS 메시지 큐] → GetMessage() → TranslateMessage() → DispatchMessage() → WndProc()
5.2 GetMessage vs PeekMessage
// 1. GetMessage: 블로킹 방식 (메시지 대기)
// - 일반 애플리케이션에 적합
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// 2. PeekMessage: 논블로킹 방식 (즉시 반환)
// - 게임, 실시간 애플리케이션에 적합
while (true) {
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) break;
TranslateMessage(&msg);
DispatchMessage(&msg);
} else {
// 메시지가 없으면 게임 로직 실행
UpdateGame();
RenderFrame();
}
}
실전 예제: 60 FPS 게임 루프
#include <chrono>
using namespace std::chrono;
const double TARGET_FPS = 60.0;
const double FRAME_TIME = 1.0 / TARGET_FPS;
auto lastFrameTime = high_resolution_clock::now();
while (true) {
auto currentTime = high_resolution_clock::now();
double deltaTime = duration<double>(currentTime - lastFrameTime).count();
// 메시지 처리
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) goto exit_loop;
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// 프레임 제한
if (deltaTime >= FRAME_TIME) {
UpdateGame(deltaTime);
RenderFrame();
lastFrameTime = currentTime;
} else {
// CPU 절약
Sleep(1);
}
}
exit_loop:
5.3 TranslateMessage의 역할
// TranslateMessage가 없으면:
case WM_KEYDOWN:
if (wParam == VK_RETURN) {
// Enter 키 눌림
}
break;
// TranslateMessage가 있으면:
case WM_CHAR:
// 실제 문자 입력 (한글, 영문 등)
wchar_t ch = (wchar_t)wParam;
// 텍스트 입력 처리
break;
실무 예제: 텍스트 에디트 구현
std::wstring text;
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg) {
case WM_CHAR:
if (wParam == VK_BACK) {
// 백스페이스
if (!text.empty()) text.pop_back();
} else if (wParam >= 32) {
// 출력 가능한 문자
text += (wchar_t)wParam;
}
InvalidateRect(hwnd, NULL, TRUE);
return 0;
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
TextOut(hdc, 10, 10, text.c_str(), (int)text.length());
EndPaint(hwnd, &ps);
return 0;
}
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
6. 윈도우 프로시저 (WndProc) - 메시지 처리의 핵심
6.1 윈도우 프로시저 시그니처
LRESULT CALLBACK WndProc(
HWND hwnd, // 메시지를 받은 윈도우 핸들
UINT msg, // 메시지 ID (WM_PAINT, WM_LBUTTONDOWN 등)
WPARAM wParam, // 추가 정보 1 (키 코드, 버튼 상태 등)
LPARAM lParam // 추가 정보 2 (마우스 좌표 등)
) {
// 메시지 처리
return 0; // 또는 DefWindowProc 호출
}
6.2 주요 메시지 완전 정복
WM_CREATE: 윈도우 생성 시
case WM_CREATE: {
// 윈도우가 생성되는 순간 (ShowWindow 전)
// 초기화 작업 수행
CREATESTRUCT* pCS = (CREATESTRUCT*)lParam;
// 자식 컨트롤 생성
HWND hButton = CreateWindow(
L"BUTTON", L"클릭하세요",
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
10, 10, 100, 30,
hwnd, // 부모 윈도우
(HMENU)1001, // 버튼 ID
pCS->hInstance, NULL
);
// 타이머 시작
SetTimer(hwnd, 1, 1000, NULL); // 1초마다 WM_TIMER 발생
return 0;
}
WM_DESTROY: 윈도우 파괴 시
case WM_DESTROY:
// 리소스 정리
KillTimer(hwnd, 1);
// 메시지 루프 종료
PostQuitMessage(0);
return 0;
WM_PAINT: 화면 그리기
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
// 그리기 작업
RECT rect;
GetClientRect(hwnd, &rect);
// 배경 채우기
FillRect(hdc, &rect, (HBRUSH)(COLOR_WINDOW + 1));
// 텍스트 출력
SetTextColor(hdc, RGB(255, 0, 0));
SetBkMode(hdc, TRANSPARENT);
DrawText(hdc, L"Hello, Windows!", -1, &rect, DT_CENTER | DT_VCENTER);
EndPaint(hwnd, &ps);
return 0;
}
마우스 메시지
case WM_LBUTTONDOWN: {
// 왼쪽 버튼 클릭
int x = LOWORD(lParam); // X 좌표
int y = HIWORD(lParam); // Y 좌표
// wParam: 키 상태
bool shiftPressed = (wParam & MK_SHIFT) != 0;
bool ctrlPressed = (wParam & MK_CONTROL) != 0;
wchar_t buf[256];
swprintf_s(buf, L"클릭 위치: (%d, %d)", x, y);
MessageBox(hwnd, buf, L"정보", MB_OK);
return 0;
}
case WM_RBUTTONDOWN: {
// 오른쪽 버튼 클릭 → 컨텍스트 메뉴
POINT pt = {LOWORD(lParam), HIWORD(lParam)};
ClientToScreen(hwnd, &pt); // 클라이언트 좌표 → 화면 좌표
HMENU hMenu = CreatePopupMenu();
AppendMenu(hMenu, MF_STRING, 1, L"복사");
AppendMenu(hMenu, MF_STRING, 2, L"붙여넣기");
AppendMenu(hMenu, MF_SEPARATOR, 0, NULL);
AppendMenu(hMenu, MF_STRING, 3, L"삭제");
TrackPopupMenu(hMenu, TPM_LEFTALIGN | TPM_TOPALIGN,
pt.x, pt.y, 0, hwnd, NULL);
DestroyMenu(hMenu);
return 0;
}
case WM_MOUSEMOVE: {
// 마우스 이동
int x = LOWORD(lParam);
int y = HIWORD(lParam);
// 제목 표시줄에 좌표 표시
wchar_t buf[256];
swprintf_s(buf, L"마우스 위치: (%d, %d)", x, y);
SetWindowText(hwnd, buf);
return 0;
}
키보드 메시지
case WM_KEYDOWN: {
// 키 눌림 (가상 키 코드)
switch (wParam) {
case VK_ESCAPE:
PostMessage(hwnd, WM_CLOSE, 0, 0);
break;
case VK_F1:
MessageBox(hwnd, L"도움말", L"F1", MB_OK);
break;
case VK_LEFT:
// 왼쪽 화살표
break;
}
return 0;
}
case WM_CHAR: {
// 문자 입력 (TranslateMessage 필요)
wchar_t ch = (wchar_t)wParam;
if (ch == L'\r') {
// Enter 키
} else if (ch >= 32) {
// 출력 가능한 문자
}
return 0;
}
크기 변경
case WM_SIZE: {
int width = LOWORD(lParam);
int height = HIWORD(lParam);
// wParam: 크기 변경 타입
switch (wParam) {
case SIZE_MINIMIZED:
// 최소화됨
break;
case SIZE_MAXIMIZED:
// 최대화됨
break;
case SIZE_RESTORED:
// 복원됨
break;
}
// 자식 컨트롤 크기 조절
HWND hButton = GetDlgItem(hwnd, 1001);
SetWindowPos(hButton, NULL, width - 110, height - 40,
100, 30, SWP_NOZORDER);
return 0;
}
6.3 DefWindowProc의 역할
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg) {
case WM_DESTROY:
PostQuitMessage(0);
return 0;
case WM_PAINT:
// ... 처리
return 0;
}
// ★ 처리하지 않은 메시지는 DefWindowProc으로 전달 (필수!)
return DefWindowProc(hwnd, msg, wParam, lParam);
}
DefWindowProc을 호출하지 않으면?
// 잘못된 코드
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg) {
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return 0; // ❌ DefWindowProc 미호출
}
// 결과:
// - 윈도우 이동 불가
// - 크기 조절 불가
// - 최소화/최대화 버튼 작동 안 함
// - 시스템 메뉴 작동 안 함
7. 실전 예제: 완전한 Windows 애플리케이션
7.1 기본 기능이 있는 윈도우
#include <windows.h>
#include <string>
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)
{
// 1. 윈도우 클래스 등록
WNDCLASSEX wcex = {};
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW;
wcex.lpfnWndProc = WndProc;
wcex.hInstance = hInstance;
wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wcex.hCursor = LoadCursor(NULL, IDC_ARROW);
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
wcex.lpszClassName = L"MyFirstApp";
wcex.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
if (!RegisterClassEx(&wcex)) {
MessageBox(NULL, L"RegisterClassEx 실패!", L"오류", MB_ICONERROR);
return 1;
}
// 2. 윈도우 생성
RECT rect = {0, 0, 800, 600};
AdjustWindowRect(&rect, WS_OVERLAPPEDWINDOW, FALSE);
int screenWidth = GetSystemMetrics(SM_CXSCREEN);
int screenHeight = GetSystemMetrics(SM_CYSCREEN);
int width = rect.right - rect.left;
int height = rect.bottom - rect.top;
int x = (screenWidth - width) / 2;
int y = (screenHeight - height) / 2;
HWND hwnd = CreateWindowEx(
0,
L"MyFirstApp",
L"내 첫 Windows 애플리케이션",
WS_OVERLAPPEDWINDOW,
x, y, width, height,
NULL, NULL, hInstance, NULL
);
if (!hwnd) {
MessageBox(NULL, L"CreateWindowEx 실패!", L"오류", MB_ICONERROR);
return 1;
}
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
// 3. 메시지 루프
MSG msg = {};
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return (int)msg.wParam;
}
std::wstring g_text = L"윈도우를 클릭하세요!";
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg) {
case WM_CREATE:
SetTimer(hwnd, 1, 1000, NULL);
return 0;
case WM_TIMER:
if (wParam == 1) {
// 1초마다 제목 업데이트
static int count = 0;
wchar_t buf[256];
swprintf_s(buf, L"실행 시간: %d초", ++count);
SetWindowText(hwnd, buf);
}
return 0;
case WM_LBUTTONDOWN: {
int x = LOWORD(lParam);
int y = HIWORD(lParam);
wchar_t buf[256];
swprintf_s(buf, L"클릭 위치: (%d, %d)", x, y);
g_text = buf;
InvalidateRect(hwnd, NULL, TRUE);
return 0;
}
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
RECT rect;
GetClientRect(hwnd, &rect);
SetTextColor(hdc, RGB(0, 0, 255));
SetBkMode(hdc, TRANSPARENT);
HFONT hFont = CreateFont(24, 0, 0, 0, FW_BOLD, FALSE, FALSE, FALSE,
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
DEFAULT_QUALITY, DEFAULT_PITCH | FF_SWISS, L"Arial");
HFONT hOldFont = (HFONT)SelectObject(hdc, hFont);
DrawText(hdc, g_text.c_str(), -1, &rect, DT_CENTER | DT_VCENTER | DT_SINGLELINE);
SelectObject(hdc, hOldFont);
DeleteObject(hFont);
EndPaint(hwnd, &ps);
return 0;
}
case WM_SIZE: {
InvalidateRect(hwnd, NULL, TRUE);
return 0;
}
case WM_KEYDOWN:
if (wParam == VK_ESCAPE) {
PostMessage(hwnd, WM_CLOSE, 0, 0);
}
return 0;
case WM_DESTROY:
KillTimer(hwnd, 1);
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
7.2 버튼과 에디트 컨트롤 추가
// 전역 변수
HWND g_hEdit;
HWND g_hButton;
case WM_CREATE: {
CREATESTRUCT* pCS = (CREATESTRUCT*)lParam;
// 에디트 컨트롤
g_hEdit = CreateWindowEx(
WS_EX_CLIENTEDGE,
L"EDIT", L"",
WS_CHILD | WS_VISIBLE | ES_LEFT | ES_AUTOHSCROLL,
10, 10, 300, 25,
hwnd, (HMENU)1001, pCS->hInstance, NULL
);
// 버튼
g_hButton = CreateWindow(
L"BUTTON", L"클릭!",
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
320, 10, 100, 25,
hwnd, (HMENU)1002, pCS->hInstance, NULL
);
return 0;
}
case WM_COMMAND: {
int id = LOWORD(wParam);
int event = HIWORD(wParam);
if (id == 1002 && event == BN_CLICKED) {
// 버튼 클릭
wchar_t buf[256];
GetWindowText(g_hEdit, buf, 256);
if (wcslen(buf) == 0) {
MessageBox(hwnd, L"텍스트를 입력하세요!", L"경고", MB_ICONWARNING);
} else {
MessageBox(hwnd, buf, L"입력한 텍스트", MB_ICONINFORMATION);
}
}
return 0;
}
8. 디버깅과 문제 해결
8.1 자주 발생하는 오류
// 1. 윈도우가 생성되지 않음
if (hwnd == NULL) {
DWORD err = GetLastError();
wchar_t buf[256];
FormatMessage(
FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
NULL, err, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
buf, 256, NULL
);
MessageBox(NULL, buf, L"CreateWindow 실패", MB_ICONERROR);
}
// 2. 메시지 루프 종료 안 됨
// 원인: WM_DESTROY에서 PostQuitMessage(0) 누락
case WM_DESTROY:
PostQuitMessage(0); // 필수!
return 0;
// 3. 윈도우가 그려지지 않음
// 원인: UpdateWindow() 누락
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd); // 필수! 즉시 WM_PAINT 발생
// 4. 깜빡임 (Flicker)
// 해결: WM_ERASEBKGND에서 배경 지우기 방지
case WM_ERASEBKGND:
return 1; // 배경 지우지 않음
8.2 메시지 로깅
#ifdef _DEBUG
void LogMessage(UINT msg, WPARAM wParam, LPARAM lParam)
{
static FILE* pFile = nullptr;
if (!pFile) fopen_s(&pFile, "messages.log", "w");
const wchar_t* msgName = L"UNKNOWN";
switch (msg) {
case WM_CREATE: msgName = L"WM_CREATE"; break;
case WM_PAINT: msgName = L"WM_PAINT"; break;
case WM_SIZE: msgName = L"WM_SIZE"; break;
// ... 더 많은 메시지
}
fwprintf(pFile, L"[%s] wParam=%llu, lParam=%llu\n", msgName, wParam, lParam);
fflush(pFile);
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
LogMessage(msg, wParam, lParam);
// ... 나머지 처리
}
#endif
9. 성능 최적화
9.1 불필요한 재그리기 방지
// 나쁜 예: 전체 윈도우 무효화
InvalidateRect(hwnd, NULL, TRUE);
// 좋은 예: 변경된 영역만 무효화
RECT rect = {10, 10, 100, 100};
InvalidateRect(hwnd, &rect, FALSE);
9.2 더블 버퍼링
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
RECT rect;
GetClientRect(hwnd, &rect);
// 메모리 DC 생성 (더블 버퍼링)
HDC hdcMem = CreateCompatibleDC(hdc);
HBITMAP hbmMem = CreateCompatibleBitmap(hdc, rect.right, rect.bottom);
HBITMAP hbmOld = (HBITMAP)SelectObject(hdcMem, hbmMem);
// 메모리 DC에 그리기
FillRect(hdcMem, &rect, (HBRUSH)(COLOR_WINDOW + 1));
TextOut(hdcMem, 10, 10, L"깜빡임 없음!", 9);
// 한 번에 화면에 복사
BitBlt(hdc, 0, 0, rect.right, rect.bottom, hdcMem, 0, 0, SRCCOPY);
// 정리
SelectObject(hdcMem, hbmOld);
DeleteObject(hbmMem);
DeleteDC(hdcMem);
EndPaint(hwnd, &ps);
return 0;
}
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- C++ 콜백 패턴 | Callback 구현 완벽 가이드
- C++ 스마트 포인터 | unique_ptr/shared_ptr 메모리 안전 가이드
- C++ 멀티스레딩 완벽 가이드 | thread·mutex·condition_variable
이 글이 도움이 되셨나요? Windows API 메시지 루프와 윈도우 프로시저를 이해하는 데 도움이 되었기를 바랍니다!
다음 글에서는 Windows API 컨트롤 (버튼, 에디트, 리스트박스)을 다루겠습니다. 🚀