windowswinapirustwindows-rs

Image not displaying on screen using Win32 API in Rust


Disclaimer

HBITMAP, created by CreateCompatibleBitmap is black and white THIS IS A SIMILAR ISSUE, BUT NOT THE SAME. I understand the second context is monochrome, but I DO NOT UNDERSTAND HOW TO NOT MAKE IT MONOCHROME.

Goal

To display an image on the top-left of the screen.

Problem

The code I wrote to achieve this does not display the image. Instead, it displays an all-black square. I have ensured that the image path is correct and tried multiple images. I am new to the Windows API so I am pretty lost. Forgive me if I missed something obvious. enter image description here

Code

https://hatebin.com/buvhsvkqvz

use image::{GenericImageView, ImageBuffer, Rgba};
use windows::{
    core::*,
    Win32::{
        Foundation::{COLORREF, HINSTANCE, HWND, LPARAM, LRESULT, WPARAM, GetLastError}, 
        Graphics::Gdi::{BeginPaint, BitBlt, CreateCompatibleBitmap, CreateCompatibleDC, DeleteDC, DeleteObject, EndPaint, GetDC, ReleaseDC, SelectObject, SetDIBitsToDevice, UpdateWindow, BITMAPINFO, BITMAPINFOHEADER, DIB_RGB_COLORS, HBRUSH, HGDIOBJ, PAINTSTRUCT, RGBQUAD, SRCCOPY}, 
        System::LibraryLoader::GetModuleHandleW, 
        UI::{HiDpi::{SetProcessDpiAwareness, PROCESS_PER_MONITOR_DPI_AWARE}, WindowsAndMessaging::{CreateWindowExW, DefWindowProcW, DispatchMessageW, GetMessageW, RegisterClassExW, SetLayeredWindowAttributes, ShowWindow, TranslateMessage, CS_HREDRAW, CS_VREDRAW, HCURSOR, HMENU, LWA_ALPHA, SHOW_WINDOW_CMD, WM_PAINT, WNDCLASSEXW, WS_EX_LAYERED, WS_EX_TOPMOST, WS_POPUP}}
    }
};

static mut BITMAP_HANDLE: Option<HGDIOBJ> = None;

fn main() {
    unsafe {
        // DPI awareness setup
        let _ = SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);
        
        // Get instance handle
        let handle_instance = GetModuleHandleW(None).unwrap();
        let window_class = "Glorp Class";
        let window_name = "Glorp Overlay";

        // Register window class
        let wc = WNDCLASSEXW {
            cbSize: std::mem::size_of::<WNDCLASSEXW>() as u32,
            style: CS_HREDRAW | CS_VREDRAW,
            lpfnWndProc: Some(window_proc),
            hInstance: handle_instance.into(),
            hCursor: HCURSOR(std::ptr::null_mut()),
            hbrBackground: HBRUSH(std::ptr::null_mut()),
            lpszClassName: w!("Glorp Class"),
            ..Default::default()
        };

        if RegisterClassExW(&wc) == 0 {
            panic!("Failed to register window class.");
        }

        // Load the image as a bitmap
        BITMAP_HANDLE = Some(load_image_as_bitmap("C:\\Users\\mdabr\\Downloads\\IMG_7592.jpg"));

        // Create the window
        let hwnd = CreateWindowExW(
            WS_EX_LAYERED | WS_EX_TOPMOST, 
            &HSTRING::from(window_class), 
            &HSTRING::from(window_name),
            WS_POPUP, 
            0, 
            0, 
            300,
            300, 
            HWND(std::ptr::null_mut()),
            HMENU(std::ptr::null_mut()), 
            handle_instance, 
            Some(std::ptr::null_mut())
        ).unwrap();

        // Set window transparency
        let transparency = 255;
        if SetLayeredWindowAttributes(hwnd, COLORREF(0), transparency, LWA_ALPHA).is_err() {
            eprintln!("Failed to set layered window attributes: {}", GetLastError().0);
        }

        // Show and update window
        ShowWindow(hwnd, SHOW_WINDOW_CMD(1));
        UpdateWindow(hwnd);

        // Message loop
        let mut msg = std::mem::zeroed();
        while GetMessageW(&mut msg, HWND(std::ptr::null_mut()), 0, 0).into() {
            TranslateMessage(&msg);
            DispatchMessageW(&msg);
        }

        // Clean up the bitmap handle
        if let Some(bitmap) = BITMAP_HANDLE {
            DeleteObject(bitmap);
        }
    }
}

