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.
To display an image on the top-left of the screen.
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.
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()
}
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.
Note: I was going to finish up this answer, but the update turned out to be 13396 characters too long. If you want code, you'll have to convince SE Inc. that the 30000 characters-per-contribution limit needs to be lifted.
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.
Creating a color bitmap:
The documentation for CreateCompatibleBitmap
explains why the code winds up with a monochrome bitmap and how to fix the issue:
When a memory device context is created, it initially has a 1-by-1 monochrome bitmap selected into it. If this memory device context is used in
CreateCompatibleBitmap
, the bitmap that is created is a monochrome bitmap. To create a color bitmap, use theHDC
that was used to create the memory device context [...].
To create a color bitmap the following function call
CreateCompatibleBitmap(hdc, width as i32, height as i32)
needs to be replaced with this
CreateCompatibleBitmap(hdc_screen, width as i32, height as i32)
Using device contexts:
The device context (DC) is at the center of Windows' GDI. All GDI render calls are issued against a DC for processing. In performing this task the DC takes on two responsibilities:
Device independence:
The DC is the core abstraction in supporting device independence. It exposes a common interface to clients while internally delegating drawing operations to "device drivers" for translation into device-specific commands.
DCs support rendering to a wide array of "devices", such as (physical or virtual) printers, window surfaces, files, or in-memory bitmaps.
Render context:
A DC also maintains a set of graphic objects used in drawing operations. Graphic objects (pen, brush, font, etc.) are swapped in and out of a DC with the SelectObject
function. Render calls use the "current" graphic object as appropriate. Rectangle
, for example, uses the DC's pen for the outline and the brush for the fill.
Since graphic objects are resource-owning and the DC assumes (temporary) ownership of the "current" graphic object of any given type, clients are responsible for managing resources. When a DC is destroyed or released it takes down its graphic objects with it. Failure to restore a DC to its initial state prior to letting go is a resource leak and a double-free bug1.
With the DC fundamentals covered, there's one graphic object that I feel needs special treatment: While graphic objects generally affect the visual outcome of drawing operations, bitmaps control the destination (or source).
Equipped with this information it should be easy to understand why the following changes are required. load_image_as_bitmap
needs to prepare the DC by selecting the destination bitmap:
fn load_image_as_bitmap(image_path: &str) -> HGDIOBJ {
// ...
// Select the bitmap into the destination device context
let prev_bmp = unsafe { SelectObject(hdc, hbitmap) };
let res = unsafe { SetDIBitsToDevice(...) };
// ...
// Restore the device context
unsafe { SelectObject(hdc, prev_bmp) };
// ...
}
The same resource management ceremony has to be applied to the code under the WM_PAINT
message handler:
unsafe extern "system" fn window_proc(...) -> LRESULT {
match msg {
WM_PAINT => {
// ...
if let Some(bitmap) = BITMAP_HANDLE {
// ...
// Store previous bitmap so that we can later restore the DC
let prev_bmp = unsafe { SelectObject(hdc_mem, bitmap) };
// Blit the bitmap to the window
if BitBlt(...).is_err() {
// ...
}
// Restore DC to its initial state prior to deletion
unsafe { SelectObject(hdc_mem, prev_bmp) };
// ...
}
// ...
}
// ...
}
}
The above changes applied the window finally starts displaying an image. In all likeliness this isn't the image you expected, but at least it's progress. This issue is down to the to_rgba8()
call. This call does what it promises, but Windows' native 32bpp color encoding is BGRA (not RGBA). The image displayed thus has its B and G color channels flipped.
I'm sure this could be addressed using the image
crate, but I'll propose a competing approach to image decoding further down2.
This addresses the immediate issues. What's following is fixes to the remaining issues as well as advice on properly using the windows
crates.
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 PostQuitMessage
3 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:
window_proc
consists of a match
expression only, meaning that whatever this expression produces is treated as the function's return value. There's no need for a return
statement (similarly for the WM_PAINT
arm).unsafe
-block even though the function is marked as unsafe
. This will become mandatory once the unsafe_block_in_unsafe_fn
RFC is implemented and deployed.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:
windows
crate provides (automatic) conversions to optional pointer values. None
is generally accepted in place of a null pointer.unwrap
call feels hand-wavey. We'll replace that with something more robust shortly.This replace the "hourglass" fallback cursor with a standard arrow cursor.
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.
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.
Param<T, C>
trait boundMany 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.