Window Management & Input
This page covers the complete WinUser subset: window class registration, window creation and lifetime, the WinAPI message loop, keyboard and mouse input, cursor control, user-mode timers, and geometry helpers.
WNDCLASSA & RegisterClassA
Before creating a window you must register a window class. The class defines the window procedure that will receive messages.
WNDCLASSA structure
| Field | Type | Used by Free API? | Notes |
|---|---|---|---|
lpfnWndProc | WNDPROC | Yes | Mandatory. The message handler function called by DispatchMessageA. |
lpszClassName | LPCSTR | Yes | Name used in CreateWindowA to look up this class. |
hInstance | HINSTANCE | Stored, ignored | Pass NULL or the value from WinMain; not used internally. |
style | UINT | Ignored | CS_VREDRAW, CS_HREDRAW defined but not acted upon. |
hIcon, hCursor, hbrBackground | handles | Stored, ignored | Icon / cursor / background paint not implemented. |
lpszMenuName | LPCSTR | Ignored | Menus not supported. |
cbClsExtra, cbWndExtra | int | Ignored | Extra bytes — not allocated. |
RegisterClassA
ATOM WINAPI RegisterClassA(const WNDCLASSA* lpWndClass);
Stores the class-name → WNDPROC mapping in an internal table. Returns a non-zero atom on success, 0 on failure (e.g. NULL lpWndClass or NULL lpszClassName).
WNDCLASSA wc = {}; // zero-init all fields
wc.lpfnWndProc = MyWindowProc; // required
wc.hInstance = hInstance;
wc.lpszClassName = "GameWindow"; // required
wc.style = CS_HREDRAW | CS_VREDRAW;
ATOM a = RegisterClass(&wc);
if (!a) {
OutputDebugString("RegisterClass failed!");
return -1;
}
Creating and managing windows
CreateWindowExA / CreateWindowA
HWND WINAPI CreateWindowExA(
DWORD dwExStyle, // extended style, e.g. WS_EX_TOPMOST
LPCSTR lpClassName, // class name from RegisterClassA
LPCSTR lpWindowName, // title bar text
DWORD dwStyle, // window style flags
int X, int Y, // initial position
int nWidth, int nHeight, // initial size
HWND hWndParent, // parent (ignored: child semantics not supported)
HMENU hMenu, // NULL
HINSTANCE hInstance,
LPVOID lpParam // passed in WM_CREATE's CREATESTRUCT.lpCreateParams
);
// Convenience macro (no extended style):
HWND WINAPI CreateWindowA(lpClassName, lpWindowName, dwStyle,
X, Y, nWidth, nHeight,
hWndParent, hMenu, hInstance, lpParam);
Window style flags
| Constant | Value | Behaviour in Free API |
|---|---|---|
WS_OVERLAPPEDWINDOW | 0x00CF0000 | Standard window with title bar and borders (default for most games). |
WS_POPUP | 0x80000000 | Borderless window — SDL window created without decorations. |
WS_POPUPWINDOW | 0x80880000 | Popup with border. |
WS_CAPTION | 0x00C00000 | Title bar. |
WS_VISIBLE | 0x10000000 | Window is shown immediately (equivalent to ShowWindow(SW_SHOW)). |
WS_CHILD | 0x40000000 | Parsed but child-window coordinate semantics are not implemented. |
WS_EX_TOPMOST | 0x00000008 | Extended style: always-on-top SDL hint set. |
// Full-screen-like popup window (no title bar) HWND hwnd = CreateWindowEx( 0, // no extended styles "GameWindow", // class name registered above "Speedy Blupi", // window title WS_POPUP | WS_VISIBLE, 0, 0, // top-left corner 640, 480, // resolution NULL, NULL, hInstance, NULL); if (!hwnd) { OutputDebugString("CreateWindow failed"); return -1; }
ShowWindow, DestroyWindow, UpdateWindow
| Function | Status | Description |
|---|---|---|
ShowWindow(HWND, SW_SHOW) | PARTIAL | Raises and shows the SDL window. SW_HIDE hides it. |
DestroyWindow(HWND) | PARTIAL | Sends WM_DESTROY, destroys the SDL window, removes the HWND from the registry. |
UpdateWindow(HWND) | PARTIAL | Presents / raises the SDL window. |
MoveWindow(HWND, x, y, w, h, repaint) | PARTIAL | Repositions and resizes via SDL. repaint is ignored. |
SetWindowTextA(HWND, LPCSTR) | PARTIAL | Sets the SDL window title. |
GetClientRect(HWND, RECT*) | PARTIAL | Returns {0, 0, w, h} from SDL window size. |
AdjustWindowRect(RECT*, style, menu) | STUB | Returns TRUE without modifying the rect. |
InvalidateRect(HWND, RECT*, erase) | STUB | Returns TRUE; no WM_PAINT queued. |
GetSystemMetrics(nIndex) | PARTIAL | Supports SM_CXSCREEN, SM_CYSCREEN (display size), SM_CYCAPTION (returns 19). |
SetFocus(HWND) | PARTIAL | Raises the SDL window. |
MessageBoxA(HWND, text, caption, type) | STUB | Logs to SDL log; returns IDOK (1). |
WinAPI message loop
The message loop is the heart of any Win32 application. Free API translates SDL3 events into WM_* messages and delivers them through the same PeekMessage → DispatchMessage → WNDPROC pipeline as real Windows.
Game calls PeekMessageA() each frame │ ├─ SDL_PumpEvents() pull OS events into SDL internal queue ├─ PumpSdlEvents() translate SDL events → Free API WM_* queue │ └─ dequeue one message or return FALSE │ ▼ TranslateMessage() WM_KEYDOWN → WM_CHAR for printable keys │ ▼ DispatchMessageA() look up HWND → WNDPROC, call it │ ▼ Your WNDPROC(hWnd, uMsg, wParam, lParam)
Loop functions
| Function | Status | Description |
|---|---|---|
PeekMessageA(MSG*, HWND, min, max, flags) |
IMPLEMENTED | Pumps SDL on every call (prevents input starvation). Returns TRUE and fills MSG if a message is available. flags: PM_REMOVE dequeues the message; PM_NOREMOVE peeks without removing. |
GetMessageA(MSG*, HWND, min, max) |
PARTIAL | Blocks until a message arrives (busy-waits with 1 ms sleep). Returns FALSE when WM_QUIT is dequeued. |
TranslateMessage(MSG*) |
PARTIAL | When msg.message == WM_KEYDOWN and wParam is a printable ASCII character, posts a WM_CHAR message. Required for name-entry screens. |
DispatchMessageA(MSG*) |
PARTIAL | Looks up the WNDPROC for msg.hwnd and calls it. Falls back to DefWindowProcA if not found. |
PostMessageA(HWND, Msg, wParam, lParam) |
PARTIAL | Enqueues a message into the global mutex-protected queue. Thread-safe — can be called from a multimedia timer callback. |
PostQuitMessage(nExitCode) |
IMPLEMENTED | Posts WM_QUIT with the given exit code as wParam. Causes GetMessageA to return FALSE. |
WaitMessage() |
PARTIAL | Busy-waits with 1 ms sleeps until the queue has at least one message. |
DefWindowProcA(HWND, Msg, wParam, lParam) |
PARTIAL | Default handler. Handles WM_CLOSE by posting WM_QUIT. All other messages return 0. |
// Pattern 1: PeekMessage loop (non-blocking, good for games that render every frame) MSG msg = {}; while (msg.message != WM_QUIT) { if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { TranslateMessage(&msg); DispatchMessage(&msg); } else { GameUpdate(); // called every frame when no messages are pending GameRender(); } } // Pattern 2: GetMessage loop (blocking, good for event-driven apps) MSG msg; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return (int)msg.wParam; // exit code from PostQuitMessage
Win32 message constants
| Constant | Value | When sent | wParam / lParam |
|---|---|---|---|
WM_NULL | 0x0000 | No-op message. | — |
WM_CREATE | 0x0001 | Window just created (inside CreateWindowA). | lParam = CREATESTRUCT* |
WM_DESTROY | 0x0002 | Window is being destroyed. | — |
WM_CLOSE | 0x0010 | User closed the window (title bar X). | — |
WM_QUIT | 0x0012 | Posted by PostQuitMessage; terminates the loop. | wParam = exit code |
WM_ACTIVATEAPP | 0x001C | App focus changed. Focus-lost is suppressed. | wParam=1 (activated) |
WM_KEYDOWN | 0x0100 | Key pressed. | wParam = VK_* code |
WM_KEYUP | 0x0101 | Key released. | wParam = VK_* code |
WM_CHAR | 0x0102 | Printable character (from TranslateMessage or SDL_EVENT_TEXT_INPUT). | wParam = char code |
WM_SYSKEYDOWN | 0x0104 | System key pressed (F10 on Windows parity). | wParam = VK_F10 |
WM_SYSKEYUP | 0x0105 | System key released. | wParam = VK_F10 |
WM_TIMER | 0x0113 | User timer fired (from SetTimer). | wParam = timer ID |
WM_MOUSEMOVE | 0x0200 | Mouse cursor moved. | lParam=(y<<16)|x; wParam=MK_* |
WM_LBUTTONDOWN | 0x0201 | Left button pressed. | lParam=(y<<16)|x; wParam=MK_* |
WM_LBUTTONUP | 0x0202 | Left button released. | same |
WM_RBUTTONDOWN | 0x0204 | Right button pressed. | same |
WM_RBUTTONUP | 0x0205 | Right button released. | same |
WM_MBUTTONDOWN | 0x0207 | Middle button pressed. | same |
WM_MBUTTONUP | 0x0208 | Middle button released. | same |
WM_MOUSEWHEEL | 0x020A | Scroll wheel moved. | wParam high word = delta (WHEEL_DELTA=120) |
WM_USER | 0x0400 | Base for app-defined messages. | Application-defined |
Keyboard — VK_* virtual-key codes
SDL3 key events are translated to WM_KEYDOWN / WM_KEYUP messages
with a VK_* code in wParam. The full set used by Speedy Blupi
and Planet Blupi is implemented.
| Constant | Value | Key |
|---|---|---|
VK_BACK | 0x08 | Backspace |
VK_TAB | 0x09 | Tab |
VK_RETURN | 0x0D | Enter / Return |
VK_SHIFT | 0x10 | Shift (either) |
VK_CONTROL | 0x11 | Ctrl (either) |
VK_MENU | 0x12 | Alt (either) |
VK_PAUSE | 0x13 | Pause / Break |
VK_ESCAPE | 0x1B | Escape |
VK_SPACE | 0x20 | Space bar |
VK_PRIOR | 0x21 | Page Up |
VK_NEXT | 0x22 | Page Down |
VK_END | 0x23 | End |
VK_HOME | 0x24 | Home |
VK_LEFT | 0x25 | Left arrow |
VK_UP | 0x26 | Up arrow |
VK_RIGHT | 0x27 | Right arrow |
VK_DOWN | 0x28 | Down arrow |
VK_INSERT | 0x2D | Insert |
VK_DELETE | 0x2E | Delete |
0x30–0x39 | — | Digits 0–9 (ASCII value) |
0x41–0x5A | — | Letters A–Z (uppercase ASCII) |
VK_F1–VK_F9 | 0x70–0x78 | Function keys F1–F9 |
VK_F10 | 0x79 | F10 — uses WM_SYSKEYDOWN/UP |
VK_F11 | 0x7A | F11 |
VK_F12 | 0x7B | F12 |
case WM_KEYDOWN: switch (wParam) { // Navigation case VK_LEFT: MovePlayerLeft(); break; case VK_RIGHT: MovePlayerRight(); break; case VK_UP: MovePlayerUp(); break; case VK_DOWN: MovePlayerDown(); break; // Actions case VK_SPACE: Jump(); break; case VK_RETURN: Action(); break; // Function keys case VK_F1: ShowHelp(); break; case VK_F5: QuickSave(); break; case VK_F9: QuickLoad(); break; // Letters: wParam is uppercase ASCII case 'P': TogglePause(); break; case 'M': ToggleMusic(); break; // Quit case VK_ESCAPE: PostQuitMessage(0); break; } return 0; case WM_CHAR: // Character input (name entry screen) HandleCharacterInput((char)wParam); return 0;
WM_SYSKEYDOWN / WM_SYSKEYUP
instead of the normal WM_KEYDOWN/UP. Free API replicates this behaviour
exactly so games that check for F10 work correctly.
Mouse events & cursor
Mouse button state flags (wParam)
| Flag | Value | Meaning |
|---|---|---|
MK_LBUTTON | 0x0001 | Left mouse button is currently held down. |
MK_RBUTTON | 0x0002 | Right mouse button is currently held down. |
MK_SHIFT | 0x0004 | Shift key is held. |
MK_CONTROL | 0x0008 | Ctrl key is held. |
MK_MBUTTON | 0x0010 | Middle mouse button is currently held down. |
case WM_MOUSEMOVE: { int x = LOWORD(lParam); // cursor X in client coords int y = HIWORD(lParam); // cursor Y in client coords BOOL lBtn = (wParam & MK_LBUTTON) != 0; // left button held? BOOL shift = (wParam & MK_SHIFT) != 0; OnMouseMove(x, y, lBtn, shift); return 0; } case WM_LBUTTONDOWN: { int x = LOWORD(lParam); int y = HIWORD(lParam); OnLeftClick(x, y); return 0; } case WM_LBUTTONUP: OnLeftRelease(LOWORD(lParam), HIWORD(lParam)); return 0; case WM_RBUTTONDOWN: OnRightClick(LOWORD(lParam), HIWORD(lParam)); return 0;
Cursor functions
| Function | Status | Description |
|---|---|---|
GetCursorPos(LPPOINT lpPoint) | PARTIAL | Reads screen-space cursor position via SDL_GetGlobalMouseState. Returns TRUE on success. |
SetCursorPos(int X, int Y) | PARTIAL | Warps cursor to screen position via SDL_WarpMouseGlobal. Returns TRUE on success. |
ClientToScreen(HWND, LPPOINT) | PARTIAL | Converts client coordinates to screen coordinates by adding the SDL window's screen position. |
ScreenToClient(HWND, LPPOINT) | PARTIAL | Converts screen coordinates to client coordinates by subtracting the SDL window's screen position. |
ShowCursor(BOOL) | STUB | Returns a display counter; does not actually hide/show the SDL cursor. |
SetCursor(HCURSOR) | STUB | Returns NULL. |
LoadCursorA(hInst, name) | STUB | Returns NULL. Pass IDC_ARROW or any value — it is ignored. |
LoadIconA(hInst, name) | STUB | Returns NULL. |
POINT pt; GetCursorPos(&pt); // pt is in screen (global) coordinates ScreenToClient(hwnd, &pt); // convert to client-area coordinates if (pt.x >= 0 && pt.y >= 0 && pt.x < 640 && pt.y < 480) { // cursor is inside the window client area }
SetTimer / KillTimer
User-mode timers deliver WM_TIMER messages through the normal message
queue. They are polled by PeekMessageA — no separate thread is involved.
For high-frequency timers, see multimedia timers.
UINT_PTR WINAPI SetTimer(
HWND hWnd, // window to receive WM_TIMER
UINT_PTR nIDEvent, // timer ID (0 = auto-assign unique ID)
UINT uElapse, // interval in milliseconds
void* lpTimerFunc // ignored — always use NULL
);
BOOL WINAPI KillTimer(HWND hWnd, UINT_PTR uIDEvent);
Behaviour: SetTimer stores the timer in an internal list.
Each call to PeekMessageA / GetMessageA checks whether any
timer has elapsed and, if so, enqueues a WM_TIMER message with
wParam = timer ID. lpTimerFunc is always ignored — route
timer actions through the WNDPROC instead.
#define TIMER_ANIMATION 1 #define TIMER_AUTOSAVE 2 // In WM_CREATE: SetTimer(hWnd, TIMER_ANIMATION, 50, NULL); // 20 Hz animation tick SetTimer(hWnd, TIMER_AUTOSAVE, 30000, NULL); // auto-save every 30 s // In WNDPROC: case WM_TIMER: switch (wParam) { case TIMER_ANIMATION: AnimationStep(); break; case TIMER_AUTOSAVE: AutoSave(); break; } return 0; // In WM_DESTROY: KillTimer(hWnd, TIMER_ANIMATION); KillTimer(hWnd, TIMER_AUTOSAVE); PostQuitMessage(0);
RECT operations (inline)
These functions are defined as inline functions in the header and require no library linkage.
| Function | Description |
|---|---|
SetRect(RECT*, l, t, r, b) | Fills all four fields. Returns TRUE, FALSE on NULL pointer. |
IntersectRect(dst, src1, src2) | Sets dst to the intersection of src1 and src2. Returns FALSE if the intersection is empty. |
UnionRect(dst, src1, src2) | Sets dst to the bounding rectangle that contains both src1 and src2. Returns TRUE. |
RECT r1, r2, result; SetRect(&r1, 0, 0, 100, 100); SetRect(&r2, 50, 50, 200, 200); if (IntersectRect(&result, &r1, &r2)) { // result = {50, 50, 100, 100} } RECT bound; UnionRect(&bound, &r1, &r2); // bound = {0, 0, 200, 200}
wsprintfA
int WINAPIV wsprintfA(LPSTR lpOut, LPCSTR lpFmt, ...); // 1024-byte fixed internal buffer — do not use for long strings char buf[64]; wsprintfA(buf, "Score: %d Level: %d", score, level);
Implemented via vsnprintf with a 1024-byte stack buffer.
The output is truncated if longer than 1023 characters.
For modern code, prefer snprintf.