Building the Smallest Win32 GUI Application in C++ with Visual Studio
Project setup in Visual Studio
- File → New → Project
- Select "Installed" → "C++" → "Windows Desktop" → "Windows Desktop Wizard" (or "Win32 Project" on older VS)
- Name the project (for example, MinimalWin32App)
- Choose "Windows application" and check "Empty project" (no ATL/MFC, no precompiled header if you prefer manual setup)
- Add a new C++ source file (e.g., main.cpp)
Required headers
At minimum, include the Win32 API definitions.
#include <windows.h>
Modern Visual Studio defaults to Unicode. The code below uses wide-character (W) APIs.
Entry point and program flow
A GUI Win32 program starts from WinMain or wWinMain. The core steps are:
- Define and register a window class (WNDCLASSEX)
- Create the main window
- Show and udpate the window
- Run the message loop
- Handle messages in a window procedure (WndProc)
Complete minimal example
The following self-contained program creates a window and paints "Hello, Windows!" centerde in the client area.
#include <windows.h>
// Window procedure declaration
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam);
int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR, int nCmdShow) {
// 1) Register a window class
const wchar_t kClassName[] = L"MinimalWin32WindowClass";
const wchar_t kWindowTitle[] = L"Minimal Win32 Window";
WNDCLASSEXW wc{};
wc.cbSize = sizeof(wc);
wc.style = CS_HREDRAW | CS_VREDRAW;
wc.lpfnWndProc = WindowProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIconW(nullptr, IDI_APPLICATION);
wc.hCursor = LoadCursorW(nullptr, IDC_ARROW);
wc.hbrBackground = reinterpret_cast<HBRUSH>(COLOR_WINDOW + 1);
wc.lpszMenuName = nullptr;
wc.lpszClassName = kClassName;
wc.hIconSm = wc.hIcon;
if (!RegisterClassExW(&wc)) {
MessageBoxW(nullptr, L"RegisterClassExW failed", L"Error", MB_ICONERROR);
return 0;
}
// 2) Create the window
HWND hwnd = CreateWindowExW(
0,
kClassName,
kWindowTitle,
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT,
640, 240,
nullptr,
nullptr,
hInstance,
nullptr
);
if (!hwnd) {
MessageBoxW(nullptr, L"CreateWindowExW failed", L"Error", MB_ICONERROR);
return 0;
}
// 3) Show and update the window
ShowWindow(hwnd, nCmdShow);
UpdateWindow(hwnd);
// 4) Message loop
MSG msg{};
while (GetMessageW(&msg, nullptr, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
return static_cast<int>(msg.wParam);
}
// 5) Window procedure: handle paint and destroy
LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hwnd, &ps);
RECT rc;
GetClientRect(hwnd, &rc);
SetBkMode(hdc, TRANSPARENT);
SetTextColor(hdc, RGB(32, 32, 32));
const wchar_t* text = L"Hello, Windows!";
DrawTextW(hdc, text, -1, &rc, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint(hwnd, &ps);
return 0;
}
case WM_DESTROY:
PostQuitMessage(0);
return 0;
default:
return DefWindowProcW(hwnd, msg, wParam, lParam);
}
}
Key building blocks
- WNDCLASSEX: Describes the window type (procedure function, background brush, cursor, icons, style, and class name). Registration with RegisterClassExW makes the class available to CreateWindowExW.
- CreateWindowExW: Instantiates the top-level window using the registered class.
- ShowWindow/UpdateWindow: Makes the window visible and triggers an initial paint if needed.
- Message loop: Retrieves messages from the queue and routes them too the window procedure.
- Window procedure (WindowProc): Switches on message IDs. WM_PAINT uses BeginPaint/EndPaint to draw; WM_DESTROY posts the quit message.
Notes
- With Unicode builds, always call wide-character APIs (the W-suffixed versions) or rely on the TCHAR mapping if you prefer the generic macros.
- DrawTextW is used instead of TextOut to conveniently center text within the client rectangle.
- If project settings disable precompiled headers or additional libraries, ensure the subsystem is "Windows" and default libraries for User32/GDI32 are linked by the toolchain.