Querying information from an Explorer window


Sometimes software development is inventing new stuff. But often, it's just putting together the stuff you already have. Today's puzzle is one of the latter type of problem.

Given a window handle, you can you determine (1) whether it is an Explorer window, and if so (2) what folder it is viewing, and (3) what item is currently focused.

This is not an inherently difficult task. You just have to put together lots of small pieces.

Start with the ShellWindows object which represents all the open shell windows. You can enumerate through them all with the Item property. This is rather clumsy from C++ because the ShellWindows object was designed for use by a scripting language like JScript or Visual Basic.

 IShellWindows *psw;
 if (SUCCEEDED(CoCreateInstance(CLSID_ShellWindows, NULL, CLSCTX_ALL,
                                IID_IShellWindows, (void**)&psw))) {
  VARIANT v;
  V_VT(&v) = VT_I4;
  IDispatch  *pdisp;
  BOOL fFound = FALSE;
  for (V_I4(&v) = 0; !fFound && psw->Item(v, &pdisp) == S_OK;
       V_I4(&v)++) {
    ...
    pdisp->Release();
  }
  psw->Release();
 }

From each item, we can ask it for its window handle and see if it's the one we want.

   IWebBrowserApp *pwba;
   if (SUCCEEDED(pdisp->QueryInterface(IID_IWebBrowserApp, (void**)&pwba))) {
     HWND hwndWBA;
     if (SUCCEEDED(pwba->get_HWND((LONG_PTR*)&hwndWBA)) &&
       hwndWBA == hwndFind) {
       fFound = TRUE;
       ...
     }
     pwba->Release();
   }

Okay, now that we have found the folder via its IWebBrowserApp, we need to get to the top shell browser. This is done by querying for the SID_STopLevelBrowser service and asking for the IShellBrowser interface.

       IServiceProvider *psp;
       if (SUCCEEDED(pwba->QueryInterface(IID_IServiceProvider, (void**)&psp))) {
         IShellBrowser *psb;
         if (SUCCEEDED(psp->QueryService(SID_STopLevelBrowser,
                              IID_IShellBrowser, (void**)&psb))) {
           ...
           psb->Release();
         }
         psp->Release();
       }

From the IShellBrowser, we can ask for the current shell view via the QueryActiveShellView method.

           IShellView *psv;
           if (SUCCEEDED(psb->QueryActiveShellView(&psv))) {
             ...
             psv->Release();
           }

Of course, what we really want is the IFolderView interface, which is the automation object that contains all the real goodies.

             IFolderView *pfv;
             if (SUCCEEDED(psv->QueryInterface(IID_IFolderView,
                                               (void**)&pfv))) {
               ...
               pfv->Release();
             }

Okay, now we're golden. What do you want to get from the view? How about the location of the IShellFolder being viewed. To do that, we need to use IPersistFolder2::GetCurFolder. The GetFolder method will give us access to the shell folder, from which we ask for IPersistFolder2. (Most of the time you want the IShellFolder interface, since that's where most of the cool stuff hangs out.)

               IPersistFolder2 *ppf2;
               if (SUCCEEDED(pfv->GetFolder(IID_IPersistFolder2,
                                            (void**)&ppf2))) {
                 LPITEMIDLIST pidlFolder;
                 if (SUCCEEDED(ppf2->GetCurFolder(&pidlFolder))) {
                   ...
                   CoTaskMemFree(pidlFolder);
                 }
                 ppf2->Release();
               }

Let's convert that pidl into a path, for display purposes.

                   if (!SHGetPathFromIDList(pidlFolder, g_szPath)) {
                     lstrcpyn(g_szPath, TEXT("<not a directory>"), MAX_PATH);
                   }
                   ...

What else can we do with what we've got? Oh right, let's see what the currently-focused object is.

                   int iFocus;
                   if (SUCCEEDED(pfv->GetFocusedItem(&iFocus))) {
                     ...
                   }

