Windows Input: Polling, Events, and Messages
Compare hotkey polling, synthetic input, and window messages so a local game tool chooses the right input boundary, and see how the window's client area, scale, and several windows change the coordinates it works with.
Cheatsheet for Lesson 6.9
A one-page summary of “Windows Input: Polling, Events, and Messages” you can keep beside your notes. Plain Markdown.
Lesson 6.9 — Windows Input: Polling, Events, and Messages: cheatsheet
- Remember: Choose input APIs by what the receiver consumes: sampled state, desktop input events, and window messages describe different things.
- Also useful: An edge detector keeps two photographs of a button: the transition appears only when the newer photograph is down and the older one is up.
Key terms
SendKeys: .NET'sSendKeysis a separate convenience layer, while the oldkeybd_eventfunction has been superseded bySendInput.- Polling: Polling is the other side of this distinction.
INPUT: A key event is anINPUTwhosetypesays “keyboard” and whose union holds aKEYBDINPUT.SendInput:SendInputis subject to User Interface Privilege Isolation (UIPI): Windows blocks input from a lower-integrity process into a window owned by a higher-integrity one, such as a game running as administrator while your tool does not.KEYEVENTF_KEYUP:KEYEVENTF_KEYUPmeans it is released, which is why the code sends one of each.HWND: AnHWNDis also called a handle, but it is owned by the window manager and is not closed withCloseHandle.
Review
- Q: Which statement correctly distinguishes SendInput from SendMessageW? A: SendInput inserts system input events, while SendMessageW calls a window procedure and may be ignored by games using device input
- Q: A tool must (a) learn whether a key is held right now, (b) make the system act as if a key was typed, and (c) notify one specific window. Which pairing of job to mechanism is right? A: Sample the key state; inject an event into the system input stream; post a window message
“Send a key” can mean three different things
Section titled ““Send a key” can mean three different things”Our DLL now has a controlled lifecycle, and the scanner can do work off the game thread. A local tool also needs a deliberate way to start or stop a feature. Pressing a menu hotkey, sending a synthetic key to an offline game, and calling a window procedure solve different problems, even though all three sound like “input.” This lesson is about the ways Windows delivers input and the ways a tool can read or send it. Lesson 8.5 follows the input inside a game once it has arrived, from device events to per-frame state and actions.
Windows has several input paths because programs receive interaction in different ways:
| Path | What it really does | Good course use | Common surprise |
|---|---|---|---|
GetAsyncKeyState |
Samples the current physical key state | Toggle a local tool menu | It does not wait for an event |
SendInput |
Inserts keyboard or mouse events into the system input stream | Drive an offline game while its window is active | It does not target one HWND |
SendMessageW / PostMessageW |
Calls or queues a message for a window procedure | Control a window you created or a documented local test window | Many games read Raw Input or device state instead |
Each path lands on a different destination, which is the real reason to pick carefully:
There is no general Win32 function named SendKey. .NET’s SendKeys is a
separate convenience layer, while the old keybd_event function has been
superseded by SendInput. Naming the path correctly prevents a lot of confused
debugging. ⌨️
State, events, and messages are not interchangeable
Section titled “State, events, and messages are not interchangeable”GetAsyncKeyState answers a state question: “is this key down at the instant I sample it?” SendInput submits input events to the desktop input pipeline. SendMessageW invokes a window-procedure message contract. Similar names do not put them on the same path.
A game may combine several paths: Raw Input for aiming, ordinary window messages for menus, and polled key state for a developer console. Observe how the target consumes input before choosing an API. Sending WM_KEYDOWN to a window that reads device state is not a failed keyboard event; it is the wrong abstraction.
Always pair a down transition with an up transition, including cancellation and error paths. A state machine should own which synthetic inputs are currently held so shutdown can release exactly those inputs.
A held button and a new press are different. This step-by-step trace needs JavaScript. Here is the code and where the run ends.
previous = last_frame_down
current = this_frame_down
pressed = current and not previous
released = previous and not current
store previous = current for the next sample
report level, press edge, and release edgeResult: level=true, pressed=true, released=false. A held button does not create another press.
Final values: previous = up; current = down; was_down = false; is_down = true; pressed = true; released = false; next_previous = true.
Read every step of the lesson’s example
Step 1, line 1. Start with the selected inputs. Follow the highlighted operation and the changing state below.
previous = up; current = down.
Step 2, line 2. Read the current level independently of the remembered level.
previous = up; current = down; was_down = false; is_down = true.
Step 3, line 3. A press exists only when current is down and previous was up.
previous = up; current = down; was_down = false; is_down = true; pressed = true.
Step 4, line 4. A release exists only when previous was down and current is up.
previous = up; current = down; was_down = false; is_down = true; pressed = true; released = false.
Step 5, line 5. Remember this level; another unchanged sample will have no edge.
previous = up; current = down; was_down = false; is_down = true; pressed = true; released = false; next_previous = true.
Step 6, line 6. Result: level=true, pressed=true, released=false. A held button does not create another press.
previous = up; current = down; was_down = false; is_down = true; pressed = true; released = false; next_previous = true.
A window receives messages through a loop
Section titled “A window receives messages through a loop”The message path makes more sense if we first follow an ordinary
window. A key press or resize can place a message in the thread’s
queue. A message loop retrieves it and DispatchMessage calls
the window’s window procedure. That procedure is a callback: a
function the application provides so Windows can call it when a
message is dispatched. For example, it might respond to WM_SIZE
by updating the window’s layout. Microsoft’s
window-message walkthrough
shows this path.
message arrives for a windowloop retrieves itDispatchMessage calls that window's procedureprocedure handles it and returnsloop retrieves the next messageGetMessage waits when no queued message is available. That suits a
window that has nothing to do until the next event. A game often must
update its world even when the player touches nothing. Its loop can
use PeekMessage to check for waiting messages without blocking,
then continue to update and draw. This is a conceptual comparison,
not complete Win32 source:
ordinary event loop: wait for a message with GetMessage translate and dispatch it
game-style loop: while PeekMessage finds a waiting message: translate and dispatch it update the world draw the next frameThe waiting path and the continuously updating path look different:
TranslateMessage can post character messages derived from key
messages. See Microsoft’s
GetMessageW
and PeekMessageW
references for their return values and queue behavior.
The callback must also return promptly. While a window procedure is busy reading a large file or waiting on a network request, other messages for windows on that thread cannot be handled. The window may stop repainting or responding to input. Microsoft’s window-procedure guidance explains this bottleneck. The same concern appears when a hook adds work to a game’s time-sensitive path: move slow work elsewhere and make overload visible, as Lesson 6.11 does for a recorder.
Polling is the other side of this distinction. If no one sends your tool a message when the game’s gold changes, the tool can check the value on its own schedule. It does work even when nothing changed. Lesson 4.9 shows how to compare successive snapshots so many polls produce one meaningful event instead of many repeated actions.
The window itself
Section titled “The window itself”The window that receives those messages is also what the game draws into, and a few of its properties explain problems you will meet in later lessons.
- Window rectangle and client area. The window rectangle includes the title bar and
the borders. The client area is the part the game draws into. On one typical
Windows setup, a window whose client area must be 1280 × 720 needs an outer rectangle of
about 1280 + 16 = 1296 wide and 720 + 39 = 759 tall, because the borders and title bar
take 16 pixels across and 39 down. The exact sizes depend on the theme and the scale, so
the system works them out (
AdjustWindowRect). A point in the client area becomes a screen point by adding where that area starts on the desktop (ClientToScreen), which is why Lesson 7.9 matches an overlay to the client area. - Resizing. Dragging the edge sends
WM_SIZEwith the new client width and height. A game then resizes its buffers and recomputes the projection’s aspect ratio (Lesson 7.1). With a client area of 1280 × 720 the aspect is 1280 ÷ 720 = 1.7778; a game that keeps the old value after the window becomes 1000 × 720 draws a squashed picture. - Scale factor. A display set to 125% reports a window 1,000 logical pixels wide as
1,000 × 1.25 = 1,250 physical pixels. A game that is not aware of this has its picture
stretched by the system, and one that is aware sizes its menus by the factor (Lesson 8.7). Moving the window to a display with a different scale sends
WM_DPICHANGED. - More than one window. An application can own several windows, such as the game’s
window and a separate settings or tool window. Each has its own handle (
HWND), client area, and scale factor, and windows made by one thread share that thread’s message queue. A screen point belongs to the window that contains it. Say the game’s client area starts at the screen point(100, 100)and is 1280 wide, and a tool window’s client area starts at(1400, 100). The screen point(1500, 200)is(1500 − 100, 200 − 100) = (1400, 100)in the game’s client coordinates, which is past its 1280-pixel width, and(1500 − 1400, 200 − 100) = (100, 100)in the tool window’s. Subtracting the wrong window’s origin is a common reason an overlay is off by a constant amount. - Fullscreen. Exclusive fullscreen takes over the display and may change its resolution. Borderless windowed is an ordinary window with no border, sized to the desktop, which is quick to switch away from and can be drawn over by other windows.
- Presenting. The game draws into a hidden back buffer and then presents it. With vertical sync on, the present waits for the display’s next refresh, so a 60 Hz display caps the game at 60 frames a second (Lesson 4.1). A game with little moving, such as a menu, may redraw only when something changes, to save power. A screenshot is a copy of the finished back buffer saved as an image file.
- Focus and the cursor. A window that loses focus may pause its game, but
GetAsyncKeyStatestill reports keys, so a tool must check focus itself (below). A game can hide the cursor, confine it to the window, replace it with its own image, or read relative mouse movement instead of a position (Lesson 8.5 shows the arithmetic of a look-around loop).
Read a hotkey with GetAsyncKeyState
Section titled “Read a hotkey with GetAsyncKeyState”The current windows crate exposes
GetAsyncKeyState
as an unsafe function taking a virtual-key number and returning an i16.
Windows puts “down right now” in the most significant bit. Because that is the
sign bit of an i16, a negative result means down:
GetAsyncKeyState returns an i16. A negative result (bit 15 set) means the key is down right now. Bit 0 is kept for old compatibility, but another process's call can consume it first, which is why the edge detector below is built from bit 15 instead.use windows::Win32::UI::Input::KeyboardAndMouse::{ GetAsyncKeyState, VIRTUAL_KEY, VK_INSERT,};
fn key_is_down(key: VIRTUAL_KEY) -> bool { // 🛡️ SAFETY: every VIRTUAL_KEY value is a valid integer query. This call // only samples desktop input state and does not retain any borrowed pointer. unsafe { GetAsyncKeyState(i32::from(key.0)) < 0 }}
assert_eq!(key_is_down(VK_INSERT), key_is_down(VK_INSERT));Do not use result & 1 != 0 as a reliable “pressed once” event. Microsoft
keeps that low bit for old compatibility, but another process can observe it
first. Build your own edge detector from the trustworthy high bit:
#[derive(Default)]struct KeyEdge { was_down: bool,}
impl KeyEdge { fn pressed_now(&mut self, key: VIRTUAL_KEY) -> bool { let down = key_is_down(key); let rising_edge = down && !self.was_down; // ✅ one event per press self.was_down = down; rising_edge }}Predict first
KeyEdge starts with was_down = false, and the tool calls pressed_now once per poll.
The player holds Insert down across 5 polls in a row. How many times does pressed_now return true?
Answer: 1. Only the first down poll still has was_down = false, so true && !false = true there. The other four have was_down = true, so the hold reports exactly one press.
Eight polls
Each poll compares now with the poll before
Result
The key is down in 5 polls but changes from up to down once. A press is “down now and not down before”, so a held key reports one press, not one per poll.
Poll at a modest rate such as every 8–16 milliseconds. A tight loop wastes a CPU core. A zero result can also mean the desktop is inactive or Windows integrity-level restrictions prevent the query; “zero” does not prove a keyboard is broken.
Sampling can miss a very short press between polls. That is a property of polling, not a reason to trust the ambiguous low bit. For a tool hotkey, choose a comfortable key and polling interval, then test focus changes, key repeat, and shutdown while held.
Gate synthetic input to the intended window
Section titled “Gate synthetic input to the intended window”SendInput writes to the system input stream. It does not accept an HWND, so
check that the target owns the foreground window before sending:
use windows::Win32::{ Foundation::HWND, UI::WindowsAndMessaging::{GetForegroundWindow, GetWindowThreadProcessId},};
fn target_is_foreground(target_window: HWND, target_pid: u32) -> bool { // 🛡️ These calls return copied identifiers; neither keeps our pointers. let foreground = unsafe { GetForegroundWindow() }; if foreground != target_window { return false; }
let mut foreground_pid = 0_u32; unsafe { GetWindowThreadProcessId(foreground, Some(&mut foreground_pid)) }; foreground_pid == target_pid}Then send a balanced key-down/key-up pair:
use std::mem::size_of;use windows::Win32::UI::Input::KeyboardAndMouse::{ INPUT, INPUT_0, INPUT_KEYBOARD, KEYBDINPUT, KEYEVENTF_KEYUP, KEYBD_EVENT_FLAGS, SendInput, VIRTUAL_KEY,};
fn keyboard_event(key: VIRTUAL_KEY, flags: KEYBD_EVENT_FLAGS) -> INPUT { INPUT { r#type: INPUT_KEYBOARD, Anonymous: INPUT_0 { ki: KEYBDINPUT { wVk: key, dwFlags: flags, ..Default::default() }, }, }}
fn tap_key(key: VIRTUAL_KEY) -> anyhow::Result<()> { let events = [ keyboard_event(key, KEYBD_EVENT_FLAGS::default()), keyboard_event(key, KEYEVENTF_KEYUP), ]; let input_size = i32::try_from(size_of::<INPUT>())?;
// 🛡️ SAFETY: `events` is initialized and its element size matches INPUT. let sent = unsafe { SendInput(&events, input_size) }; anyhow::ensure!(sent == events.len() as u32, "sent {sent} of 2 events"); Ok(())}A key event is an INPUT whose type says “keyboard” and whose union holds a
KEYBDINPUT. The code above sets two of its fields and leaves the rest at zero:
wVk, the same virtual-key number GetAsyncKeyState takes, and dwFlags, zero
for a key going down and KEYEVENTF_KEYUP for its release.
The input API reference lists every field and
flag, including the scan-code form that some games read instead.
Always release a key or mouse button during shutdown, even if a feature fails.
SendInput is subject to User Interface Privilege Isolation (UIPI): Windows
blocks input from a lower-integrity process into a window owned by a
higher-integrity one, such as a game running as administrator while your tool
does not. Windows does not tell you why: neither SendInput’s return value
nor GetLastError identifies UIPI as the cause, and the target window simply
never reacts, which looks identical to a wrong virtual-key code or an unfocused
window.
If synthetic input silently does nothing, check whether the target is elevated before suspecting your own code. Some games also intentionally use Raw Input or device APIs that do not behave like a normal text box. Do not respond by repeatedly flooding input.
SendMessageW is not simulated keyboard input
Section titled “SendMessageW is not simulated keyboard input”Every Win32 window has a window procedure that receives numbered messages.
SendMessageW invokes that procedure synchronously and waits for it to finish.
PostMessageW puts a message on the owning thread’s queue and returns. For a
window outside your process, a bounded wait is usually safer than an unlimited
one:
use windows::Win32::{ Foundation::{HWND, LPARAM, WPARAM}, UI::WindowsAndMessaging::{ SendMessageTimeoutW, SMTO_ABORTIFHUNG, WM_APP, },};
const WM_LAB_REFRESH: u32 = WM_APP + 1;
fn ask_lab_window_to_refresh(window: HWND) -> anyhow::Result<usize> { let mut message_result = 0_usize;
// 🛡️ This custom message belongs to our own lab window. It carries only // integer values—never a pointer into this process. let delivered = unsafe { SendMessageTimeoutW( window, WM_LAB_REFRESH, WPARAM(0), LPARAM(0), SMTO_ABORTIFHUNG, 100, Some(&mut message_result), ) }; anyhow::ensure!(delivered.0 != 0, "lab window did not answer in 100 ms"); Ok(message_result)}Use PostMessageW when the receiver’s documented contract says queuing is
correct and no immediate result is required. Never pass a local pointer in
LPARAM to an unrelated process: Windows automatically marshals only certain
system messages below WM_USER. A custom WM_APP message is appropriate when
both sides share a pointer-free contract.
Most 3D games do not treat WM_KEYDOWN as authoritative gameplay input. They
may poll device state, consume Raw Input, or ignore messages while unfocused.
Sending a window message is therefore not a stronger version of SendInput;
it is a different communication path.
A controlled lab checklist
Section titled “A controlled lab checklist”- Start only the course’s local/offline target.
- Discover its window by PID and confirm it is foreground.
- Use
Insertonly to toggle your menu; detect the rising edge yourself. - Send one balanced input pair and count the events
SendInputaccepted. - Move focus to Notepad and verify the tool pauses instead of typing there.
- Shut down while a feature is active and verify every held input is released.
- Log errors and stop; never escalate privileges or flood retries automatically.
Optional input API references
Section titled “Optional input API references”The code above uses only the fields and calls already explained; these references list the remaining variants.
Optional reference: every KEYBDINPUT field and flag
KEYBDINPUT: the fields that matter
wVkvirtual-key coderequired
Which key. This is the number from VK_INSERT and its relatives, the same ones
GetAsyncKeyState takes.
dwFlagsflagsdefault 0
Zero means the key goes down. KEYEVENTF_KEYUP means it is released, which is why
the code sends one of each. KEYEVENTF_SCANCODE switches the event to the
hardware scan code in wScan instead.
wScanscan codedefault 0
The key’s position on the keyboard, used only with KEYEVENTF_SCANCODE. Some games
read scan codes, so a virtual-key event does not reach them.
timemillisecondsdefault 0
Zero lets Windows stamp the event itself.
dwExtraInfointegerdefault 0
An extra value the receiving window can read back. Almost nothing uses it.
Optional reference: other Windows APIs worth knowing
Choose by question instead of memorizing a giant list:
Other input and window APIs
| Question | Relevant APIs | Check to make |
|---|---|---|
| Which top-level window belongs to my PID? | EnumWindows, GetWindowThreadProcessId |
Match the process, not a changeable title |
| Where is the drawable client area on screen? | GetClientRect, ClientToScreen, GetDpiForWindow |
Recalculate after move, resize, or DPI change |
| Is the target active? | GetForegroundWindow |
Pause synthetic input when it is not |
| Which process/module build is this? | CreateToolhelp32Snapshot, Process32FirstW, Module32FirstW |
Verify identity before using offsets |
| What access does the tool truly need? | OpenProcess |
Ask for read/query rights before write rights |
| Can this remote range be read? | VirtualQueryEx, ReadProcessMemory |
Inspect the region, then copy bounded bytes |
| Did patched code become executable correctly? | VirtualProtectEx, FlushInstructionCache |
Save/restore protection and verify every byte |
| How do I observe execution? | DebugActiveProcess, WaitForDebugEvent, ContinueDebugEvent |
Continue every event exactly once |
| How do two local components exchange data? | named pipes, file mappings, events | Frame, authenticate, bound, and close resources |
An HWND is also called a handle, but it is owned by the window manager and is
not closed with CloseHandle. This is why “wrap every H... value in the same
RAII type” is wrong. Learn the creation, borrowing, and destruction contract
for each API family.
References
Section titled “References”Choose the actual input path
Which statement correctly distinguishes SendInput from SendMessageW?
They use different Windows paths. A window message is not a stronger form of device input, and neither API guarantees that a particular game consumes it.
Finished Lesson 6.9?
Mark it done and the lesson list shows a beside it. A chapter whose lessons are all done is marked done too. Progress is saved in this browser only.
Plain Markdown, saved in this browser.
This book grew out of the original Game Hacking Academy by attilathedud and has been rewritten as a beginner-first course. Like the original repository, its source is published under the Apache License 2.0.