2

I'm trying to get a list of all "File Explorer" instances currently running. It's fairly straight forward getting a list that includes all instances, but I find myself running into a brick wall filtering that list to only "File Explorer" instances.

The following piece of code retrieves all Explorer instances, meaning both "File Explorer" and "Internet Explorer":

#include <comdef.h>
#include <ExDisp.h>
#include <ShlGuid.h>
#include <Windows.h>

#include <cstdio>

using _com_util::CheckError;
using std::puts;

_COM_SMARTPTR_TYPEDEF(IShellWindows, __uuidof(IShellWindows));

int main()
{
    CheckError(::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE));

    // Acquire IShellWindows interface
    IShellWindowsPtr spShellWindows{};
    CheckError(spShellWindows.CreateInstance(CLSID_ShellWindows, nullptr, CLSCTX_LOCAL_SERVER));

    // Request iterator
    IUnknownPtr spEnum{};
    CheckError(spShellWindows->_NewEnum(&spEnum));
    IEnumVARIANTPtr spEnumVariant{};
    CheckError(spEnum.QueryInterface(__uuidof(spEnumVariant), &spEnumVariant));

    // Iterate over shell windows ...
    while (true) {
        variant_t var{};
        // ... one element at a time
        HRESULT hr = spEnumVariant->Next(1, &var, nullptr);
        CheckError(hr);

        // Iterator depleted?
        if (hr == S_FALSE) break;

        // Did we get the expected `IDispatch` interface?
        if (var.vt != VT_DISPATCH) continue;

        IDispatchPtr spDisp{};
        spDisp.Attach(var.pdispVal, true);

        puts("Got suspect; need ID");

        // That was easy; now on to some real challenges
    }
}

The obvious attempt

My first take at the problem was to just get rid of everything that isn't "File Explorer". Asking for the IWebBrowser2 interface would certainly only get an affirmative response from objects that actually are web browsers. Adding the following to the code above:

_COM_SMARTPTR_TYPEDEF(IWebBrowser2, __uuidof(IWebBrowser2));

// ...

