window_enumerator/enumerator.rs
1use std::os::windows::ffi::OsStringExt;
2use windows::core::*;
3use windows::Win32::Foundation::*;
4use windows::Win32::System::ProcessStatus::*;
5use windows::Win32::System::Threading::*;
6use windows::Win32::UI::WindowsAndMessaging::*;
7
8use crate::errors::{Result, WindowError};
9use crate::types::{FilterCriteria, WindowInfo, WindowPosition};
10use crate::utils;
11
12#[cfg(feature = "selection")]
13use crate::types::Selection;
14
15#[cfg(feature = "sorting")]
16use crate::types::SortCriteria;
17
18#[cfg(feature = "sorting")]
19use crate::models::WindowSorter;
20
21/// The main window enumeration and inspection interface.
22///
23/// This struct provides methods to discover, filter, and sort Windows windows
24/// with various criteria. It serves as the primary entry point for the library.
25pub struct WindowEnumerator {
26 windows: Vec<WindowInfo>,
27}
28
29impl WindowEnumerator {
30 /// Creates a new window enumerator.
31 ///
32 /// The enumerator starts with no windows loaded. Call [`enumerate_all_windows`]
33 /// to populate it with the current system windows.
34 ///
35 /// # Examples
36 ///
37 /// ```
38 /// use window_enumerator::WindowEnumerator;
39 ///
40 /// let enumerator = WindowEnumerator::new();
41 /// ```
42 ///
43 /// [`enumerate_all_windows`]: WindowEnumerator::enumerate_all_windows
44 pub fn new() -> Self {
45 Self {
46 windows: Vec::new(),
47 }
48 }
49
50 /// Enumerates all visible windows on the system.
51 ///
52 /// This method populates the internal window list with all currently
53 /// visible, non-child windows. Each window is assigned a 1-based index.
54 ///
55 /// # Errors
56 ///
57 /// Returns [`WindowError::WindowsApiError`] if the Windows API call fails.
58 ///
59 /// # Examples
60 ///
61 /// ```no_run
62 /// use window_enumerator::WindowEnumerator;
63 ///
64 /// let mut enumerator = WindowEnumerator::new();
65 /// enumerator.enumerate_all_windows().unwrap();
66 /// ```
67 pub fn enumerate_all_windows(&mut self) -> Result<()> {
68 self.windows.clear();
69
70 unsafe {
71 EnumWindows(
72 Some(Self::enum_windows_proc),
73 LPARAM(self as *mut _ as isize),
74 )
75 .map_err(|e| Error::new(e.code(), "Failed to enumerate windows".into()))?;
76 }
77
78 // Assign 1-based indices to each window
79 for (index, window) in self.windows.iter_mut().enumerate() {
80 window.index = index + 1;
81 }
82
83 Ok(())
84 }
85
86 /// Windows enumeration callback function.
87 unsafe extern "system" fn enum_windows_proc(hwnd: HWND, lparam: LPARAM) -> BOOL {
88 let enumerator = &mut *(lparam.0 as *mut WindowEnumerator);
89
90 // Skip invisible windows and child windows
91 if IsWindowVisible(hwnd).as_bool() && GetParent(hwnd).0 == 0 {
92 if let Ok(mut window_info) = enumerator.get_window_info(hwnd) {
93 // Temporary index, will be reassigned later
94 window_info.index = enumerator.windows.len() + 1;
95 enumerator.windows.push(window_info);
96 }
97 }
98
99 BOOL::from(true) // Continue enumeration
100 }
101
102 /// Gathers information about a specific window.
103 fn get_window_info(&self, hwnd: HWND) -> Result<WindowInfo> {
104 unsafe {
105 // Get window title
106 let title = Self::get_window_text(hwnd);
107
108 // Get window class name
109 let class_name = Self::get_class_name(hwnd);
110
111 // Get process ID
112 let pid = Self::get_process_id(hwnd);
113
114 // Get process information
115 let (process_name, process_file) = if pid > 0 {
116 Self::get_process_info(pid).unwrap_or_default()
117 } else {
118 (String::new(), std::path::PathBuf::new())
119 };
120
121 // Get window position and size
122 let position = Self::get_window_position(hwnd);
123
124 Ok(WindowInfo {
125 hwnd: hwnd.0,
126 pid,
127 title,
128 class_name,
129 process_name,
130 process_file,
131 position,
132 index: 0, // Temporary value, will be set later
133 })
134 }
135 }
136
137 /// Retrieves the text of a window.
138 unsafe fn get_window_text(hwnd: HWND) -> String {
139 let mut buffer = [0u16; 256];
140 let len = GetWindowTextW(hwnd, &mut buffer);
141 if len > 0 {
142 std::ffi::OsString::from_wide(&buffer[..len as usize])
143 .to_string_lossy()
144 .into_owned()
145 } else {
146 String::new()
147 }
148 }
149
150 /// Retrieves the class name of a window.
151 unsafe fn get_class_name(hwnd: HWND) -> String {
152 let mut buffer = [0u16; 256];
153 let len = GetClassNameW(hwnd, &mut buffer);
154 if len > 0 {
155 std::ffi::OsString::from_wide(&buffer[..len as usize])
156 .to_string_lossy()
157 .into_owned()
158 } else {
159 String::new()
160 }
161 }
162
163 /// Retrieves the process ID of a window.
164 unsafe fn get_process_id(hwnd: HWND) -> u32 {
165 let mut pid: u32 = 0;
166 GetWindowThreadProcessId(hwnd, Some(&mut pid));
167 pid
168 }
169
170 /// Retrieves the position and dimensions of a window.
171 unsafe fn get_window_position(hwnd: HWND) -> WindowPosition {
172 let mut rect = RECT::default();
173 if GetWindowRect(hwnd, &mut rect).is_ok() {
174 WindowPosition {
175 x: rect.left,
176 y: rect.top,
177 width: rect.right - rect.left,
178 height: rect.bottom - rect.top,
179 }
180 } else {
181 WindowPosition::default()
182 }
183 }
184
185 /// Retrieves process information for a given process ID.
186 unsafe fn get_process_info(pid: u32) -> Result<(String, std::path::PathBuf)> {
187 let process_handle = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid)?;
188
189 let mut file_buffer = [0u16; MAX_PATH as usize];
190 let len = GetProcessImageFileNameW(process_handle, &mut file_buffer);
191
192 if len > 0 {
193 let full_path = std::ffi::OsString::from_wide(&file_buffer[..len as usize]);
194 let path_buf = std::path::PathBuf::from(&full_path);
195
196 // Extract just the filename
197 let process_name = path_buf
198 .file_name()
199 .map(|name| name.to_string_lossy().into_owned())
200 .unwrap_or_default();
201
202 CloseHandle(process_handle).ok();
203 Ok((process_name, path_buf))
204 } else {
205 CloseHandle(process_handle).ok();
206 // 使用标准库的方法获取错误代码
207 let last_error = std::io::Error::last_os_error();
208 Err(WindowError::WindowsApiError(
209 last_error.raw_os_error().unwrap_or(0) as u32,
210 ))
211 }
212 }
213
214 /// Finds windows by title containing the specified string (case-insensitive).
215 ///
216 /// This is a convenience method for simple title-based filtering.
217 ///
218 /// # Arguments
219 ///
220 /// * `title_substring` - The substring to search for in window titles
221 ///
222 /// # Returns
223 ///
224 /// A vector containing windows whose titles contain the specified substring.
225 ///
226 /// # Examples
227 ///
228 /// ```no_run
229 /// use window_enumerator::WindowEnumerator;
230 ///
231 /// let mut enumerator = WindowEnumerator::new();
232 /// enumerator.enumerate_all_windows().unwrap();
233 ///
234 /// let chrome_windows = enumerator.find_by_title("Chrome");
235 /// for window in chrome_windows {
236 /// window.print_compact();
237 /// }
238 /// ```
239 pub fn find_by_title(&self, title_substring: &str) -> Vec<WindowInfo> {
240 let criteria = FilterCriteria {
241 title_contains: Some(title_substring.to_string()),
242 ..Default::default()
243 };
244 self.filter_windows(&criteria)
245 }
246
247 /// Filters windows based on the specified criteria.
248 ///
249 /// # Arguments
250 ///
251 /// * `criteria` - The filter criteria to apply
252 ///
253 /// # Returns
254 ///
255 /// A vector containing only the windows that match all criteria.
256 ///
257 /// # Examples
258 ///
259 /// ```no_run
260 /// use window_enumerator::{WindowEnumerator, FilterCriteria};
261 ///
262 /// let mut enumerator = WindowEnumerator::new();
263 /// enumerator.enumerate_all_windows().unwrap();
264 ///
265 /// let criteria = FilterCriteria {
266 /// title_contains: Some("Chrome".to_string()),
267 /// ..Default::default()
268 /// };
269 /// let chrome_windows = enumerator.filter_windows(&criteria);
270 /// ```
271 pub fn filter_windows(&self, criteria: &FilterCriteria) -> Vec<WindowInfo> {
272 self.windows
273 .iter()
274 .filter(|window| utils::matches_criteria(window, criteria))
275 .cloned()
276 .collect()
277 }
278
279 /// Filters and sorts windows based on the specified criteria.
280 ///
281 /// Requires the `sorting` feature.
282 ///
283 /// # Arguments
284 ///
285 /// * `criteria` - The filter criteria to apply
286 /// * `sort_criteria` - The sort criteria to apply
287 ///
288 /// # Returns
289 ///
290 /// A vector containing the filtered and sorted windows.
291 #[cfg(feature = "sorting")]
292 pub fn filter_and_sort_windows(
293 &self,
294 criteria: &FilterCriteria,
295 sort_criteria: &SortCriteria,
296 ) -> Vec<WindowInfo> {
297 WindowSorter::filter_and_sort_windows(&self.windows, criteria, sort_criteria)
298 }
299
300 /// Filters windows with selection criteria.
301 ///
302 /// Requires the `selection` feature.
303 ///
304 /// # Arguments
305 ///
306 /// * `criteria` - The filter criteria to apply
307 /// * `selection` - The selection criteria to apply
308 ///
309 /// # Returns
310 ///
311 /// A vector containing the selected windows that match the filter criteria.
312 #[cfg(feature = "selection")]
313 pub fn filter_windows_with_selection(
314 &self,
315 criteria: &FilterCriteria,
316 selection: &Selection,
317 ) -> Vec<WindowInfo> {
318 let filtered = self.filter_windows(criteria);
319
320 match selection {
321 Selection::All => filtered,
322 Selection::Indices(indices) => filtered
323 .into_iter()
324 .filter(|window| indices.contains(&window.index))
325 .collect(),
326 }
327 }
328
329 /// Filters, sorts, and selects windows based on the specified criteria.
330 ///
331 /// Requires both `sorting` and `selection` features.
332 ///
333 /// # Arguments
334 ///
335 /// * `criteria` - The filter criteria to apply
336 /// * `sort_criteria` - The sort criteria to apply
337 /// * `selection` - The selection criteria to apply
338 ///
339 /// # Returns
340 ///
341 /// A vector containing the filtered, sorted, and selected windows.
342 #[cfg(all(feature = "sorting", feature = "selection"))]
343 pub fn filter_sort_windows_with_selection(
344 &self,
345 criteria: &FilterCriteria,
346 sort_criteria: &SortCriteria,
347 selection: &Selection,
348 ) -> Vec<WindowInfo> {
349 let filtered =
350 WindowSorter::filter_and_sort_windows(&self.windows, criteria, sort_criteria);
351
352 match selection {
353 Selection::All => filtered,
354 Selection::Indices(indices) => filtered
355 .into_iter()
356 .filter(|window| indices.contains(&window.index))
357 .collect(),
358 }
359 }
360
361 /// Returns a reference to all enumerated windows.
362 ///
363 /// # Returns
364 ///
365 /// A slice containing all windows that were enumerated.
366 pub fn get_windows(&self) -> &[WindowInfo] {
367 &self.windows
368 }
369
370 /// Retrieves a window by its 1-based index.
371 ///
372 /// # Arguments
373 ///
374 /// * `index` - The 1-based index of the window to retrieve
375 ///
376 /// # Returns
377 ///
378 /// `Some(&WindowInfo)` if a window with the given index exists, `None` otherwise.
379 pub fn get_window_by_index(&self, index: usize) -> Option<&WindowInfo> {
380 self.windows.iter().find(|w| w.index == index)
381 }
382
383 /// Prints all enumerated windows with their indices in a formatted table.
384 ///
385 /// This is useful for debugging and for users to see available windows
386 /// before making selections.
387 ///
388 /// # Examples
389 ///
390 /// ```no_run
391 /// use window_enumerator::WindowEnumerator;
392 ///
393 /// let mut enumerator = WindowEnumerator::new();
394 /// enumerator.enumerate_all_windows().unwrap();
395 /// enumerator.print_windows_with_indices();
396 /// ```
397 pub fn print_windows_with_indices(&self) {
398 println!("Index | Handle | PID | Position | Title");
399 println!("------|-------------|--------|-------------|-------------------");
400 for window in &self.windows {
401 println!(
402 "{:5} | 0x{:08x} | {:6} | {:4},{:4} | {}",
403 window.index,
404 window.hwnd,
405 window.pid,
406 window.position.x,
407 window.position.y,
408 window.title
409 );
410 }
411 }
412}
413
414impl Default for WindowEnumerator {
415 fn default() -> Self {
416 Self::new()
417 }
418}