Let's display the name of the focused item. To do that we need the item's pidl and the IShellFolder. (See, I told you the IShellFolder is where the cool stuff is.) The item comes from the Item method (surprisingly enough).

                     LPITEMIDLIST pidlItem;
                     if (SUCCEEDED(pfv->Item(iFocus, &pidlItem))) {
                       ...
                       CoTaskMemFree(pidlItem);
                     }

(If we had wanted a list of selected items we could have used the Items method, passing SVGIO_SELECTION.)

After we get the item's pidl, we also need the IShellFolder:

                       IShellFolder *psf;
                       if (SUCCEEDED(ppf2->QueryInterface(IID_IShellFolder,
                                                          (void**)&psf))) {
                         ...
                         psf->Release();
                       }

Then we put the two together to get the item's display name, with the help of the GetDisplayNameOf method.

                         STRRET str;
                         if (SUCCEEDED(psf->GetDisplayNameOf(pidlItem,
                                                   SHGDN_INFOLDER,
                                                   &str))) {
                           ...
                         }

We can use the helper function StrRetToBuf to convert the kooky STRRET structure into a boring string buffer. (The history of the kooky STRRET structure will have to wait for another day.)

                           StrRetToBuf(&str, pidlItem, g_szItem, MAX_PATH);

Okay, let's put this all together. It looks rather ugly because I put everything into one huge function instead of breaking them out into subfunctions. In "real life" I would have broken things up into little helper functions to make things more manageable.

Start with the scratch program and add this new function:

#include <shlobj.h>
#include <exdisp.h>

TCHAR g_szPath[MAX_PATH];
TCHAR g_szItem[MAX_PATH];

void CALLBACK RecalcText(HWND hwnd, UINT, UINT_PTR, DWORD)
{
 HWND hwndFind = GetForegroundWindow();
 g_szPath[0] = TEXT('\0');
 g_szItem[0] = TEXT('\0');

 IShellWindows *psw;
 if (SUCCEEDED(CoCreateInstance(CLSID_ShellWindows, NULL, CLSCTX_ALL,
                                IID_IShellWindows, (void**)&psw))) {
  VARIANT v;
  V_VT(&v) = VT_I4;
  IDispatch  *pdisp;
  BOOL fFound = FALSE;
  for (V_I4(&v) = 0; !fFound && psw->Item(v, &pdisp) == S_OK;
       V_I4(&v)++) {
   IWebBrowserApp *pwba;
   if (SUCCEEDED(pdisp->QueryInterface(IID_IWebBrowserApp, (void**)&pwba))) {
     HWND hwndWBA;
     if (SUCCEEDED(pwba->get_HWND((LONG_PTR*)&hwndWBA)) &&
       hwndWBA == hwndFind) {
       fFound = TRUE;
       IServiceProvider *psp;
       if (SUCCEEDED(pwba->QueryInterface(IID_IServiceProvider, (void**)&psp))) {
         IShellBrowser *psb;
         if (SUCCEEDED(psp->QueryService(SID_STopLevelBrowser,
                              IID_IShellBrowser, (void**)&psb))) {
           IShellView *psv;
           if (SUCCEEDED(psb->QueryActiveShellView(&psv))) {
             IFolderView *pfv;
             if (SUCCEEDED(psv->QueryInterface(IID_IFolderView,
                                               (void**)&pfv))) {
               IPersistFolder2 *ppf2;
               if (SUCCEEDED(pfv->GetFolder(IID_IPersistFolder2,
                                            (void**)&ppf2))) {
                 LPITEMIDLIST pidlFolder;
                 if (SUCCEEDED(ppf2->GetCurFolder(&pidlFolder))) {
                   if (!SHGetPathFromIDList(pidlFolder, g_szPath)) {
                     lstrcpyn(g_szPath, TEXT("<not a directory>"), MAX_PATH);
                   }
                   int iFocus;
                   if (SUCCEEDED(pfv->GetFocusedItem(&iFocus))) {
                     LPITEMIDLIST pidlItem;
                     if (SUCCEEDED(pfv->Item(iFocus, &pidlItem))) {
                       IShellFolder *psf;
                       if (SUCCEEDED(ppf2->QueryInterface(IID_IShellFolder,
                                                          (void**)&psf))) {
                         STRRET str;
                         if (SUCCEEDED(psf->GetDisplayNameOf(pidlItem,
                                                   SHGDN_INFOLDER,
                                                   &str))) {
                           StrRetToBuf(&str, pidlItem, g_szItem, MAX_PATH);
                         }
                         psf->Release();
                       }
                       CoTaskMemFree(pidlItem);
                     }
                   }
                   CoTaskMemFree(pidlFolder);
                 }
                 ppf2->Release();
               }
               pfv->Release();
             }
             psv->Release();
           }
           psb->Release();
         }
         psp->Release();
       }
     }
     pwba->Release();
   }
    pdisp->Release();
  }
  psw->Release();
 }
 InvalidateRect(hwnd, NULL, TRUE);
}

