Skip to content
Lesson 6.9Windows Processes and Callable Interfaces

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.

Lesson 6.9
Read or listenPrint chapter or book
About 36 minutes

Keyboard shortcuts

?
Open this guide
Alt + B
Show or hide the lesson list
Alt + O
Show or fold the lesson outline
Alt + N
Open or close notes
Alt + H
Toggle semantic colours
Alt + D
Toggle page brightness
Alt + C
Toggle code brightness
← →
Move through a focused code trace
Home End
First or final step in a focused trace
Escape
Close this guide or an open panel

Reader shortcuts pause while you type in a text field.

Cheatsheet for Lesson 6.9Key ideas, reference and questions

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's SendKeys is a separate convenience layer, while the old keybd_event function has been superseded by SendInput.
  • Polling: Polling is the other side of this distinction.
  • INPUT: A key event is an INPUT whose type says “keyboard” and whose union holds a KEYBDINPUT.
  • SendInput: 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.
  • KEYEVENTF_KEYUP: KEYEVENTF_KEYUP means it is released, which is why the code sends one of each.
  • HWND: An HWND is also called a handle, but it is owned by the window manager and is not closed with CloseHandle.

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
Download cheatsheet (.md)

“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:

SendMessageW / PostMessageW
SendInput
GetAsyncKeyState
your code calls or queues a
message for a window procedure
your code inserts an event
into the system input stream
your code asks: is this
key down right now?
desktop-wide physical key state
whichever window currently
has keyboard focus
one specific HWND's
window procedure

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 edge

Result: 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
  1. Step 1, line 1. Start with the selected inputs. Follow the highlighted operation and the changing state below.

    previous = up; current = down.

  2. Step 2, line 2. Read the current level independently of the remembered level.

    previous = up; current = down; was_down = false; is_down = true.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

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 window
loop retrieves it
DispatchMessage calls that window's procedure
procedure handles it and returns
loop retrieves the next message

GetMessage 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 frame

The waiting path and the continuously updating path look different:

wait for a message: the thread sleeps
handle it
handle waiting messages; continue at once if there are none
update the world
draw the frame

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.

being handledWM_KEYDOWN, reading a large file
waitingWM_MOUSEMOVE
waitingWM_PAINT
waitingWM_CLOSE
these messages wait until the first handler returns
One thread's message queue. A slow first handler delays movement, repainting, and close handling.

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 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_SIZE with 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 GetAsyncKeyState still 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).

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:

bit 15 (sign)1 if down now
bits 14-1unused here
bit 0stale “pressed since last call” flag
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.

KeyEdge over eight pollspressed_now is down && !was_down, and was_down starts false. Click a poll to press or release the key there. 1 = down, ✓ = true.

Eight polls

Each poll compares now with the poll before

key_is_down
poll 1poll 8
0
1
1
1
1
1
0
0
was_down (before)
0
0
1
1
1
1
1
0
pressed_now
–
✓
–
–
–
–
–
–

Result

Presses reported
1 press, at poll 2

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.

equal or lower
higher, e.g. game runs as admin
SendInput(events)
target window's integrity level
vs. your process's integrity level
event reaches the window normally
UIPI blocks the event
nothing says UIPI was the cause:
the window simply never reacts

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.

  1. Start only the course’s local/offline target.
  2. Discover its window by PID and confirm it is foreground.
  3. Use Insert only to toggle your menu; detect the rising edge yourself.
  4. Send one balanced input pair and count the events SendInput accepted.
  5. Move focus to Notepad and verify the tool pauses instead of typing there.
  6. Shut down while a feature is active and verify every held input is released.
  7. Log errors and stop; never escalate privileges or flood retries automatically.

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.

The four flags and how they combine
Flag Value Meaning
KEYEVENTF_EXTENDEDKEY 0x0001 the scan code carries the 0xE0 prefix that extended keys use, such as the arrow keys in their own cluster and the right-hand Ctrl and Alt
KEYEVENTF_KEYUP 0x0002 the key is released
KEYEVENTF_UNICODE 0x0004 wScan holds a Unicode character instead of a key, and wVk must be zero
KEYEVENTF_SCANCODE 0x0008 wScan identifies the key, and wVk is ignored

Flags are bits, so they combine with OR. Releasing a key by scan code sets two of them: 0x0008 | 0x0002 = 0x000A, which is 8 + 2 = 10.

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.

Knowledge check

Choose the actual input path

Which statement correctly distinguishes SendInput from SendMessageW?

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.

Chapter 6: 11 lessons

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.