int main()
{
    // ...

        IWebBrowser2Ptr spWebBrowser{};
        hr = spDisp.QueryInterface(__uuidof(spWebBrowser), &spWebBrowser);
        if (SUCCEEDED(hr)) puts("Implements IWebBrowser2");

    // ...

After making the changes and running the code while an "Internet Explorer" instance is running produces the desired output. However, running the code while a "File Explorer" instance is running produces the same output! That's a surprise and a disappointment, all at the same time.

More robust, less useful

Exluding objects that can be identified as "not File Explorer" didn't work out. Let's try to only include objects that can be identified as "File Explorer" instead. That sounds even more obvious, but as we've learned, "obvious" and "not" go hand in hand when it comes to the Windows Shell.

I haven't actually implemented this, but the IShellWindows interface provides an Item method that can return only objects that match a particular ShellWindowTypeConstants (e.g. SWC_EXPLORER or SWC_BROWSER). Or return an object at a particular index in the window collection. But not BOTH!

So, yes, (potentially) more robust, but also less useful as it doesn't meet my requirements as soon as more than one instances of "File Explorer" are running. Bummer.

Circumstantial evidence

While neither of the above led anywhere, I started over and went on a full-blown investigation looking for hints. Since "File Explorer" browses the Shell namespace, there may be something to that account. The following outlines the approach, based on an article by Raymond Chen titled A big little program: Monitoring Internet Explorer and Explorer windows, part 1: Enumeration:

  1. Starting from the IDispatch interface above, ask for a service with ID SID_STopLevelBrowser to get an IShellBrowser interface.
  2. Call IShellBrowser::QueryActiveShellView to get an IShellView interface.
  3. Ask the IShellView whether it implements something Shell-namespace-y, e.g. IPersistIDList.
  4. If it does, conclude that we're holding a reference to a "File Explorer" instance.

This appears to produce the desired result, though it's not clear to me future-proof this is or when it stops working. Leaving aside how overly convoluted this appears, I'm concerned about its reliability.

Question

What's the recommended/robust/reliable way to identify all "File Explorer" instances in an IShellWindows collection? I will favor solutions based on official documentation, though I understand that this is the Windows Shell and there's next to no documentation at all.

IInspectable
  • 46,945
  • 8
  • 85
  • 181
  • 2
    I don't think there's any official way. There are many unofficial. Querying for a location is a good way. You can also ask IWebBrowser2 for Path or FullName properties. You can also QueryInterface on IDispatch for IServiceProvider and QueryService on this for INamespaceTreeControl SID with INamespaceTreeControl IID. Only Explorer has a namespace tree control. – Simon Mourier Aug 19 '22 at 14:10
  • Remember that in 98/2000/XP there is basically no difference between IE and Explorer, both can display the file list and web pages. And what about 3rd-party file managers? Which feature of Explorer are you actually interested in? – Anders Aug 19 '22 at 14:36
  • @sid That's unfortunate. Querying for the `INamespaceTreeControl` service seems more direct than navigating all the way to a `PIDL`. Do you know the exact name of the `SID` constant that returns an `INamespaceTreeControl`? Also, do you know of a list that maps `SID`s to `IID`s? – IInspectable Aug 19 '22 at 15:43
  • @and I don't care about anything that's not the Shell. – IInspectable Aug 19 '22 at 15:44
  • 1
    You can just use `__uuidof(INamespaceTreeControl)` for both the SID and the IID. There's not one unique list of SIDs. Here is a raw list but SIDs an be IIDs too `https://www.magnumdb.com/search?q=SID_*+AND+type%3Dguid` – Simon Mourier Aug 19 '22 at 15:56
  • @sid `hr = IUnknown_QueryService(spDisp, spNSTreeCtrl.GetIID(), IID_PPV_ARGS(&spNSTreeCtrl));` does indeed return a success code for "File Explorer" and fails for "Internet Explorer". I'll probably go with that unless I find a better way. – IInspectable Aug 19 '22 at 17:13

1 Answers1

0

Here's a possibility ...

#include <comdef.h>
#include <ExDisp.h>
#include <ShlGuid.h>
#include <Windows.h>

#include <cstdio>
#include <atlbase.h>
#include <string>

using _com_util::CheckError;
using std::puts;
using std::string;

_COM_SMARTPTR_TYPEDEF(IShellWindows, __uuidof(IShellWindows));

//nicked from StackOverflow
std::string ProcessIdToName(DWORD_PTR processId)
{
    std::string ret;
    HANDLE handle = OpenProcess(
        PROCESS_QUERY_LIMITED_INFORMATION,
        FALSE,
        processId /* This is the PID, you can find one from windows task manager */
    );
    if (handle)
    {
        DWORD buffSize = 1024;
        CHAR buffer[1024];
        if (QueryFullProcessImageNameA(handle, 0, buffer, &buffSize))
        {
            ret = buffer;
        }
        else
        {
            printf("Error GetModuleBaseNameA : %lu", GetLastError());
        }
        CloseHandle(handle);
    }
    else
    {
        printf("Error OpenProcess : %lu", GetLastError());
    }
    return ret;
}

int main()
{
    CheckError(::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE));

    // Acquire IShellWindows interface
    IShellWindowsPtr spShellWindows{};
    CheckError(spShellWindows.CreateInstance(CLSID_ShellWindows, nullptr, CLSCTX_LOCAL_SERVER));

    // Request iterator
    IUnknownPtr spEnum{};
    CheckError(spShellWindows->_NewEnum(&spEnum));
    IEnumVARIANTPtr spEnumVariant{};
    CheckError(spEnum.QueryInterface(__uuidof(spEnumVariant), &spEnumVariant));

    // Iterate over shell windows ...
    while (true) {
        variant_t var{};
        // ... one element at a time
        HRESULT hr = spEnumVariant->Next(1, &var, nullptr);
        CheckError(hr);

        // Iterator depleted?
        if (hr == S_FALSE) break;

        // Did we get the expected `IDispatch` interface?
        if (var.vt != VT_DISPATCH) continue;

        IDispatchPtr spDisp{};
        spDisp.Attach(var.pdispVal, true);

        puts("Got suspect; need ID");

        // That was easy; now on to some real challenges

        CComPtr<IWebBrowser2> lpWB;
        spDisp->QueryInterface(&lpWB);
        SHANDLE_PTR hWnd{ 0 };
        lpWB->get_HWND(&hWnd);

        if (hWnd)
        {
            DWORD pid = 0;
            GetWindowThreadProcessId((HWND)hWnd, &pid);

            if (pid != 0)
            {
                puts("pid");
                auto s = ProcessIdToName((DWORD_PTR)pid);
                puts(s.c_str());
            }
        }
    }
}
Joseph Willcoxson
  • 5,853
  • 1
  • 15
  • 29
  • How would I get, say, an [`IShellFolderViewDual`](https://learn.microsoft.com/en-us/windows/win32/api/shldisp/nn-shldisp-ishellfolderviewdual) out of a process ID? – IInspectable Aug 19 '22 at 22:35
  • IDK. Does the shell window have some property where you can get the interface from the HWND ? Do the windows have a special class or a hidden name that you can enumerate windows in the process... IDK. – Joseph Willcoxson Aug 19 '22 at 22:41
  • The rich edit control has a message ( EM_GETOLEINTERFACE ) that can get an interface from an HWND to a rich edit. IDK if the shell windows have something similar. If they do, you could iterate windows in a process and go until you hit one... use Spy++ to see what special properties/classes/names the window you want might have. Just spitballing... – Joseph Willcoxson Aug 19 '22 at 22:51
  • The shell has a message to get an interface from a HWND but it's undocumented and useless cross-process. – Anders Aug 20 '22 at 10:27