Now all we have to do is call this function periodically and print the results.

BOOL
OnCreate(HWND hwnd, LPCREATESTRUCT lpcs)
{
    SetTimer(hwnd, 1, 1000, RecalcText);
    return TRUE;
}

void
PaintContent(HWND hwnd, PAINTSTRUCT *pps)
{
  TextOut(pps->hdc, 0, 0, g_szPath, lstrlen(g_szPath));
  TextOut(pps->hdc, 0, 20, g_szItem, lstrlen(g_szItem));
}

We're ready to roll. Run this program and set it to the side. Then launch an Explorer window and watch the program track the folder you're in and what item you have focused.

Okay, so I hope I made my point: Often, the pieces you need are already there; you just have to figure out how to put them together. Notice that each of the pieces is in itself not very big. You just had to recognize that they could be put together in an interesting way.

Exercise: Change this program so it takes the folder and switches it to details view.

[Raymond is currently on vacation; this message was pre-recorded.]

Comments (37)
  1. Anonymous says:

    If everything is so nicely scoped, why not use CComPtr<>s / CComQIPtr<>s ? This very same code would be so much more readable (no ugly QueryInterfaces, Releases, uninitialized variables)…

    Guess it’s a habit thing.

  2. Anonymous says:

    I try to avoid using any extra libraries in these articles. If you like those libraries, you can translate raw C++ into your library; but it’s harder for others to translate a library into raw C++. (For example, I could’ve used a library to auto-free the pidlFolder, but that would have confused anybody who wasn’t familiar with that library.)

  3. Anonymous says:

    Hideous. That screams "potential memory leaks!"

  4. Anonymous says:

    That code is hilarious. It’s stuff like that which really puts me off learning about COM. Fortunately, I’ve managed to basically ignore COM so far, and if all this .NET stuff becomes popular maybe I can just pretend it never happened. :)

    Pleaaase release me, let me gooooooo……

  5. Anonymous says:

    Well, you could easily flatten out that code with a CComPtr class, a CComMemoryHandle class, and a function called GuaranteeSuccess that throws an exception on a bad HRESULT and catch that in the function.

  6. Anonymous says:

    Any reader who wants to make an ATL version of this function is free to do so. It would probably be a lot prettier. But I write in pure C++ in order to avoid arguing over which template library is best.

  7. Anonymous says:

    cant compile : error C2065: ‘StrRetToBuf’ : undeclared identifier

  8. Anonymous says:

    Does Explorer lock windows that external clients are looking at (or indeed <em>all</em> of them while the windows are being enumerated) or is this code vulnerable to race conditions?

  9. Anonymous says:

    I try to avoid using any extra libraries in these articles. <

    I for one am glad you approach your problems this way. It shows me what is "really going on". Keep up the good work!!

  10. Anonymous says:

    I believe the only point you’ve made is that only VB or C# should ever be used to access COM :-)



    Cedric

  11. Anonymous says:

    I have a burning sensation in my eyes, is that normal??

  12. Anonymous says:

    Never use goto statements? I LOVE goto statements :)

  13. Anonymous says:

    Zach: Ummm, except now you can’t use C++ construction/destruction semantics with yoru code…

  14. Anonymous says:

    Having had to actually code up a program using a raw dispatch interface in C++, I really appreciate that you wrote this example without libraries. When I was trying to design my program I nearly tore my hair out trying to translate the things written on MSDN with the wrapper code into raw function calls that were actually useful to me.

  15. Anonymous says:

    The thing that bugs me is the use of Hungarian notation. ‘psf’ doesn’t say much, ‘shellFolder’ does.

  16. Anonymous says:

    Zachary: Note however that some of your code goto’s over variable initialization – for example if the initial CoCreateInstance fails. This results in the RELEASE trying to use an uninitialized variable.

  17. Anonymous says:

    Zachary, you have to make sure that when you make macros you take care to write them correctly. For instance, this hypothetical code is wrong:

    if(–i == 0) // we have no more use for the object

    RELEASE(pObj);

    else

    COM_CALL(pObj->foo(), finish);

    First, because the RELEASE() macro has a semi-colon, you end up with two semi-colons and that’ll terminate the if statement before the else. If you remove the extra semi-colon, then the else clause gets attached to the if statement in the macro, and not the if(–i == 0).

    I think the usual way of handling this is to use:

    #define RELEASE(ptr)

    if (ptr) ptr->Release(); else

    So that the semi-colon at the end of RELEASE(foo); will close the else.

  18. Anonymous says:

    Or instead of a macro, you use something like a CComPtr, and to fix the nesting, you do like this:

    enum EBadResult

    {

    kBadResult

    };

    void Succeed( HRESULT hr )

    {

    if ( !SUCCEEDED(hr) )

    {

    throw kBadResult;

    }

    }

    … then in your code, you can simply do …

    try

    {

    Object obj;

    Succeed( incoming->DoThis( obj.GetPtrPtr() ) );

    Succeed( obj->DoThisNow() );

    }

    catch ( EBadResult )

    {

    // Do what you need to in order to fail

    }

    … and you will get automatic destruction of the intermediate objects that you didn’t use. You write a small amount more code, you get similar ease of use as goto, and much MUCH more robust error handling in the language.

  19. Anonymous says:

    Great post. Keep ’em coming with more good stuff for the shell!

  20. Anonymous says:

    Even in pure C++, better to just stick a label at the bottom and have a goto statement if the COM call fails, rather than use deeper nesting if the call succeeds.

    To the poster who said it’s things like that make you not want to learn COM, it’s very simple and easy to read as long as you provide an appropriate framework for making it easy to read. For example, consider the following two macros:

    #define COM_CALL(pfn, lbl)

    if (FAILED(pfn)) goto lbl;

    #define RELEASE(ptr)

    if (ptr) ptr->Release();

    Now the RecalcText function is as follows:

    void CALLBACK RecalcText(HWND hwnd, UINT, UINT_PTR, DWORD)

    {

    HWND hwndFind = GetForegroundWindow();

    g_szPath[0] = TEXT(‘’);

    g_szItem[0] = TEXT(‘’);

    IShellWindows *psw;

    COM_CALL(CoCreateInstance(CLSID_ShellWindows, NULL, CLSCTX_ALL, IID_IShellWindows, (void**)&psw), finish);

    VARIANT v;

    V_VT(&v) = VT_I4;

    IDispatch *pdisp;

    BOOL fFound = FALSE;

    for (V_I4(&v) = 0; !fFound && psw->Item(v, &pdisp) == S_OK; V_I4(&v)++)

    {

    }

    IWebBrowserApp *pwba = NULL;

    IServiceProvider *psp = NULL;

    IShellBrowser *psb = NULL;

    IShellView *psv = NULL;

    IFolderView *pfv = NULL;

    IPersistFolder2 *ppf2 = NULL;

    LPITEMIDLIST pidlFolder = NULL;

    LPITEMIDLIST pidlItem = NULL;

    IShellFolder *psf = NULL;

    STRRET str;

    COM_CALL(pdisp->QueryInterface(IID_IWebBrowserApp, (void**)&pwba), finish);

    if (hwndWBA != hwndFind)

    goto finish;

    COM_CALL(pwba->QueryInterface(IID_IServiceProvider, (void**)&psp), finish);

    COM_CALL(psp->QueryService(SID_STopLevelBrowser, IID_IShellBrowser, (void**)&psb), finish);

    COM_CALL(psb->QueryActiveShellView(&psv), finish);

    COM_CALL(psv->QueryInterface(IID_IFolderView, (void**)&pfv), finish);

    COM_CALL(pfv->GetFolder(IID_IPersistFolder2, (void**)&ppf2), finish);

    COM_CALL(ppf2->GetCurFolder(&pidlFolder), finish);

    if (!SHGetPathFromIDList(pidlFolder, g_szPath))

    lstrcpyn(g_szPath, TEXT("<not a directory>"), MAX_PATH);

    int iFocus;

    COM_CALL(pfv->GetFocusedItem(&iFocus), finish);

    COM_CALL(pfv->Item(iFocus, &pidlItem), finish);

    COM_CALL(ppf2->QueryInterface(IID_IShellFolder, (void**)&psf), finish);

    COM_CALL(psf->GetDisplayNameOf(pidlItem, SHGDN_INFOLDER, &str), finish);

    StrRetToBuf(&str, pidlItem, g_szItem, MAX_PATH);

    finish:

    RELEASE(psf);

    TASK_FREE(pidlItem);

    TASK_FREE(pidlFolder);

    RELEASE(ppf2);

    RELEASE(pfv);

    RELEASE(psv);

    RELEASE(psb);

    RELEASE(psp);

    RELEASE(pwba);

    RELEASE(pdisp);

    RELEASE(psw);

    InvalidateRect(hwnd, NULL, TRUE);

    }

    I probably introduced a few bugs here, but I just typed this in notepad and didn’t bother compiling. The point is obvious, just change the logic flow and COM is much easier to follow. Don’t listen to what they say, that you should "never" use goto statements.

  21. Anonymous says:

    For writing anything but the simplest macros, I typically recommend enclosing in a do { … } while(0) block as follows:

    #define DoStuff()

    do {

    // statement 1;

    // statement 2;

    // …;

    } while (0)

    The if (…) else trick should work as well.

    I also explained my C/C++ error-handling style which uses gotos, along with its limitations, in:

    http://www.livejournal.com/users/sengelha/35640.html

    http://www.livejournal.com/users/sengelha/37087.html

    http://www.livejournal.com/users/sengelha/37182.html

  22. Anonymous says:

    I try to avoid using any extra libraries in these articles.

    Even the Microsoft C++ runtime library includes COM support classes. For most Windows developers, that’s hardly an ‘extra library’.

  23. Anonymous says:

    I tend to use the exception concept and ATL com smart pointers. I know the example wasn’t written in ATL, but even if thats the only ATL functionality you use its worth it in my opinion.

    As for the macro, I typically use

    inline HRESULT TestComCall( HRESULT hr ) throw (_com_error)

    {

    if ( FAILED( hr ) )

    _com_issue_error( hr );

    return hr;

    }

    it returns hr so you can use it with the Next method on IEnum interfaces. You can replace the _com_error with your favourite exception. Also its a shame, but the microsoft compiler whinges about the exception specification.

  24. Anonymous says:

    Hy,

    Is there any way to similarly get a handle of the text inputs in the Internet Explorer ?

    If it could be a write-only way (for setting texts inside) that would be great, because it would mean that I could write a program to fill my credentials on my visited sites without sharing them with password manager and such.

    So, is it possible?

    Could you give me some guidelines, please, as I am new to advanced COM programming.

    Thank you!

  25. Anonymous says:

    mshtml.h contains the interfaces you need. I leave filling in the details as an exercise. (Besides, this is an IE question and that’s not my area.)

  26. Anonymous says:

    Steve, I’m curious about the use of do…while(0) in your macro. I’ve written scads of multiline code macros, and all I ever do is enclose them in braces. So your example would be:

    #define DoStuff()

    {

    // statement 1;

    // statement 2;

    // …;

    }

    Is there a situation where that would fail and do…while(0) would fix it?

  27. Anonymous says:

    Michael Geary: The #define DoStuff() { … } style works in most cases but not all. See http://www.livejournal.com/users/sengelha/38124.html for details.

  28. Anonymous says:

    Well as I said, I typed it in notepad and didn’t compile anything ;-) The point was just to illustrate a general concept, which when applied would simplify readability. Thanks for pointing out the errors though. To respond to the individual comments (I’m sure everyone knows everything I’m about to say, but I might as well say it anyway for the sake of completeness):

    Raymond: Skipping over variable initialization is easily handled by moving all declarations and setting them equal to 0. The release macro checks whether or not the value of the pointer is 0, and if it is it does nothing. So you never release garbage that way. Obviously you know that already, but like I said just pointing it out for completeness’ sake.

    Jack: Are you referring to construction/destruction semantics with regards to COM smart pointers auto add-refing and releasing? You can use whatever you want, this was just an attempt to simplify readability in a way that didn’t use any smart pointer template library, which was Raymond’s initial goal as well. Since some readers said "it sucks, you can’t read it without smart pointers", I was just providing a way by which you could ;-)

    Joe: Good point, thanks :)

    Jack (2nd message): Raymond’s original goal was specifically NOT to use any library such as that, so that people didn’t have to argue over which template library was best.

    Michael: Yes, the do while macro allows you to put a semicolon at the end Consider your macro to be called DOMICHAEL, and steve’s to be called DOSTEVE. Now consider the following code:

    if (1)

    DOMIKE();

    else

    DOSTEVE();

    if (1)

    DOSTEVE();

    else

    DOMIKE();

    The first if/else block generates a compiler error, because a ; after DOMIKE() terminates the if block, as Joe pointed out earlier. The second if block does NOT generate a compiler error, however, as you are required to put a ; to terminate the do while.

    That’s the only difference that I see. This only applies for single line if blocks as well, if you already had enclosing braces for the if block there would be no difference.

  29. Anonymous says:

    Seems to me you might as well stick with the simple braces method since a do { … } while (0) approach also results in a syntax error if you leave off the semicolon.

    i.e. this won’t compile either:

    if ( expression )

    do { a(); b(); } while (0)

    else

    c();

    so its really just a question of whether you think semicolon’s should be used to terminate macro usage.

    I’ll grant you the lack of a semicolon is a bit more obvious a syntax error for the macro than a mismatched else.

    -Michael

  30. Anonymous says:

    I was once reminded of the constant controversy that surrounds the use of goto by the two following posts: Raymond Chen’s article on a COM trick to track the Explorer…

  31. Anonymous says:

    Stephen and Zachary, many thanks for the explanation on the do…while(0) bit. All makes sense now, and I’ll start doing my own macros that way.

    Michael M., when I write a code macro like this, I like to do it in such a way that I can use the semicolon when I use the macro. The idea is that the code that calls the macro should look the same whether it’s a macro or a function.

    Taking a simpler example, I would write a macro like this:

    #define DoStuff() CallSomething()

    and not like this:

    #define DoStuff() CallSomething();

    because when I call DoStuff, I want to call it like this:

    DoStuff();

    and not like this:

    DoStuff()

    The latter style means that the caller has to know it’s a macro, which I wouldn’t want.

  32. Anonymous says:

    The semicolon-less style also messes up most code auto-formatters / syntax colorers.

  33. Anonymous says:

    buncha jealous heytah’s ;-)

  34. Anonymous says:

    Looks like this turned into a discussion about code readability and the use of gotos.

    Gotos are not good, but suggest the need for better error handling than the inadequate try-catch semantics. I suggest Structured Cascaded Exception Handling solves this problem and should be adopted in the next generation of languages.

  35. Anonymous says:

    You forgot to mention the Accessibilty Interfaces! Go straight to the focused item, get notified when it changes rather than polling…

    Run the magnifier (Start->Programs->Accessories->Accessibility->Magnifier) or Narrator (same place) to see what those can do. Then see http://microsoft.com/enable/ for a ling to the developer docs.

  36. Anonymous says:

    The ShellWindows object was designed for scripting.

Comments are closed.