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 an HMODULE type, which differs from HINSTANCE in rust. Fortunately, both types implement From trait, allowing seamless conversion between them.
  • GetStartupInfoW retrieves the contents of the STARTUPINFOW structure. Some blogs suggest using its wShowWindow member to determine the value of nCmdShow argument for WinMain, but it seems to be more nuanced than that. It depends on dwFlags and the value can’t be SW_SHOWDEFAULT. Since minesweeper-rs doesn’t use nCmdShow, 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:

window

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",
]