unsafe extern "system" fn window_proc(
    hwnd: HWND,
    msg: u32,
    wparam: WPARAM,
    lparam: LPARAM,
) -> LRESULT {
    match msg {
        WM_PAINT => {
            let mut ps: PAINTSTRUCT = std::mem::zeroed();
            let hdc = BeginPaint(hwnd, &mut ps);
            
            if let Some(bitmap) = BITMAP_HANDLE {
                let hdc_mem = CreateCompatibleDC(hdc);
                if hdc_mem.0.is_null() {
                    eprintln!("Failed to create compatible DC: {}", GetLastError().0);
                }

                if SelectObject(hdc_mem, bitmap).0.is_null() {
                    eprintln!("Failed to select bitmap into DC: {}", GetLastError().0);
                }

                // Blit the bitmap to the window
                if BitBlt(hdc, 0, 0, 300, 300, hdc_mem, 0, 0, SRCCOPY).is_err() {
                    eprintln!("BitBlt failed: {}", GetLastError().0);
                }

                DeleteDC(hdc_mem);
            }

            EndPaint(hwnd, &ps);
            return LRESULT(0);
        }
        _ => DefWindowProcW(hwnd, msg, wparam, lparam),
    }
}

fn load_image_as_bitmap(image_path: &str) -> HGDIOBJ {
    // Load the image
    let img = image::open(image_path).expect("Failed to load image");
    let (width, height) = img.dimensions();
    let img = img.to_rgba8(); // Convert to RGBA

    // Create a device context for the screen
    let hdc_screen = unsafe { GetDC(HWND(std::ptr::null_mut())) };
    if hdc_screen.0.is_null() {
        panic!("Failed to get screen DC: {}", unsafe { GetLastError().0 });
    }

    // Create a compatible device context using the screen's DC
    let hdc = unsafe { CreateCompatibleDC(hdc_screen) };
    if hdc.0.is_null() {
        panic!("Failed to create compatible DC: {}", unsafe { GetLastError().0 });
    }

    // Create a compatible bitmap using the screen's DC
    let hbitmap = unsafe { CreateCompatibleBitmap(hdc, width as i32, height as i32) };
    if hbitmap.0.is_null() {
        panic!("Failed to create compatible bitmap: {}", unsafe { GetLastError().0 });
    }

    // Set bitmap data
    let bmp_info_header = BITMAPINFOHEADER {
        biSize: std::mem::size_of::<BITMAPINFOHEADER>() as u32,
        biWidth: width as i32,
        biHeight: -(height as i32), // Negative for top-down bitmap
        biPlanes: 1,
        biBitCount: 32,
        biCompression: 0,
        ..Default::default()
    };

    let mut bmp_info = BITMAPINFO {
        bmiHeader: bmp_info_header,
        bmiColors: [RGBQUAD::default(); 1],
    };

    unsafe {
        let res = SetDIBitsToDevice(
            hdc,
            0,
            0,
            width as u32,
            height as u32,
            0,
            0,
            0,
            height as u32,
            img.as_raw().as_ptr() as *const _,
            &mut bmp_info,
            DIB_RGB_COLORS,
        );

        if res == 0 {
            eprintln!("SetDIBitsToDevice failed: {}", GetLastError().0);
        }
    }

    // Clean up
    unsafe {
        DeleteDC(hdc);
        ReleaseDC(HWND(std::ptr::null_mut()), hdc_screen);
    }

    hbitmap.into()
}


