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... |
#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 »