win32 with rust: simple windows
Your First Windows Program is a canonical guide for creating a win32 window application. Although the example is written in C++, it is easy to translate the code to Rust with the windows crate. To brush up on my win32 skills and explore rust’s abstractions over win32 types, I decided to follow the guide and write a simple window application in rust.
The crate documentation provides a straightforward usage example that shows some text in the MessageBox. Since our focus is on creating windows, the process can be distilled into the following steps:
use windows::{
core::w,
Win32::UI::WindowsAndMessaging::{MessageBoxW, MB_ICONEXCLAMATION, MB_OK},
};
fn main() {
unsafe {
MessageBoxW(
None,
w!("Hello World!"),
w!("Just another Hello World program!"),
MB_ICONEXCLAMATION | MB_OK,
);
}
}
The code is a standard rust main
function, unlike the typical WinMain
used
inside C/C++ code. Although this example doesn’t require WinMain
arguments,
more complex application will. Therefore, we need to understand
how to retrieve and convert these values to the expected types.
To save up some time, I borrowed some ideas from minesweeper-rs, which is
a great project that the official windows-rs guide mentions.
After reviewing some code, consulting the official documentation
and refreshing my memory, I discovered the following:
- GetModuleHandleW returns the
handle to the current module. Rust doesn’t automatically select
API functions based on the character set (
*W
vs.*A
), but it’s manageable. The function returns anHMODULE
type, which differs fromHINSTANCE
in rust. Fortunately, both types implementFrom
trait, allowing seamless conversion between them. - GetStartupInfoW retrieves
the contents of the
STARTUPINFOW
structure. Some blogs suggest using itswShowWindow
member to determine the value ofnCmdShow
argument forWinMain
, but it seems to be more nuanced than that. It depends on dwFlags and the value can’t beSW_SHOWDEFAULT
. Since minesweeper-rs doesn’t usenCmdShow
, I won’t either.
With this knowledge, we can register a simple window class. Here is the code:
fn main() -> windows::core::Result<()> {
let instance: HINSTANCE = unsafe { GetModuleHandleW(None)?.into() };
let window_class = unsafe {
let class = WNDCLASSW {
lpszClassName: w!("Sample Window Class"),
lpfnWndProc: Some(proc),
hInstance: instance,
hCursor: LoadCursorW(None, IDC_ARROW)?,
..Default::default()
};
assert_ne!(RegisterClassW(&class), 0);
class
};
Ok(())
}
unsafe extern "system" fn proc(
window: HWND,
message: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
match message {
WM_DESTROY => unsafe {
PostQuitMessage(0);
LRESULT(0)
},
WM_PAINT => unsafe {
let mut paint = PAINTSTRUCT::default();
{
// device context
let dc = BeginPaint(window, &mut paint);
let color_index = SYS_COLOR_INDEX(COLOR_WINDOW.0 + 1);
FillRect(dc, &paint.rcPaint, GetSysColorBrush(color_index));
_ = EndPaint(window, &paint);
}
LRESULT(0)
},
_ => unsafe { DefWindowProcW(window, message, wparam, lparam) },
}
}
Once the window class is registered, we can proceed to create a window using the class name and display it:
let window = unsafe {
CreateWindowExW(
WINDOW_EX_STYLE(0), // Optional window styles.
window_class.lpszClassName, // Window class name
w!("Learn to Program Windows"), // Window text
WS_OVERLAPPEDWINDOW, // Window style
// Size and position
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
None, // Parent window
None, // Menu
Some(instance), // Instance handle
None, // Additional application data
)?
};
unsafe { _ = ShowWindow(window, SW_SHOW) };
Next, let’s implement a message processing loop within the main function:
let mut message = MSG::default();
unsafe {
// wait for the next message in the queue, store the result in 'message'
while GetMessageW(&mut message, None, 0, 0).into() {
_ = TranslateMessage(&message);
DispatchMessageW(&message);
}
}
The final step is to handle the program’s exit. We can either mimic the original code by returning Ok(())
or, for a more pedantic approach, return the wParam
of the WM_QUIT
message:
std::process::exit(message.wParam.0 as i32);
}
Running the application with cargo run
will display a window with a grey background:
The complete code:
use windows::{
core::w,
Win32::{
Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, WPARAM},
Graphics::Gdi::{
BeginPaint, EndPaint, FillRect, GetSysColorBrush, COLOR_WINDOW, PAINTSTRUCT,
SYS_COLOR_INDEX,
},
System::LibraryLoader::GetModuleHandleW,
UI::WindowsAndMessaging::{
CreateWindowExW, DefWindowProcW, DispatchMessageW, GetMessageW, LoadCursorW,
PostQuitMessage, RegisterClassW, ShowWindow, TranslateMessage, CW_USEDEFAULT,
IDC_ARROW, MSG, SW_SHOW, WINDOW_EX_STYLE, WM_DESTROY, WM_PAINT, WNDCLASSW,
WS_OVERLAPPEDWINDOW,
},
},
};
fn main() -> windows::core::Result<()> {
let instance: HINSTANCE = unsafe { GetModuleHandleW(None)?.into() };
let window_class = unsafe {
let class = WNDCLASSW {
lpszClassName: w!("Sample Window Class"),
lpfnWndProc: Some(proc),
hInstance: instance,
hCursor: LoadCursorW(None, IDC_ARROW)?,
..Default::default()
};
assert_ne!(RegisterClassW(&class), 0);
class
};
let window = unsafe {
CreateWindowExW(
WINDOW_EX_STYLE(0), // Optional window styles.
window_class.lpszClassName, // Window class name
w!("Learn to Program Windows"), // Window text
WS_OVERLAPPEDWINDOW, // Window style
// Size and position
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
None, // Parent window
None, // Menu
Some(instance), // Instance handle
None, // Additional application data
)?
};
unsafe { _ = ShowWindow(window, SW_SHOW) };
let mut message = MSG::default();
unsafe {
// wait for the next message in the queue, store the result in 'message'
while GetMessageW(&mut message, None, 0, 0).into() {
_ = TranslateMessage(&message);
DispatchMessageW(&message);
}
}
std::process::exit(message.wParam.0 as i32);
}
unsafe extern "system" fn proc(
window: HWND,
message: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
match message {
WM_DESTROY => unsafe {
PostQuitMessage(0);
LRESULT(0)
},
WM_PAINT => unsafe {
let mut paint = PAINTSTRUCT::default();
{
// device context
let dc = BeginPaint(window, &mut paint);
let color_index = SYS_COLOR_INDEX(COLOR_WINDOW.0 + 1);
FillRect(dc, &paint.rcPaint, GetSysColorBrush(color_index));
_ = EndPaint(window, &paint);
}
LRESULT(0)
},
_ => unsafe { DefWindowProcW(window, message, wparam, lparam) },
}
}
And Cargo.toml dependencies:
[dependencies.windows]
version = "0.60.0"
features = [
"Win32_Foundation",
"Win32_Security",
"Win32_System_Threading",
"Win32_System_LibraryLoader",
"Win32_UI_WindowsAndMessaging",
"Win32_Graphics_Gdi",
]