//! 快速面板:检测前台 Explorer 窗口的当前目录。 //! //! 必须在快捷键回调(`show_popup`)内调用:此时前台窗口仍是资源管理器, //! 面板尚未取得焦点,`GetForegroundWindow` 拿到的才是 Explorer 主窗口; //! 若等面板显示后再调用,前台就变成面板自身了。 //! //! 思路(Listary / PowerToys Run 同款):前台窗口 HWND 匹配 `IShellWindows` //! 中某个 Shell 窗口 → 取其 `LocationURL`(file:///...)→ 转成本地路径。 //! //! Win11 多选项卡:同一顶层窗口下每个选项卡都是独立的 `IShellWindows` 条目, //! 共享顶层 HWND。活动选项卡的内容窗口(`ShellTabWindowClass`)在子窗口 //! z-order 顶层,用 `IID_IShellBrowser` 作为 `QueryService` 的 service ID 获取 //! 每个选项卡自己的 `IShellBrowser`(而非 `SID_STopLevelBrowser` 返回的顶层 //! browser),再通过 `GetWindow()` 拿到该选项卡的内容窗口句柄,与活动选项卡 //! 的内容窗口比对,从而定位当前正在浏览的选项卡(`IsWindowVisible` 对所有 //! 选项卡都成立,不可用)。 use std::path::Path; #[cfg(windows)] use windows::core::ComInterface; #[cfg(windows)] use windows::Win32::Foundation::HWND; #[cfg(windows)] use windows::Win32::System::Com::{ CoCreateInstance, CoInitializeEx, CoUninitialize, CLSCTX_ALL, COINIT_APARTMENTTHREADED, IServiceProvider, }; #[cfg(windows)] use windows::Win32::System::Variant::{VARIANT, VT_I4}; #[cfg(windows)] use windows::Win32::UI::Shell::{ IWebBrowserApp, IShellBrowser, IShellWindows, ShellWindows, }; #[cfg(windows)] use windows::Win32::UI::WindowsAndMessaging::{ GetClassNameW, GetForegroundWindow, GetWindow, GW_CHILD, GW_HWNDNEXT, }; /// 检测前台 Explorer 窗口的当前目录。 /// 返回 `None`:前台不是 Explorer / COM 初始化失败 / URL 无法转路径。 #[cfg(windows)] pub fn detect_explorer_folder() -> Option { // 首次 COM 初始化失败(例如已在 MTA 线程)时,后续 COM 调用一般仍可用,忽略错误继续。 let _ = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) }; let fg = unsafe { GetForegroundWindow() }; let result = if fg.0 == 0 { None } else { unsafe { find_folder_for_hwnd(fg) } }; unsafe { CoUninitialize() }; result } #[cfg(windows)] unsafe fn find_folder_for_hwnd(fg: HWND) -> Option { let shell: IShellWindows = CoCreateInstance(&ShellWindows, None, CLSCTX_ALL).ok()?; let count = shell.Count().ok()?; // Win11 多选项卡:活动选项卡的内容窗口(ShellTabWindowClass)在子窗口 z-order // 顶层。在 IShellWindows 条目中,用 IShellBrowser::GetWindow() 取到的内容窗口 // HWND 与它比对,即可定位当前正在浏览的选项卡(IsWindowVisible 对所有选项卡 // 都成立,不可用)。 let active_tab = find_active_shell_tab(fg); for i in 0..count { // 索引过期的窗口会返回失败,跳过继续即可,不能 `?` 提前结束整个循环。 // 0.52 的 Win32 VARIANT 无 From,手动构造 VT_I4 变体。 let mut index = VARIANT::default(); { let value = &mut *index.Anonymous.Anonymous; value.vt = VT_I4; value.Anonymous.lVal = i; } let Ok(dispatch) = shell.Item(index) else { continue }; let Ok(app) = dispatch.cast::() else { continue }; // 只考虑前台顶层窗口对应的条目;同一窗口的多个选项卡条目共享顶层句柄。 let Ok(hwnd) = app.HWND() else { continue }; if HWND(hwnd.0) != fg { continue; } // 有选项卡时,必须匹配活动选项卡的内容窗口;否则退化为任意条目(旧版无选项卡)。 if let Some(active) = active_tab { let Ok(svc) = app.cast::() else { continue }; let Ok(browser) = svc.QueryService::(&IShellBrowser::IID) else { continue; }; let Ok(this_tab) = browser.GetWindow() else { continue }; if this_tab != active { continue; } } if let Ok(url) = app.LocationURL() { return url_to_path(&url.to_string()); } } None } /// 枚举前台窗口的子窗口(z-order 自上而下),返回第一个类名为 `ShellTabWindowClass` /// 的窗口句柄,即 Win11 资源管理器活动选项卡的内容窗口;无选项卡时返回 `None`。 #[cfg(windows)] unsafe fn find_active_shell_tab(fg: HWND) -> Option { const CLASS: &str = "ShellTabWindowClass"; let mut hwnd = GetWindow(fg, GW_CHILD); while hwnd.0 != 0 { let mut buf = [0u16; 64]; let len = GetClassNameW(hwnd, &mut buf); if len > 0 { let name = String::from_utf16_lossy(&buf[..len as usize]); if name == CLASS { return Some(hwnd); } } hwnd = GetWindow(hwnd, GW_HWNDNEXT); } None } /// 把 `file:///C:/xxx`(可能带百分号编码)转成本地路径,仅接受目录。 #[cfg(windows)] fn url_to_path(url: &str) -> Option { if !url.starts_with("file:") { return None; } let parsed = url::Url::parse(url).ok()?; let path = parsed.to_file_path().ok()?; let path = Path::new(&path); if path.is_dir() { Some(path.to_string_lossy().to_string()) } else { None } } /// 非 Windows 平台占位:保持模块可编译。 #[cfg(not(windows))] pub fn detect_explorer_folder() -> Option { None }