Solution

  • Note: I didn't find the time to complete this proposed answer. There's still a bit of territory to cover. However, I believe that it's helpful already, so I went ahead and published it. I'll keep updating this in the future and own up to the promises I made.


    There are several problems with the code, but the core issue is found in the load_image_as_bitmap function: It creates a monochrome (1bpp) bitmap and never draws into it. The result is that BITMAP_HANDLE refers to a fully black image that's being displayed under the WM_PAINT message handler. This coincides with the observed behavior.

    The solution is fairly simple but requires a working knowledge of Windows' GDI.

    This addresses the immediate issues. What's following is fixes to the remaining issues as well as advice on properly using the windows crates.


    Process termination

    The program currently doesn't support a clean exit. While its main window can be destroyed via the Alt+F4 keyboard shortcut, the process continues running its message loop. The only way to terminate the process is by pressing Ctrl+C on the console the system allocates (which we'll get rid of further down), yielding

    error: process didn't exit successfully: `program.exe` (exit code: 0xc000013a, STATUS_CONTROL_C_EXIT)
    

    This doesn't look nice (and hints at a potential vulnerability which we'll address later). A better way to shut down the process is due.

    Luckily, almost everything that's required is already implemented. Default processing of the Alt+F4 shortcut results in a DestroyWindow call issued against the focus window of the foreground thread. This sends a WM_NCDESTROY message to this window, giving us the opportunity to notify the message loop to close shop. The PostQuitMessage3 does just that.

    The following change to the window_proc implements clean process termination via the Alt+F4 shortcut:

    unsafe extern "system" fn window_proc(...) -> LRESULT {
        match msg {
            WM_NCDESTROY => {
                unsafe { PostQuitMessage(0) };
                LRESULT(0)
            }
            // ...
        }
    }
    

    There are two points worth noting:

    The "hourglass" cursor

    A window has two ways to control the cursor shape as the mouse moves across it: Dynamically (by handling the WM_SETCURSOR message), or statically (via the hCursor field of the WNDCLASSEXW structure used in window class registration).

    Since the program doesn't handle the WM_SETCURSOR message and supplies a null pointer to the hCursor field, the system picks a fallback cursor shape as the mouse moves across it. That's why you see an "hourglass" cursor shape.

    The easiest way to fix this is by providing a standard cursor shape during class registration. LoadCursorW knows how to procure standard cursor shapes:

    let wc = WNDCLASSEXW {
        // ...
        hCursor: LoadCursorW(None, IDC_ARROW).unwrap(),
        // ...
    };
    

    Again, two points worth mentioning:

    This replace the "hourglass" fallback cursor with a standard arrow cursor.

    User interaction [optional]

    Since the window is created without any window chrome or system menu there's no way for a user to interact with it (other than closing it). It may still be desirable to allow users to move the window around by clicking anywhere inside the window.

    The customization point for this is the WM_NCHITTEST message. It is sent to a window whenever the system needs to determine what part of the window has been clicked. The following update to the window_proc has the system assume that the (inexistent) window caption had been clicked whenever the window's client was:

    unsafe extern "system" fn window_proc(...) -> LRESULT {
        match msg {
            WM_NCHITTEST => {
                let res = unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }.0;
                if res == HTCLIENT as usize as isize {
                    LRESULT(HTCAPTION as usize as isize)
                } else {
                    LRESULT(res)
                }
            }
            // ...
        }
    }
    

    This allows the window to be moved by clicking and holding the left mouse button. Note that the as usize as isize casts seem superfluous. Their purpose is to prevent sign-extension. I haven't checked whether any of the 32-bit constants have the MSB set, but since there is no adverse effect I'm keeping the casts even if not strictly required.

    Error propagation

    Rust's error propagation is built on the Result enum type. In combination with the question mark operator this allows for convenient error handling. Applying ? to a Result expression either unwraps the value or returns the error as long as the enclosing function returns a Result. Starting with Rust 1.26 fn main() can return a Result making error propagation a very streamlined experience. Building on this infrastructure mostly eliminates the need for any unwrap, expect, or eprintln! calls4.

    The windows crate has separated out its specialized Result type into the windows-result crate. The goal is (presumably) to make this a stable type that can appear in public interfaces. This isn't something to worry about in self-contained program, but I'll be using it for illustration.

    There are two popular crates that make dealing with errors easier as a library or application grows: thiserror provides with much of the boilerplate code when composing custom error types. This is commonly used in library code. anyhow supports use cases where the specific type of error isn't important. It is useful in constructing user-facing error messages.

    I will be using windows-result only here. The full program is at the end of this post.

    The mysterious Param<T, C> trait bound

    Many functions in the windows crate are generic. As an example, the GetDC function is declared as

    pub unsafe fn GetDC<P0>(hwnd: P0) -> HDC
    where
        P0: Param<HWND>,
    

    There isn't any more information from the documentation (version 0.58.0 at the time of writing). In particular, the Param used in the trait bound isn't hyperlinked5, giving no clue as to what it is or why it's there. You have to know where to go looking for it: The Param trait is defined in the windows-core crate with a somewhat terse description:

    Provides automatic parameter conversion in cases where the Windows API expects implicit conversion support.

    Even with this information it is very hard to understand that it allows use to replace

    GetDC(HWND(std::ptr::null_mut()))
    

    with

    GetDC(None)
    

    There's more conversion support hidden behind the (undocumented?) TypeKind trait. The SelectObject generic, for example, allows passing an HPEN that gets automatically converted into a generic HGDIOBJ (the load_image_as_bitmap function is already making use of this). Automatic parameter conversion is incredibly useful in practice, but also incredibly hard to discover.

    As implemented, automatic conversion works for function parameters only. This tool isn't available when, e.g., populating a structure (like WNDCLASSEXW).


    1 The system implements safeguards to protect against (common) bugs. Not every violation of the documented specification results in a leak and double-free. However, this is non-contractual behavior.

    2 The image crate is massive in sheer code size, and it is slow. Plus, it pulls in ~100 crates to implement its functionality. That's demanding way more blind faith than I'm willing to expend.

    3 Why is there a special PostQuitMessage function? answers questions you may not even have asked.

    4 This requires all participating functions to return a Result, which isn't always possible. Notably, the window_proc has a fixed signature that cannot be changed to match, so custom error handling is still required. Keep in mind that ? potentially hides a return statement. This is important when holding on to resources that require manual cleanup (such as DCs). Crates like scopeguard aid in disarming this footgun.

    5 This is a bad situation, that apparently cannot easily be fixed. rustdoc works until you throw a large crate at it. Over time this forced the windows crate to repeatedly strip the amount of documentation it contains, much to my dismay.