优化调整
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
//! 快速面板:检测前台 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<String> {
|
||||
// 首次 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<String> {
|
||||
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<i32>,手动构造 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::<IWebBrowserApp>() 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::<IServiceProvider>() else { continue };
|
||||
let Ok(browser) = svc.QueryService::<IShellBrowser>(&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<HWND> {
|
||||
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<String> {
|
||||
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<String> {
|
||||
None
|
||||
}
|
||||
Reference in New Issue
Block a user