windows_helpers/win32_app/
window.rs

1use crate::{
2    core::{CheckNullError, CheckNumberError, ResultExt},
3    windows, Null, Zeroed,
4};
5use std::{cell::Cell, mem};
6use windows::{
7    core::{HSTRING, PCWSTR},
8    Win32::{
9        Foundation::{SetLastError, ERROR_SUCCESS, HWND, LPARAM, LRESULT, POINT, SIZE, WPARAM},
10        System::{LibraryLoader::GetModuleHandleW, Performance::QueryPerformanceCounter},
11        UI::WindowsAndMessaging::{
12            CreateWindowExW, DefWindowProcW, DestroyWindow, GetWindowLongPtrW, IsWindow,
13            RegisterClassExW, SetWindowLongPtrW, UnregisterClassW, CW_USEDEFAULT, GWLP_USERDATA,
14            HMENU, HWND_MESSAGE, WINDOW_EX_STYLE, WINDOW_STYLE, WNDCLASSEXW,
15        },
16    },
17};
18
19mod translate;
20
21pub use translate::*;
22
23thread_local! {
24    static NEXT_WINDOW_USER_DATA_ON_INIT: Cell<isize> = const { Cell::new(0) };
25}
26
27// For trait bounds in this API.
28pub trait WndProc: FnMut(HWND, u32, WPARAM, LPARAM) -> Option<LRESULT> {}
29
30// For accepting any matching closure type where the trait bound is required.
31impl<F> WndProc for F where F: FnMut(HWND, u32, WPARAM, LPARAM) -> Option<LRESULT> {}
32
33/// A window class registered with `RegisterClassExW()`, containing a window procedure closure. Necessary for creating windows.
34///
35/// - Don't drop it before any [`Window`]s created with it, because this tries to unregister the class (struct field order is relevant).
36/// - Don't use `Get...`/`SetWindowLongPtrW(...GWLP_USERDATA...)` on a window created from an instance of this struct, because it stores internal data necessary for the struct to function.
37pub struct WindowClass<'a> {
38    atom: u16,
39    /// Double-`Box`, converted with `Box::into_raw()` (to get thin pointer).
40    wnd_proc_ptr: *mut Box<dyn WndProc + 'a>,
41}
42
43impl<'a> WindowClass<'a> {
44    pub fn new<F>(wnd_proc: F) -> windows::core::Result<Self>
45    where
46        F: WndProc + 'a,
47    {
48        //! Creates a new class with a name from [`Self::make_name()`].
49        //!
50        //! Pass the window procedure of the class that you implement. Its parameters are:
51        //!
52        //! - Without types: `hwnd, msg_id, wparam, lparam`
53        //! - With types: `hwnd: HWND, msg_id: u32, wparam: WPARAM, lparam: LPARAM`
54        //!
55        //! Return `None` from the procedure to cause [`DefWindowProcW()`][1] being called and its return value being used. You sometimes should also call it yourself when handling certain messages and returning `Some(...)`.
56        //!
57        //! Note that some functions like, e.g., [`DestroyWindow()`][2] and [`MoveWindow()`][3] synchronously cause the window procedure to be called again during their calls. This means that closing over an [`Rc<RefCell<...>>`](std::cell::RefCell) and calling `borrow_mut()` to call your actual procedure implementation (can be a method with self parameter) will cause a borrowing panic. Using `Rc<ReentrantRefCell<...>>` instead solves this. See [`crate::ReentrantRefCell`] for more information.
58        //!
59        //! [1]: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-defwindowprocw
60        //! [2]: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-destroywindow
61        //! [3]: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-movewindow
62
63        Self::with_name(&Self::make_name()?, wnd_proc)
64    }
65
66    pub fn with_name<F>(name: &str, wnd_proc: F) -> windows::core::Result<Self>
67    where
68        F: WndProc + 'a,
69    {
70        Self::with_details(
71            WNDCLASSEXW {
72                cbSize: mem::size_of::<WNDCLASSEXW>() as _,
73                lpfnWndProc: Some(Self::base_wnd_proc),
74                hInstance: unsafe { GetModuleHandleW(PCWSTR::NULL)? }.into(),
75                lpszClassName: PCWSTR(HSTRING::from(name).as_ptr()),
76                ..Default::default()
77            },
78            wnd_proc,
79        )
80    }
81
82    pub fn with_details<F>(
83        mut wnd_class_ex: WNDCLASSEXW,
84        wnd_proc: F,
85    ) -> windows::core::Result<Self>
86    where
87        F: WndProc + 'a,
88    {
89        //! The `lpfnWndProc` field will be overwritten.
90
91        wnd_class_ex.lpfnWndProc = Some(Self::base_wnd_proc);
92
93        Ok(Self {
94            atom: unsafe { RegisterClassExW(&wnd_class_ex) }.nonzero_or_win32_err()?,
95            // Double indirection to get thin pointer.
96            wnd_proc_ptr: Box::into_raw(Box::new(Box::new(wnd_proc))),
97        })
98    }
99
100    pub fn make_name() -> windows::core::Result<String> {
101        //! Generates a time-based class name.
102
103        let mut precise_time = 0;
104        unsafe { QueryPerformanceCounter(&mut precise_time)? };
105
106        Ok(format!("unnamed_{precise_time:x}"))
107    }
108
109    pub fn atom(&self) -> u16 {
110        self.atom
111    }
112
113    extern "system" fn base_wnd_proc(
114        hwnd: HWND,
115        msg_id: u32,
116        wparam: WPARAM,
117        lparam: LPARAM,
118    ) -> LRESULT {
119        // Retrieve saved window procedure.
120        let mut user_data = unsafe {
121            SetLastError(ERROR_SUCCESS);
122            GetWindowLongPtrW(hwnd, GWLP_USERDATA)
123        };
124
125        // On first message, save window procedure for subsequent calls. This is the first time that the `HWND` is known.
126        if user_data == 0 {
127            //. Consume value, so failing once below makes for failing on subsequent calls (until `CreateWindowExW()` was aborted).
128            user_data = NEXT_WINDOW_USER_DATA_ON_INIT.replace(0);
129
130            let result = Result::<(), windows::core::Error>::from_win32().and_then(|_| unsafe {
131                SetLastError(ERROR_SUCCESS);
132                SetWindowLongPtrW(hwnd, GWLP_USERDATA, user_data).nonzero_with_win32_or_err()
133            });
134
135            if result.is_err() {
136                // Make `CreateWindowExW()` fail.
137                // (First message may be `WM_GETMINMAXINFO`, then, `WM_NCCREATE` is expected, which still happens during the `CreateWindowExW()` call. `LRESULT(0)` indicates an error for `WM_NCCREATE`, while it indicates success for `WM_GETMINMAXINFO` and many other messages.)
138                return LRESULT(0);
139            }
140        };
141
142        // Call window procedure.
143        // (Outer box was dissolved into raw pointer, whose data is simply referenced here. The `Box` you see is the inner `Box`.)
144        let wnd_proc = unsafe { &mut *(user_data as *mut Box<dyn WndProc>) };
145
146        if let Some(lresult) = wnd_proc(hwnd, msg_id, wparam, lparam) {
147            lresult
148        } else {
149            // Call default message handler.
150            unsafe { DefWindowProcW(hwnd, msg_id, wparam, lparam) }
151        }
152    }
153}
154
155impl Drop for WindowClass<'_> {
156    fn drop(&mut self) {
157        unsafe {
158            if let Ok(h_module) = GetModuleHandleW(PCWSTR::NULL) {
159                let result = UnregisterClassW(PCWSTR(self.atom as _), h_module);
160                debug_assert!(
161                    result.is_ok(),
162                    "couldn't unregister window class (did you adhere to proper drop order?): {result:?}"
163                );
164            }
165
166            drop(Box::from_raw(self.wnd_proc_ptr));
167        }
168    }
169}
170
171/// A window created with a [`WindowClass`].
172///
173/// The first calls of the window procedure are made during the constructor call; then during the message loop.
174pub struct Window {
175    hwnd: HWND,
176}
177
178impl Window {
179    pub fn new_msg_only(class: &WindowClass) -> windows::core::Result<Self> {
180        //! Creates a message-only window.
181        //!
182        //! See <https://learn.microsoft.com/en-us/windows/win32/winmsg/window-features#message-only-windows>.
183
184        Self::with_details(
185            class,
186            Some(HWND_MESSAGE),
187            WINDOW_STYLE(0),
188            None,
189            None,
190            None,
191            None,
192        )
193    }
194
195    pub fn new_invisible(class: &WindowClass) -> windows::core::Result<Self> {
196        //! Meant for windows that stay invisible. Necessary instead of a message-only window, if you want to receive broadcast messages like `WM_ENDSESSION` or `RegisterWindowMessageW(w!("TaskbarCreated"))`.
197
198        Self::with_details(
199            class,
200            None,
201            WINDOW_STYLE(0),
202            None,
203            Some((POINT::zeroed(), SIZE::zeroed())),
204            None,
205            None,
206        )
207    }
208
209    pub fn with_details(
210        class: &WindowClass,
211        parent: Option<HWND>,
212        style: WINDOW_STYLE,
213        ex_style: Option<WINDOW_EX_STYLE>,
214        placement: Option<(POINT, SIZE)>,
215        text: Option<PCWSTR>,
216        menu: Option<HMENU>,
217    ) -> windows::core::Result<Self> {
218        //! Creates a window with `CreateWindowExW()`.
219        //!
220        //! `None` for `placement` uses `CW_USEDEFAULT` for all four values.
221
222        // Pass window procedure via thread-local storage instead of `CREATESTRUCTW`, because `WM_GETMINMAXINFO` can be sent before `WM_NCCREATE`.
223        NEXT_WINDOW_USER_DATA_ON_INIT.set(class.wnd_proc_ptr as _);
224
225        // Create window.
226        let (pos, size) = placement.unwrap_or((
227            POINT {
228                x: CW_USEDEFAULT,
229                y: CW_USEDEFAULT,
230            },
231            SIZE {
232                cx: CW_USEDEFAULT,
233                cy: CW_USEDEFAULT,
234            },
235        ));
236
237        let hwnd = unsafe {
238            CreateWindowExW(
239                ex_style.unwrap_or(WINDOW_EX_STYLE(0)),
240                PCWSTR(class.atom as _),
241                text.unwrap_or(PCWSTR::NULL),
242                style,
243                pos.x,
244                pos.y,
245                size.cx,
246                size.cy,
247                parent.unwrap_or(HWND::NULL),
248                menu.unwrap_or(HMENU::NULL),
249                GetModuleHandleW(PCWSTR::NULL)?,
250                None,
251            )
252        };
253        #[cfg(any(feature = "windows_v0_48", feature = "windows_v0_52"))]
254        let hwnd = hwnd.nonnull_or_e_handle()?; // Checking `GetLastError()` would be better.
255        #[cfg(not(any(feature = "windows_v0_48", feature = "windows_v0_52")))]
256        let hwnd = hwnd?;
257
258        Ok(Self { hwnd })
259    }
260
261    pub fn hwnd(&self) -> HWND {
262        self.hwnd
263    }
264
265    pub fn is_valid(&self) -> bool {
266        //! Returns whether the associated `HWND` is still valid.
267        //!
268        //! It isn't valid anymore, if [`DestroyWindow()`][1] was called.
269        //!
270        //! [1]: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-destroywindow
271
272        unsafe { IsWindow(self.hwnd) }.as_bool()
273    }
274}
275
276impl Drop for Window {
277    fn drop(&mut self) {
278        // Calling it again when it was already called on the window is simply a no-op. This can regularly happen, when, e.g., `DefWindowProcW()` calls it on `WM_CLOSE`.
279        let _ = unsafe { DestroyWindow(self.hwnd) };
280    }
281}
282
283#[cfg(all(test, feature = "windows_latest_compatible_all"))]
284mod tests {
285    use super::{Window, WindowClass};
286    use crate::{foundation::LParamExt, win32_app::msg_loop, windows, Null};
287    use std::{cell::RefCell, rc::Rc};
288    use windows::{
289        core::{w, HSTRING, PCWSTR},
290        Win32::{
291            Foundation::{HWND, LRESULT, POINT, SIZE},
292            UI::WindowsAndMessaging::{
293                MessageBoxW, PostQuitMessage, MB_OK, MINMAXINFO, WM_DESTROY, WM_GETMINMAXINFO,
294                WM_LBUTTONUP, WS_OVERLAPPEDWINDOW, WS_VISIBLE,
295            },
296        },
297    };
298
299    #[ignore]
300    #[test]
301    fn create_window() -> windows::core::Result<()> {
302        let counter = Rc::new(RefCell::new(1));
303
304        let class = WindowClass::new(|hwnd, msg_id, wparam, mut lparam| {
305            println!("window msg received: {hwnd:?}, msg 0x{msg_id:04x}, {wparam:?}, {lparam:?}");
306
307            match msg_id {
308                WM_LBUTTONUP => {
309                    *counter.borrow_mut() += 1;
310
311                    unsafe {
312                        MessageBoxW(
313                            HWND::NULL,
314                            PCWSTR(HSTRING::from(format!("{counter:?}")).as_ptr()),
315                            w!("Message Box"),
316                            MB_OK,
317                        )
318                    };
319
320                    Some(LRESULT(0))
321                }
322                WM_GETMINMAXINFO => {
323                    let min_max_info = unsafe { lparam.cast_to_mut::<MINMAXINFO>() };
324                    min_max_info.ptMaxTrackSize = POINT { x: 300, y: 300 };
325
326                    Some(LRESULT(0))
327                }
328                WM_DESTROY => {
329                    unsafe { PostQuitMessage(0) };
330                    Some(LRESULT(0))
331                }
332                _ => None,
333            }
334        })?;
335
336        *counter.borrow_mut() += 1;
337
338        let _window = Window::with_details(
339            &class,
340            None,
341            WS_OVERLAPPEDWINDOW | WS_VISIBLE,
342            None,
343            Some((POINT { x: 100, y: 100 }, SIZE { cx: 500, cy: 500 })),
344            Some(PCWSTR(HSTRING::from("Test Window").as_ptr())),
345            None,
346        )?;
347
348        *counter.borrow_mut() += 1;
349
350        msg_loop::run()?;
351
352        Ok(())
353    }
354}