Skip to main content

windows_capture/
window.rs

1//! Utilities for querying and working with top-level windows.
2//!
3//! Provides [`Window`] for finding and inspecting windows (title, process),
4//! testing capture suitability, enumerating capturable windows, and converting
5//! a window into a graphics capture item.
6//!
7//! Common tasks include:
8//! - Getting the foreground window via [`Window::foreground`].
9//! - Finding by exact title via [`Window::from_name`].
10//! - Finding by substring via [`Window::from_contains_name`].
11//! - Enumerating capturable windows via [`Window::enumerate`].
12//! - Getting the owning process name via [`Window::process_name`].
13//! - Computing the title bar height via [`Window::title_bar_height`].
14//!
15//! To acquire a [`crate::GraphicsCaptureItem`] for a window, use
16//! [`crate::settings::TryIntoCaptureItemWithDetails`] for [`Window`].
17use std::ptr;
18
19use windows::Graphics::Capture::GraphicsCaptureItem;
20use windows::Win32::Foundation::{GetLastError, HWND, LPARAM, RECT, TRUE};
21use windows::Win32::Graphics::Dwm::{DWMWA_EXTENDED_FRAME_BOUNDS, DwmGetWindowAttribute};
22use windows::Win32::Graphics::Gdi::{MONITOR_DEFAULTTONULL, MonitorFromWindow};
23use windows::Win32::System::ProcessStatus::GetModuleBaseNameW;
24use windows::Win32::System::Threading::{GetCurrentProcessId, OpenProcess, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ};
25use windows::Win32::System::WinRT::Graphics::Capture::IGraphicsCaptureItemInterop;
26use windows::Win32::UI::HiDpi::GetDpiForWindow;
27use windows::Win32::UI::WindowsAndMessaging::{
28    EnumChildWindows, FindWindowW, GWL_EXSTYLE, GWL_STYLE, GetClientRect, GetDesktopWindow, GetForegroundWindow,
29    GetWindowLongPtrW, GetWindowRect, GetWindowTextLengthW, GetWindowTextW, GetWindowThreadProcessId, IsWindowVisible,
30    WS_CHILD, WS_EX_TOOLWINDOW,
31};
32use windows::core::{BOOL, HSTRING, Owned};
33
34use crate::monitor::Monitor;
35use crate::settings::GraphicsCaptureItemType;
36
37#[derive(thiserror::Error, Eq, PartialEq, Clone, Debug)]
38/// Errors that can occur when querying or manipulating top-level windows via [`Window`].
39pub enum Error {
40    /// There is no foreground window at the time of the call.
41    ///
42    /// Returned by [`Window::foreground`].
43    #[error("No active window found.")]
44    NoActiveWindow,
45    /// No window matched the provided title or substring.
46    ///
47    /// Returned by [`Window::from_name`] and [`Window::from_contains_name`].
48    #[error("Failed to find a window with the name: {0}")]
49    NotFound(String),
50    /// Converting a UTF-16 Windows string to `String` failed.
51    #[error("Failed to convert a Windows string from UTF-16")]
52    FailedToConvertWindowsString,
53    /// A Windows API call returned an error.
54    ///
55    /// Wraps [`windows::core::Error`].
56    #[error("A Windows API call failed: {0}")]
57    WindowsError(#[from] windows::core::Error),
58}
59
60/// Represents a window that can be captured.
61///
62/// # Example
63/// ```no_run
64/// use windows_capture::window::Window;
65///
66/// fn main() -> Result<(), Box<dyn std::error::Error>> {
67///     let window = Window::foreground()?;
68///     println!("Foreground window title: {}", window.title()?);
69///
70///     Ok(())
71/// }
72/// ```
73#[derive(Eq, PartialEq, Clone, Copy, Debug)]
74pub struct Window {
75    window: HWND,
76}
77
78unsafe impl Send for Window {}
79
80impl Window {
81    /// Returns the window that is currently in the foreground.
82    ///
83    /// # Errors
84    ///
85    /// - [`Error::NoActiveWindow`] when there is no foreground window
86    #[inline]
87    pub fn foreground() -> Result<Self, Error> {
88        let window = unsafe { GetForegroundWindow() };
89
90        if window.is_invalid() {
91            return Err(Error::NoActiveWindow);
92        }
93
94        Ok(Self { window })
95    }
96
97    /// Finds a window by its exact title.
98    ///
99    /// # Errors
100    ///
101    /// - [`Error::WindowsError`] when the underlying `FindWindowW` call fails
102    /// - [`Error::NotFound`] when no window with the specified title is found
103    #[inline]
104    pub fn from_name(title: &str) -> Result<Self, Error> {
105        let hstring_title = HSTRING::from(title);
106        let window = unsafe { FindWindowW(None, &hstring_title)? };
107
108        if window.is_invalid() {
109            return Err(Error::NotFound(String::from(title)));
110        }
111
112        Ok(Self { window })
113    }
114
115    /// Finds a window whose title contains the given substring.
116    ///
117    /// # Errors
118    ///
119    /// - [`Error::WindowsError`] when enumerating windows fails
120    /// - [`Error::FailedToConvertWindowsString`] when converting a window title from UTF-16 fails
121    /// - [`Error::NotFound`] when no window title contains the specified substring
122    #[inline]
123    pub fn from_contains_name(title: &str) -> Result<Self, Error> {
124        let windows = Self::enumerate()?;
125
126        let mut target_window = None;
127        for window in windows {
128            if window.title()?.contains(title) {
129                target_window = Some(window);
130                break;
131            }
132        }
133
134        target_window.map_or_else(|| Err(Error::NotFound(String::from(title))), Ok)
135    }
136
137    /// Returns the title of the window.
138    ///
139    /// # Errors
140    ///
141    /// - [`Error::FailedToConvertWindowsString`] when converting the window title from UTF-16 fails
142    #[inline]
143    pub fn title(&self) -> Result<String, Error> {
144        let len = unsafe { GetWindowTextLengthW(self.window) };
145
146        if len == 0 {
147            return Ok(String::new());
148        }
149
150        let mut buf = vec![0u16; usize::try_from(len).unwrap() + 1];
151        let copied = unsafe { GetWindowTextW(self.window, &mut buf) };
152        if copied == 0 {
153            return Ok(String::new());
154        }
155
156        let name = String::from_utf16(&buf[..copied as usize]).map_err(|_| Error::FailedToConvertWindowsString)?;
157
158        Ok(name)
159    }
160
161    /// Returns the process ID of the window.
162    ///
163    /// # Errors
164    ///
165    /// - [`Error::WindowsError`] when `GetWindowThreadProcessId` reports an error
166    #[inline]
167    pub fn process_id(&self) -> Result<u32, Error> {
168        let mut id = 0;
169        unsafe { GetWindowThreadProcessId(self.window, Some(&mut id)) };
170
171        if id == 0 {
172            return Err(Error::WindowsError(unsafe { GetLastError().into() }));
173        }
174
175        Ok(id)
176    }
177
178    /// Returns the name of the process that owns the window.
179    ///
180    /// This function requires the `PROCESS_QUERY_INFORMATION` and `PROCESS_VM_READ` permissions.
181    ///
182    /// # Errors
183    ///
184    /// - [`Error::WindowsError`] when opening the process or querying its base module name fails
185    /// - [`Error::FailedToConvertWindowsString`] when converting the process name from UTF-16 fails
186    #[inline]
187    pub fn process_name(&self) -> Result<String, Error> {
188        let id = self.process_id()?;
189
190        let process = unsafe { OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, id) }?;
191        let process = unsafe { Owned::new(process) };
192
193        let mut name = vec![0u16; 260];
194        let size = unsafe { GetModuleBaseNameW(*process, None, &mut name) };
195
196        if size == 0 {
197            return Err(Error::WindowsError(unsafe { GetLastError().into() }));
198        }
199
200        let name =
201            String::from_utf16(&name.as_slice().iter().take_while(|ch| **ch != 0x0000).copied().collect::<Vec<u16>>())
202                .map_err(|_| Error::FailedToConvertWindowsString)?;
203
204        Ok(name)
205    }
206
207    /// Returns the monitor that has the largest area of intersection with the window.
208    ///
209    /// Returns `None` if the window does not intersect with any monitor.
210    #[inline]
211    #[must_use]
212    pub fn monitor(&self) -> Option<Monitor> {
213        let window = self.window;
214
215        let monitor = unsafe { MonitorFromWindow(window, MONITOR_DEFAULTTONULL) };
216
217        if monitor.is_invalid() { None } else { Some(Monitor::from_raw_hmonitor(monitor.0)) }
218    }
219
220    /// Returns the bounding rectangle of the window in screen coordinates.
221    ///
222    /// # Errors
223    ///
224    /// - [`Error::WindowsError`] when `GetWindowRect` fails
225    #[inline]
226    pub fn rect(&self) -> Result<RECT, Error> {
227        let mut rect = RECT::default();
228        let result = unsafe { GetWindowRect(self.window, &mut rect) };
229        if result.is_ok() { Ok(rect) } else { Err(Error::WindowsError(unsafe { GetLastError().into() })) }
230    }
231
232    /// Calculates the height of the window's title bar in pixels.
233    ///
234    /// # Errors
235    ///
236    /// - [`Error::WindowsError`] when `DwmGetWindowAttribute` or `GetClientRect` fails
237    #[inline]
238    pub fn title_bar_height(&self) -> Result<u32, Error> {
239        let mut window_rect = RECT::default();
240        let mut client_rect = RECT::default();
241
242        unsafe {
243            DwmGetWindowAttribute(
244                self.window,
245                DWMWA_EXTENDED_FRAME_BOUNDS,
246                &mut window_rect as *mut RECT as *mut std::ffi::c_void,
247                std::mem::size_of::<RECT>() as u32,
248            )
249        }?;
250
251        unsafe { GetClientRect(self.window, &mut client_rect) }?;
252
253        let window_height = window_rect.bottom - window_rect.top;
254        let dpi = unsafe { GetDpiForWindow(self.window) };
255        let client_height = (client_rect.bottom - client_rect.top) * dpi as i32 / 96;
256        let actual_title_height = (window_height - client_height) as i32;
257
258        Ok(actual_title_height as u32)
259    }
260
261    /// Checks whether the window is a valid target for capture.
262    ///
263    /// # Returns
264    ///
265    /// Returns `true` if the window is visible, not a tool window, and not a child window.
266    /// Returns `false` otherwise.
267    #[inline]
268    #[must_use]
269    pub fn is_valid(&self) -> bool {
270        if !unsafe { IsWindowVisible(self.window).as_bool() } {
271            return false;
272        }
273
274        let mut id = 0;
275        unsafe { GetWindowThreadProcessId(self.window, Some(&mut id)) };
276        if id == unsafe { GetCurrentProcessId() } {
277            return false;
278        }
279
280        let mut rect = RECT::default();
281        let result = unsafe { GetClientRect(self.window, &mut rect) };
282        if result.is_ok() {
283            #[cfg(target_pointer_width = "64")]
284            let styles = unsafe { GetWindowLongPtrW(self.window, GWL_STYLE) };
285            #[cfg(target_pointer_width = "64")]
286            let ex_styles = unsafe { GetWindowLongPtrW(self.window, GWL_EXSTYLE) };
287
288            #[cfg(target_pointer_width = "32")]
289            let styles = unsafe { GetWindowLongPtrW(self.window, GWL_STYLE) as isize };
290            #[cfg(target_pointer_width = "32")]
291            let ex_styles = unsafe { GetWindowLongPtrW(self.window, GWL_EXSTYLE) as isize };
292
293            if (ex_styles & isize::try_from(WS_EX_TOOLWINDOW.0).unwrap()) != 0 {
294                return false;
295            }
296            if (styles & isize::try_from(WS_CHILD.0).unwrap()) != 0 {
297                return false;
298            }
299        } else {
300            return false;
301        }
302
303        true
304    }
305
306    /// Returns a list of all capturable windows.
307    ///
308    /// # Errors
309    ///
310    /// - [`Error::WindowsError`] when `EnumChildWindows` fails
311    #[inline]
312    pub fn enumerate() -> Result<Vec<Self>, Error> {
313        let mut windows: Vec<Self> = Vec::new();
314
315        unsafe {
316            EnumChildWindows(
317                Some(GetDesktopWindow()),
318                Some(Self::enum_windows_callback),
319                LPARAM(ptr::addr_of_mut!(windows) as isize),
320            )
321            .ok()?;
322        };
323
324        Ok(windows)
325    }
326
327    /// Returns the width of the window in pixels.
328    ///
329    /// # Errors
330    ///
331    /// - [`Error::WindowsError`] when retrieving the window rectangle fails
332    pub fn width(&self) -> Result<i32, Error> {
333        let rect = self.rect()?;
334        Ok(rect.right - rect.left)
335    }
336
337    /// Returns the height of the window in pixels.
338    ///
339    /// # Errors
340    ///
341    /// - [`Error::WindowsError`] when retrieving the window rectangle fails
342    pub fn height(&self) -> Result<i32, Error> {
343        let rect = self.rect()?;
344        Ok(rect.bottom - rect.top)
345    }
346
347    /// Constructs a `Window` instance from a raw `HWND` handle.
348    #[inline]
349    #[must_use]
350    pub const fn from_raw_hwnd(hwnd: *mut std::ffi::c_void) -> Self {
351        Self { window: HWND(hwnd) }
352    }
353
354    /// Returns the raw `HWND` handle of the window.
355    #[inline]
356    #[must_use]
357    pub const fn as_raw_hwnd(&self) -> *mut std::ffi::c_void {
358        self.window.0
359    }
360
361    // Callback used for enumerating all valid windows.
362    #[inline]
363    unsafe extern "system" fn enum_windows_callback(window: HWND, vec: LPARAM) -> BOOL {
364        let windows = unsafe { &mut *(vec.0 as *mut Vec<Self>) };
365
366        if Self::from_raw_hwnd(window.0).is_valid() {
367            windows.push(Self { window });
368        }
369
370        TRUE
371    }
372}
373
374impl TryInto<GraphicsCaptureItemType> for Window {
375    type Error = windows::core::Error;
376
377    #[inline]
378    fn try_into(self) -> Result<GraphicsCaptureItemType, Self::Error> {
379        let window = HWND(self.as_raw_hwnd());
380
381        let interop = windows::core::factory::<GraphicsCaptureItem, IGraphicsCaptureItemInterop>()?;
382        let item = unsafe { interop.CreateForWindow(window)? };
383
384        Ok(GraphicsCaptureItemType::Window((item, self)))
385    }
386}