Opening the classic folder browser dialog with a specific folder preselected


Today's Little Program shows how to set the initial selection in the SHBrowse­For­Folder dialog.

The design of the SHBrowse­For­Folder function had a defect: The BROWSEINFO structure doesn't have a cbSize member at the start. This means that the structure cannot ever change because the function would have no way of knowing whether you are calling with the old structure or the new one. If it weren't for this defect, setting the initial selection would have been easy: Add a pidlInitialSelection member to the structure and have people fill it in.

Alas, any new functionality in the SHBrowse­For­Folder function therefore requires that the new functionality be expressed within the constraints of the existing structure.

Fortunately, there's a callback that takes a message number.

The workaround, therefore, is to express any new functionalty in the form of new callback messages.

And that's how the ability to set the initial selection in the folder browser dialog came about. A new message BFFM_INITIALIZED was created, and in handling that message, the callback can specify what it wants the selection to be.

#define UNICODE
#define _UNICODE
#define STRICT_TYPED_ITEMIDS
#include <windows.h>
#include <ole2.h>
#include <oleauto.h>
#include <shlobj.h>
#include <stdio.h> // horrors! Mixing C and C++!

int CALLBACK Callback(
    HWND hwnd, UINT uMsg, LPARAM lParam, LPARAM lpData)
{
 switch (uMsg) {
 case BFFM_INITIALIZED:
  SendMessage(hwnd, BFFM_SETSELECTION, TRUE,
              reinterpret_cast<LPARAM>(L"C:\\Windows"));
  break;
 }
 return 0;
}

int __cdecl wmain(int, wchar_t **)
{
 CCoInitialize init;
 TCHAR szDisplayName[MAX_PATH];
 BROWSEINFO info = { };
 info.pszDisplayName = szDisplayName;
 info.lpszTitle = TEXT("Pick a folder");
 info.ulFlags = BIF_RETURNONLYFSDIRS;
 info.lpfn = Callback;
 PIDLIST_ABSOLUTE pidl = SHBrowseForFolder(&info);
 if (pidl) {
  SHGetPathFromIDList(pidl, szDisplayName);
  wprintf(L"You chose %ls\n", szDisplayName);
  CoTaskMemFree(pidl);
 }
 return 0;
}

We initialize COM and then call the SHBrowse­For­Folder function with information that includes a callback. The callback responds to the BFFM_INITIALIZED message by changing the selection.

It's not hard, but it's a bit cumbersome.

Sorry.

Bonus chatter: The presence of the callback means that the function cannot shunt the work to a new thread when called from an MTA thread because you are now stuck with the problem of which thread the callback should run on.

  • The callback may want to access resources that belong to the original thread, so that argues for the callback being run on the original thraed.

  • The callback may also want to access resources inside the dialog box, say in order to customize it. That argues for the callback being run on the new thread.

You can't have it both ways, so you're stuck.

But it doesn't really matter, because you shouldn't be performing UI from a multi-threaded apartment anyway. There's not much point in making it easier for people to do the wrong thing.

