Search code examples
rustwindows-rs

Get path to selected files in active explorer window


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.


Solution

  • 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).

    VARIANTs 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: