Home » Blog
date 29.Aug.2021

■ Fix for SHBrowseForFolder starting folder selection


Ever since windows 7 at least, the trusty SHBrowseForFolder API, that helps programmers select folders, has a silly little bug: BFFM_SETSELECTION that allows you to choose the starting folder to show in the dialog works only the first time (!?) used, then it mostly doesn't, your specified folder is selected but not brought into view.

All over the net, stack overflow and what have you, people are reporting the bug, recommending desperate solutions, but microsoft shell team are ignoring them. Since they've done nothing all these years — and now they are busy with important things like rounding windows corners for windows 11 <g> — I put together a few lines of code to correct the problem for all shell programmers.

The correction is simple and straightforward. When you pass the starting folder in BFFCALLBACK in response to BFFM_INITIALIZED event, you must find the tree control and send it a TVM_ENSUREVISIBLE message to bring the selected starting folder into view. Since the tree control isn't a direct child of the dialog (it's inside some "SHBrowseForFolder ShellNameSpace Control" class), and in the interest of a generic solution that doesn't rely on any fixed dialog IDs (that can change without notice), we enumerate all child windows.

...Only problem: that works half of the time! So instead of directly posting the message, we must use a 1-shot timer. A bit clunky, but I can't think of anything else...
  browse for folder
#include <ShlObj.h> // and all the rest of it

// locate the treeview in browse for folder dialog
static BOOL CALLBACK winenumfn(HWND hc, LPARAM lp)
{
   TCHAR buf[100];
   buf[0] = 0;
   GetClassName(hc, buf, ARRAYSIZE(buf));

   if(lstrcmpi(WC_TREEVIEW, buf)==0) {
      *(HWND*)lp = hc;
      return 0; // found
   }

   return 1; // more
}

// timer, sorry about the jump
static void CALLBACK BFFTimer(HWND hc, UINT , UINT_PTR idEvent, DWORD )
{
   ATLASSERT(404==idEvent);
   KillTimer(hc, idEvent);

   /*HTREEITEM*/LRESULT sel = SendMessage(hc, TVM_GETNEXTITEM, TVGN_CARET, 0L);
   if(sel != 0)
       PostMessage(hc, TVM_ENSUREVISIBLE,0,sel);
   else
      ATLASSERT(0);
}

// callback setting the initial directory
static int CALLBACK PIDLBrowseCallbackProc(HWND hWnd, UINT uMsg, LPARAM , LPARAM lpData)
{
   if(uMsg == BFFM_INITIALIZED) {
      ATLASSERT(lpData); // full path of the selection to be made
      SendMessage(hWnd, BFFM_SETSELECTION, TRUE/*path not pidl*/, lpData);

      // find the tree and ensure selection is visible
      HWND hc = 0;
      EnumChildWindows(hWnd, winenumfn, (LPARAM)&hc);
      ATLASSERT(hc); // not found?
      if(hc) {
         // direct ensurevisible only works half of the time, so try a timer instead
         SetTimer(hc, 404, 250/*ms*/, BFFTimer);
      }
   }

   return 0;
}

// choose a folder and create its pidl
LPITEMIDLIST PIDL_CreateByBrowsing(LPCTSTR title, LPCTSTR iniDir, UINT ulFlags=BIF_RETURNONLYFSDIRS | BIF_USENEWUI)
{
   // a parent window is required to keep it modal
   BROWSEINFO bi = {GetFocus(), NULL/*browse ALL*/, NULL/*FULL name*/, 
      title, ulFlags, PIDLBrowseCallbackProc, (LPARAM)iniDir, 0};

   LPITEMIDLIST pidl = SHBrowseForFolder(&bi);
   return pidl; // must CoTaskMemFree
}

Use this PIDL_CreateByBrowsing passing a window title and the starting folder full path, and the 2 aforementioned callbacks, and Bob's your uncle. Shell bug fixed!

Post a comment on this topic »

Share |

©2002-2021 ZABKAT LTD, all rights reserved | Privacy policy | Sitemap