動機
この記事を見て、「WinAPI で実現できそう」と思ったので調べてみました。
環境
Windows 11 25H2
rustc 1.92.0
windows crate 0.62.2
実装
エクスプローラのタブを開くのはShellTabWindowClassに0xA21Bを送ればよいらしい。この方法は Microsoft のドキュメントにはない非公式な方法なので、今後変更される可能性があります。↓参考。
開いたタブでフォルダを読み込むにはIWebBrowser2::Navigate2関数を呼べばいいとのこと。↓参考。
上記参考コードを Rust に写経します。短いので全部載せます。
Cargo.toml
[package]
name = "restore_tab"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1.0"
[dependencies.windows]
version = "0.62"
features = [
"Win32_UI_Shell",
"Win32_UI_Shell_Common",
"Win32_UI_WindowsAndMessaging",
"Win32_System_Com",
"Win32_System_Com_StructuredStorage",
"Win32_System_Ole",
"Win32_System_Variant",
]
src/main.rs
use anyhow::{Result, anyhow};
use std::{
cell::Cell,
char::{REPLACEMENT_CHARACTER, decode_utf16},
thread,
time::Duration,
};
use windows::{
Win32::{
Foundation::{DISP_E_PARAMNOTFOUND, HWND, LPARAM, WPARAM},
System::{
Com::{CLSCTX_ALL, CoCreateInstance, CoInitialize, CoUninitialize},
Variant::{InitVariantFromBuffer, VARIANT, VT_ERROR},
},
UI::{
Shell::{ILCreateFromPathW, ILGetSize, IShellWindows, IWebBrowser2},
WindowsAndMessaging::{
EnumChildWindows, FindWindowW, GetClassNameW, SendMessageW, WM_COMMAND,
},
},
},
core::{BOOL, GUID, HSTRING, Interface, w},
};
// ExDisp.Idl
const CLSID_SHELL_WINDOWS: GUID = GUID {
data1: 0x9BA05972,
data2: 0xF6A8,
data3: 0x11CF,
data4: [0xA4, 0x42, 0x00, 0xA0, 0xC9, 0x0A, 0x8F, 0x39],
};
// ShellTabWindowClass のウィンドウハンドルを保持しておくためのグローバル変数
thread_local! {
static TAB_HWND: Cell<Option<HWND>> = const { Cell::new(None) };
}
// COM を初期化・終了するための構造体
struct Com;
impl Com {
fn new() -> Result<Self> {
unsafe { CoInitialize(None).ok()? };
Ok(Self)
}
}
impl Drop for Com {
fn drop(&mut self) {
unsafe { CoUninitialize() };
}
}
// EnumChildWindows に渡すコールバック関数
unsafe extern "system" fn enum_proc(hwnd: HWND, _: LPARAM) -> BOOL {
let mut buf = [0; 256];
unsafe { GetClassNameW(hwnd, &mut buf) };
// Windows の文字列を Rust の文字列に変換
let class_name = decode_utf16(buf.into_iter().take_while(|c| c.ne(&0)))
.map(|r| r.unwrap_or(REPLACEMENT_CHARACTER))
.collect::<String>();
// クラス名が ShellTabWindowClass だったらグローバル変数にウィンドウハンドルを保存
if class_name.eq("ShellTabWindowClass") {
TAB_HWND.set(Some(hwnd));
return false.into();
}
true.into()
}
// 引数に渡されたパスを新しいタブで開く関数。エクスプローラが起動してないと失敗します。
fn restore_tab(path: &[&str]) -> Result<()> {
// エクスプローラの親ウィンドウを探す
let hwnd = unsafe { FindWindowW(w!("CabinetWClass"), None)? };
// ShellTabWindowClass を探す
_ = unsafe { EnumChildWindows(Some(hwnd), Some(enum_proc), LPARAM::default()) };
let Some(hwnd) = TAB_HWND.get() else {
return Err(anyhow!("failed to retrieve shell tab window class"));
};
// COM の初期化
let _com = Com::new()?;
// IShellWindows の取得
let windows: IShellWindows =
unsafe { CoCreateInstance(&CLSID_SHELL_WINDOWS, None, CLSCTX_ALL)? };
for p in path {
// 新しいタブを開く
unsafe { SendMessageW(hwnd, WM_COMMAND, Some(WPARAM(0xA21B)), None) };
// タブが開くのを待つ
thread::sleep(Duration::from_secs(1));
// タブの数を取得
let count = unsafe { windows.Count() }?;
// オプションのVARIANTを構築
let mut optional: VARIANT = DISP_E_PARAMNOTFOUND.0.into();
unsafe { (*optional.Anonymous.Anonymous).vt = VT_ERROR };
// 最後のタブを選択
let index: VARIANT = (count - 1).into();
let obj = unsafe { windows.Item(&index) }?;
let app: IWebBrowser2 = obj.cast()?;
let pidl = unsafe { ILCreateFromPathW(&HSTRING::from(*p)) };
let url = unsafe { InitVariantFromBuffer(pidl as _, ILGetSize(Some(pidl))) }?;
unsafe {
// パスを開く
app.Navigate2(
&url,
Some(&optional),
Some(&optional),
Some(&optional),
Some(&optional),
)?
};
}
Ok(())
}
fn main() -> Result<()> {
let path = ["C:\\", "C:\\Users", "C:\\Windows"];
restore_tab(&path)?;
Ok(())
}
まとめ
やったぜ!![]()