Im trying get the path to the files which are selected in the currently active explorer window. Similar to this example in c# or this example in python. In the internet I found out, that you can use windows-rs to do things with win32.
But I only manage to write following code:
use windows::Win32::UI::WindowsAndMessaging::*;
use std::{thread, time, str};
fn main() {
// wait to let me select the explorer window
let ten_millis = time::Duration::from_millis(2000);
thread::sleep(ten_millis);
// get foreground window and check if it is an explorer window
let handle = unsafe { GetForegroundWindow() };
let buff = &mut [0; 100];
unsafe { GetWindowModuleFileNameA(handle, buff)};
let encoded = str::from_utf8_mut(buff).unwrap();
let path = encoded.replace("\0", "");
println!("{:?}", path);
if path == "C:\\Windows\\explorer.exe" {
// get folders?
}
}
With this code I still have the problem that I only get the path from the rust exe while all other programs output an empty buffer.
I also couldn't get any further with it. I found IShellFolderViewDual2 in the docs but haven't found a way to use it with the hwnd of the active window.
Little known, Explorer (both Internet Explorer and File Explorer) in Windows have a common programming interface that lets you access all sorts of information, including the currently viewed folder location (in case of File Explorer).
While COM is a fairly awesome technology, its documentation is generally not. You'll find all sorts of interfaces documented, in isolation, but there is little to no conceptual coverage. This leads to a situation, where navigating interface hierarchies is like a walk in a maze. You won't get far without a scout. Luckily, Raymond Chen knows a fair bit of the Shell's internals, and frequently shares this information1.
The following is a rough translation of part 1 into Rust using the windows
crate. The code is using version 0.39.0, and while the library is still evolving some things may need to be changed in future versions.
The innermost function takes an IShellBrowser
interface and extracts the currently selected folder:
fn get_location_from_view(browser: &IShellBrowser) -> Result<Vec<u16>> {
let shell_view = unsafe { browser.QueryActiveShellView() }?;
let persist_id_list: IPersistIDList = shell_view.cast()?;
let id_list = unsafe { persist_id_list.GetIDList() }?;
let mut item = MaybeUninit::<IShellItem>::uninit();
unsafe { SHCreateItemFromIDList(id_list, &IShellItem::IID, addr_of_mut!(item) as _) }?;
let item = unsafe { item.assume_init() };
let ptr = unsafe { item.GetDisplayName(SIGDN_DESKTOPABSOLUTEPARSING) }?;
// Copy UTF-16 string to `Vec<u16>` (including NUL terminator)
let mut path = Vec::new();
let mut p = ptr.0 as *const u16;
loop {
let ch = unsafe { *p };
path.push(ch);
if ch == 0 {
break;
}
p = unsafe { p.add(1) };
}
// Cleanup
unsafe { CoTaskMemFree(ptr.0 as _) };
unsafe { CoTaskMemFree(id_list as _) };
Ok(path)
}
There's a lot of magic going on here. I can't explain why that is how it needs to be done, but I can at least comment on some of the rough edges of the Rust code:
While I'm not aware of a way to create an IShellItem
interface initialized from a null pointer, I went on to declare it as MaybeUninit<IShellItem>
instead, and used addr_of_mut!
to pass its address to the C function SHCreateItemFromIDList
. When that returns successfully, item
can be assumed initialized (following core COM rules).
There's a slight window where the function can leak the memory allocated from GetIDList
and GetDisplayName
. The solution would be to wrap those pointers inside #[repr(transparent)]
tuple-like structs with a Drop
implementation that calls CoTaskMemFree
. I might revisit this to make it not leak (though leaking resources isn't considered a safety issue in Rust).
The next piece of code takes an IDispatch
interface representing an Explorer instance, and attempts to get the specific interface that represents the File Explorer's address bar:
fn get_browser_info<'a, P>(unk: P, hwnd: &mut HWND) -> Result<Vec<u16>>
where
P: Into<InParam<'a, IUnknown>>,
{
let shell_browser: IShellBrowser =
unsafe { IUnknown_QueryService(unk, &SID_STopLevelBrowser) }?;
*hwnd = unsafe { shell_browser.GetWindow() }?;
get_location_from_view(&shell_browser)
}
The generic type P
is just a convenient way to have the IDispatch
interface automatically converted into an IUnknown
as required by IUnknown_QueryService
. This function also queries for the native HWND
that's backing the specific Explorer instance. If you want to filter you could compare it to the return value of GetForegroundWindow
, for example.
The following part is the main driver:
fn dump_windows(windows: &IShellWindows) -> Result<()> {
let unk_enum = unsafe { windows._NewEnum() }?;
let enum_variant = unk_enum.cast::<IEnumVARIANT>()?;
loop {
let mut fetched = 0;
let mut var: [VARIANT; 1] = [VARIANT::default(); 1];
let hr = unsafe { enum_variant.Next(&mut var, &mut fetched) };
// No more windows?
if hr == S_FALSE || fetched == 0 {
break;
}
// Not an IDispatch interface?
if unsafe { var[0].Anonymous.Anonymous.vt } != VT_DISPATCH.0 as _ {
continue;
}
// Get the information
let mut hwnd = Default::default();
let location = get_browser_info(
unsafe {
var[0]
.Anonymous
.Anonymous
.Anonymous
.pdispVal
.as_ref()
.unwrap()
},
&mut hwnd,
)?;
// Convert UTF-16 to UTF-8 for display
let location = String::from_utf16_lossy(&location);
println!("Explorer location: \"{}\"", location);
}
Ok(())
}
This is taking an IShellWindows
interface, and iterating over the list of instances, one item at a time. If it finds an entry that holds an IDispatch
interface, it passes it on to get_browser_info
(see above).
VARIANT
s are pretty complex types, and the windows
crate represents them very accurately. The resulting code is pretty unwieldy, though I suppose this is a point where the windows
crate sees room for improvements. I'm also pretty sure that the code is leaking the interface borrowed from pdispVal
, though I haven't had time yet to look into this in detail. Leaking interfaces is more serious than leaking, for example, memory, so this is something I will have to come back to later.
Finally, the main entry point:
fn main() -> Result<()> {
unsafe {
CoInitializeEx(
ptr::null(),
COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE,
)
}?;
let windows: IShellWindows =
unsafe { CoCreateInstance(&ShellWindows, None, CLSCTX_LOCAL_SERVER) }?;
dump_windows(&windows)?;
Ok(())
}
This initializes the thread into the STA, which is required for the remaining code. Another detail worth pointing out: CoCreateInstance
needs to instantiate the ShellWindows
object into the CLSCTX_LOCAL_SERVER
; if you instead passed CLSCTX_INPROC_SERVER
, the call would fail with REGDB_E_CLASSNOTREG
.
The remainder is just your usual boilerplate. That includes both the imports as well as the Cargo.toml file. If you cannot import a particular type, you're probably missing the respective feature in the Cargo.toml file.
use std::{
mem::MaybeUninit,
ptr::{self, addr_of_mut},
};
use windows::{
core::{IUnknown, InParam, Interface, Result},
Win32::{
Foundation::{HWND, S_FALSE},
System::{
Com::{
CoCreateInstance, CoInitializeEx, CoTaskMemFree, CLSCTX_LOCAL_SERVER,
COINIT_APARTMENTTHREADED, COINIT_DISABLE_OLE1DDE, VARIANT,
},
Ole::{IEnumVARIANT, VT_DISPATCH},
},
UI::Shell::{
IPersistIDList, IShellBrowser, IShellItem, IShellWindows, IUnknown_QueryService,
SHCreateItemFromIDList, SID_STopLevelBrowser, ShellWindows,
SIGDN_DESKTOPABSOLUTEPARSING,
},
},
};
[package]
name = "explorer_selection"
version = "0.0.0"
edition = "2021"
[dependencies.windows]
version = "0.39.0"
features = [
"Win32_Foundation",
"Win32_System_Com",
"Win32_System_Ole",
"Win32_UI_Shell",
"Win32_UI_Shell_Common",
]
1 References: