windows_capture/
window.rs

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