Comments (24)
  1. skSdnW says:

    The struct might not have a size member but you could invent a new BIF flag which increases the size or changes one of the existing members to be a pointer to another struct with more fields. (Yes, there is the buggy app stack garbage issue)

    When coding something like this the callback naturally ends up on the UI thread (or at least it is synchronized with the UI thread), doing it any other way makes things strange and so much more complicated. If the callback was asynchronous/on another thread the UI would probably have to disable its input controls or just ignore mouse/keyboard input while a callback is active.

    I'm pretty sure parts of a IShellFolder implementation has to be free threaded these days while it was STA in Win95 so you could end up with UI on a MTA thread if someone has a "creative" folder implementation.

  2. DWalker says:

    Why is the word "apartment" in MTA?  Why not call it a multi-threaded model?  I looked around Bing and I couldn't figure out the reason.  Apartment seems like a strange word to throw in there.  Why not a skyscraper or a swimming pool?

  3. Anon says:

    @DWalker

    There's a lot of that in development. 'Apartments,' 'Sockets,' 'Singlets,' 'Threads.'

    Every industry has it to a degree (woodworking uses a lot of anatomy, and vice-versa), but computing has some of the most tenuously-connected 'borrowed' jargon of any.

    The "Apartment" model is supposed to imply that one thread lives there and anyone else who wants access has to go through a third-party. But of course, that's not how Apartments work. If someone wants access to my apartment, I can open the door, or give them a key, or leave the door unlocked, or leave the key under the mat, or add them to my lease. Or, the least-effective and least-convenient, I could make them go through a third party (the landlord, or a neighbour).

    And the analogy breaks down to the point of utter absurdity when talking free- or MTA.

  4. BrianM says:

    COM objects "live" in exactly one apartment – that's more or less why the name "apartment" was chosen.  The apartment may be associated with just one thread, with all MTA threads, or with no threads, but all objects in the same apartment get the same threading behavior.  Threading and apartments are what makes the COM threading model just so easy to understand (I'm being cynical here).

  5. Erik says:

    Why is the (apparent) convention to have a Size member in a structure to indicate its version, instead of say a Version member? With a size, I imagine you can only add new stuff, whereas you might want to replace stuff by other stuff also. Seems the result will be that in 50 years we're passing around 10kb structs with 50 functional bytes…

  6. DWalker says:

    @Erik:  I wondered that too.  A new version might want a smaller size Struct, or, as you say, a Struct with different contents but the same size.  I understand checking the size of the struct …

    The linked article says "But the old version of the operating system should accept any size that is greater than or equal to the size it expects."

    Maybe the struct can't get smaller for backwards-compatibility.  Still, I would not rely on the size to INDICATE the version number.  I would use a version number in the struct for that.

  7. DWalker says:

    @BrianM:  Not sure if you're being cynical in your entire message, or just the last part.  I wonder why apartments were chosen, as opposed to, say, a motel or a long-term-care facility.  I suppose they have to live somewhere…  Put the objects out in the open prairie!

  8. Bunk says:

    @Anon: All analogies break down. That's why they're analogies, and not identities. They're just mean to be suggestive of meaning.

  9. 640k says:

    @Bunk: As usual ms optimize for broken apps, and makes all development more cumbersome for developers who do the right thing. A reason why I left win32. Or actually, THE reason.

  10. JamesJohnston says:

    "For Windows Vista or later, it is recommended that you use IFileDialog with the FOS_PICKFOLDERS option rather than the SHBrowseForFolder function. This uses the Open Files dialog in pick folders mode and is the preferred implementation."

    Similar statements exist for the old GetOpenFileName / GetSaveFileName APIs.

    I wish more developers would heed this advice.  I am sick and tired of using software that still uses the old Win95 folder browser (especially if they forego the Win2000 new-style folder dialog).  The GetOpen/SaveFileName APIs can easily fall back to old Win2000-style file dialogs as well if the wrong flags are picked.

    The problem with these old-style dialogs?  There's no tree view on the left with my "favorites" included as a section.  If I'm working in one area or another, I'll commonly put it custom in my favorites list, so I can get to it quickly.  A common trick I use with old-style dialogs is to copy/paste the path from an open Explorer window to get to it quickly in the dialog.

    This old folder selection dialog is the absolute worst of the bunch, worse than the old file dialogs.  If the app forgot to specify the Win2000 new-style flag, I can't even copy/paste a path!

    Yet I still run across software that is copyrighted 2015 that still throws up legacy dialogs.

  11. John says:

    I would have expected the lack of a cbSize member to be fixed by introducing a SHBrowseForFolderEx function which takes a BROWSEINFOEX struct (now with a cbSize member).

  12. Bunk says:

    @640K: Happy for you. We should all live in crystal palaces. The difference here is that Raymond characterizes the situation for SHBrowse­For­Folder as a defect, not an optimization.

  13. Henri Hein says:

    @JamesJohnston:

    I understand your grievances, but some (a lot) of us still have to support XP.  

  14. @Henri Hein: Is it that difficult to dynamically add a couple flags depending on what OS your running on?  Most GUI toolkits should be doing this as a general rule anyway.

  15. 640k says:

    No one is using SHBrowse­For­Folder any more. GetOpenFileName is the thing to use when you want the user to select a folder. Just look at ms own apps, they have all migrated to use GetOpenFileName instead.

  16. Henri Hein says:

    @MNGoldenEagle:

    No, and I do try to up- and downgrade gracefully.  I think JamesJohnston referred to the OFN_EXPLORER flag, which is only "new" in the context of XP and Win2k being newer than Windows 95/98.  My problem is that I really *want* to use IFileDialog, but have yet to work on a project where I could shunt XP support.

  17. Sven2 says:

    A very simple way to change it would be to just replace one of the pointer members in BROWSEINFO with cbSize (which would have to be pointer-sized then). Valid values for cbSize would be too small to be valid pointers, so SHBrowseForFolder can easily check for that.

    It also makes the SHBrowseForFolder implementation very much straightforward:

    if (info->cbSize == sizeof(BROWSEINFO_V1))

    {

     // version 1

    }

    else if (info->cbSize == sizeof(BROWSEINFO_V2))

    {

     // version 2

    }

    else if (info->cbSize > 2048) // POINTER!

    {

     // version 0, the one before cbSize was added

     szDisplayName = (char *) info->cbSize;

    }

    else error(invalid param)

  18. dmex says:

    "The BROWSEINFO structure doesn't have a cbSize member at the start"

    The CONSOLE_SCREEN_BUFFER_INFO structure used by GetConsoleScreenBufferInfo had the same issue and thats how GetConsoleScreenBufferInfoEx and CONSOLE_SCREEN_BUFFER_INFOEX (including cbSize parameter) was included with Vista.

    "The workaround, therefore, is to express any new functionalty in the form of new callback messages."

    Wow. Introduce SHBrowse­For­FolderEx and a new structure including the cbSize parameter and be done with it before we get stuck implementing (and Microsoft having to support) dirty hacks supporting those features until the end of time.

  19. Cesar says:

    @dmex: That sounds like new functionality, to be expressed in the form of a new callback message! Let's introduce a new message BFFM_EX where the callback can specify the address of the new structure!

  20. Paramanand Singh says:

    I like the comment "// horrors! Mixing C and C++!". However horrible it seems, its a frequent practice to use stdio.h just to get access to the printf family.

  21. poizan42 says:

    @Paramanand Singh: The proper way is to include cstdio.

  22. @Cesar and dmex: I wonder if this came about due to the fact that they didn't realize they needed to modify this structure until after the Windows API was locked down for whichever release it was that inspired the shell team to add this callback workaround.  The SHBrowseForFolderEx solution would make more sense and be more future-proof, but if they were stuck with their current API then that's a non-starter.

  23. JamesJohnston says:

    @Henri Hein:  You can have it both ways: IFileDialog and support for Win XP.  Like this…  (We do this, and it works fine on XP):

    if (FAILED(CoCreateInstance(CLSID_FileOpenDialog))) {

      // We must be on Win XP; code to call legacy API goes here…

    } else {

      // Use our new IFileDialog…

    }

    Note that OFN_EXPLORER was introduced with Windows 95, not Windows 2000.  If you don't specify that and you hook, you get an old Windows 3.1-style dialog (!).  The documentation refers to this as the "old-style" dialog.

    Note that Windows has to fall back on the older non-Vista style file dialogs for compatibility reasons when hooking is used.  And if you use SHBrowseForFolder, you're doomed to the old dialogs, no matter what.

    It's worth noting that the wrapper classes provided in old versions of MFC and Borland would routinely hook the dialog, even for very basic "pick a simple file" uses where the user didn't actually need hooking.  That causes a fallback to the old Win2000-style dialog, or even the Win95-style dialog.  So it seems this problem is more common than I'd like.  And I think many devs must still be using older versions of Visual Studio/MFC and don't bother working around this problem.

    @640k:  Are you sure those Microsoft apps aren't using IFileDialog to browse for a folder?  I can't locate any flag that allows you to use GetOpenFileName to pick a folder…

Comments are closed.

Skip to main content