Hi,

The Windows API function to select a directory is really bad, so I'm trying to use GetOpenFileName() instead.

For that, the idea is to set a hook function, so that when the user hits the OK button without having actually selected a file, the hook function retrieves the currently selected folder, and closes the dialog.

The initial file filter is the 0x01 character (not shown below, but it's there), so that the dialog is only showing folders, allowing the user to navigate them without seing the files.

Here is the code I'm trying to use:

#include <windows.h>
#include <stdio.h>

UINT_PTR CALLBACK OFNHookProc(
  _In_  HWND hdlg,
  _In_  UINT uiMsg,
  _In_  WPARAM wParam,
  _In_  LPARAM lParam
)
{
  int i, idCtrl, lTime;
  NMHDR *pnmh;
  OFNOTIFY *pnot;
  char buff[2000];

  switch (uiMsg)
   { case WM_NOTIFY:
      i = 0;

      pnot = (OFNOTIFY *)lParam;
      pnmh = &pnot->hdr;
      switch(pnmh->code)
       { case CDN_FILEOK:
          i = CommDlg_OpenSave_GetFolderPath(hdlg, buff, 2000);
          break;

         case CDN_FOLDERCHANGE:
          i = CommDlg_OpenSave_GetFolderPath(hdlg, buff, 2000);
          lTime = GetCurrentTime () ;
          PostMessage (hdlg, IDCANCEL, 0, lTime);

          break;
       }
      break;
   }
  return(0);
}

void main(int argc, char **argv)
{
   OPENFILENAME ofn;       // common dialog box structure
   char szFile[260];       // buffer for file name
   HWND hwnd;              // owner window
   HANDLE hf;              // file handle

   // Initialize OPENFILENAME
   ZeroMemory(&ofn, sizeof(ofn));
   ofn.lStructSize = sizeof(ofn);
   ofn.hwndOwner = NULL;
   ofn.lpstrFile = szFile;
   // Set lpstrFile[0] to '\0' so that GetOpenFileName does not
   // use the contents of szFile to initialize itself.
   ofn.lpstrFile[0] = '\0';
   ofn.nMaxFile = sizeof(szFile);
   ofn.lpstrFilter = "Folders\\0";
   ofn.nFilterIndex = 1;
   ofn.lpstrFileTitle = NULL;
   ofn.nMaxFileTitle = 0;
   ofn.lpstrInitialDir = NULL;
   ofn.Flags = OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST |  OFN_ENABLEHOOK |OFN_EXPLORER|OFN_ENABLESIZING;
   ofn.lpfnHook = OFNHookProc;

   // Display the Open dialog box.

   if (GetOpenFileName(&ofn)==TRUE)
       hf = CreateFile(ofn.lpstrFile,
                       GENERIC_READ,
                       0,
                       (LPSECURITY_ATTRIBUTES) NULL,
                       OPEN_EXISTING,
                       FILE_ATTRIBUTE_NORMAL,
                       (HANDLE) NULL);
}

I'm having 4 problems here:

  • The displayed dialog box is the pre-W7 dialog one, rather than the new one displayed when "OFN_ENABLEHOOK |OFN_EXPLORER" is not used.

  • The "case CDN_FILEOK" part is never reached. Whatever the user action, it always goes to "case CDN_FOLDERCHANGE".

  • "i = CommDlg_OpenSave_GetFolderPath(hdlg, buff, 2000);" does not work. i is set to 0, and buff is not updated with the current folder.

  • "PostMessage (hdlg, IDCANCEL, 0, lTime);" does not make the dialog return.

So, basically, nothing work. :)

I'm probably missing something vey obvious, any idea?

Here are the links that helped me write this useless code:

http://winapi.freetechsecrets.com/win32/WIN32CDMGETFOLDERPATH_New__Windows_NT.htm
http://msdn.microsoft.com/en-us/library/windows/desktop/ms646960(v=vs.85).aspx
http://msdn.microsoft.com/en-us/library/windows/desktop/ms646951(v=vs.85).aspx
http://winapi.freetechsecrets.com/win32/WIN32ExplorerStyle_Hook_Procedures.htm

Any help be greatly appreciated.

Edited 3 Years Ago by BobFX

SHBrowseForFolder() GUI is awfull, the point is to avoid it!

You can't enter manually the start of a path, the windows is small, navigation to find a deep folder is a nightmare, and you don't have a favorite places item.

In all, it's a pain to use. I cringe every time an application pops it, and I don't want mine to do it!

And, yes the code is awful. That's why I'm here!

If you are unhappy with the SHBrowseForFolder(), perhaps have a look at the Show Shell common file dialog sample from MSDN.

In your hook procedure, you need to use the parent of the hdlg in order to make CommDlg_OpenSave_GetFolderPath() work, i.e. CommDlg_OpenSave_GetFolderPath(GetParent(hdlg), ...);

PS. I think attempting to cancel the selection inside CDN_FOLDERCHANGE switch, is frankly a horrendous idea.

if you are unhappy with the SHBrowseForFolder(), perhaps have a loo

Thanks, that looks very interesting, I'll look into it.

In your hook procedure, you need to use the parent of the hdlg in order to make CommDlg_OpenSave_GetFolderPath() work, i.e. CommDlg_OpenSave_GetFolderPath(GetParent(hdlg), ...);

That works now.

PS. I think attempting to cancel the selection inside CDN_FOLDERCHANGE switch, is frankly a horrendous idea.

Of course.

But I'm having the problem that whatever the user interaction, I always go into CDN_FOLDERCHANGE, even when the OK button is clicked on.

I put some code here to be able to test it, but it certainly belongs into CDN_FILEOK.

Any idea what I'm doing wrong that prevents pnmh->code from having the real code event?

Any idea what I'm doing wrong that prevents pnmh->code from having the real code event?

Not really, except maybe you are still doing some wild stuff ( like say, PostMessage (hdlg, IDCANCEL, 0, lTime); ) and suffering because of it? Or is your switch/case missing a break and you are falling through to CDN_FOLDERCHANGE?

Then again, are you still trying to select a folder using GetOpenFileName() (which is intended for selecting files)?

Not really, except maybe you are still doing some wild stuff ( like say, PostMessage (hdlg, IDCANCEL, 0, lTime); ) and suffering because of it?

No, I did the tests without the wild stuff.

Or is your switch/case missing a break and you are falling through to CDN_FOLDERCHANGE?

Neither, the code value is always CDN_FOLDERCHANGE, I double ckeked it.

Then again, are you still trying to select a folder using GetOpenFileName() (which is intended for selecting files)?

Yes, because the UI is better than any other specific folder dialog, so it would be very nifty to be able to use it.

Then again, are you still trying to select a folder using GetOpenFileName() (which is intended for selecting files)?

I ran the sample from the link you sent me, and it's exactly what it's doing: using the GetOpenFileName() GUI to selecte folders!

Thanks a bunch.

I'm still curious to know what I'm doing wrong with the hook function, but I will certainly use your sample instead.

Then again, are you still trying to select a folder using GetOpenFileName() (which is intended for selecting files)?
...
Yes, because the UI is better than any other specific folder dialog, so it would be very nifty to be able to use it.

I was implying that it is highly unlikely that you'd find any kind of solution using GetOpenFileName(), strictly because it is not intended for selecting folders.

I ran the sample from the link you sent me, and it's exactly what it's doing: using the GetOpenFileName() GUI to selecte folders!

The GUI that you're seeing may be alike that of GetOpenFileName(), but the code does not invoke GetOpenFileName(), which (again) is a key point.

I'm still curious to know what I'm doing wrong with the hook function, but I will certainly use your sample instead.

You might post your exact code, maybe someone here might figure out what's wrong with it.

I was implying that it is highly unlikely that you'd find any kind of solution using GetOpenFileName(), strictly because it is not intended for selecting folders.

Well, you're right, but it's not a reason not to modify it with the hook function if possible.

Also, the sample you directed me shows this comment:

//
//   FUNCTION: OnOpenAFolder(HWND)
//
//   PURPOSE:  Use the common file open dialog to select a folder.
//
void OnOpenAFolder(HWND hWnd)
{

So, they are using a File Open dialog to select a folder.

Unfortunately, it seems that I cannot use in it my project.

My project is C in fact, not C++. It does not use COM functions, which I'm not familiar with, and I can't even compile the provided sample. I need to work ouside VS 2008 altogether, using the command line compiler and nmake utility (because the same project is also compiled on various Unix systems).

So, I need a strict old C WIN32 API solution, which could be customising GetOpenFileName() if I manage to do it.

Or the C++ sample you sent me to, if I can understand how to make it work in C.

Edit: it seems that I just did!

You might post your exact code, maybe someone here might figure out what's wrong with it.

It's the code posted above, minus the PostMessage part that was for testing purposes.

Edited 3 Years Ago by BobFX

It does not use COM functions

COM is C, not C++. In fact, the entire win32 api (which COM is a part) is C. That doesn't mean you can use c++, just that all win32 api functions have C interface so that they can be called from a variety of programming languages.

After a little testing with that code, I think you are right, it does need to be compiled as c++. I extracted some of the relevant code for FindAFolder(), copied it into a console project then changed the file exten to *.c and it won't compile.

Edited 3 Years Ago by Ancient Dragon

After a little testing with that code, I think you are right, it does need to be compiled as c++. I extracted some of the relevant code for FindAFolder(), copied it into a console project then changed the file exten to *.c and it won't compile.

Yes, I reached that conclusion too. I just added the "extern "C" { " declaration, compiled as C++, and now I can call it from my C program, and it works fine.

It's a much more beautiful inteface than SHBrowseForFolder(), everybody should be using it!

I am just stuck with setting the default directory, due to my ignorance about these classes.

I need to use this:

http://msdn.microsoft.com/en-us/library/windows/desktop/bb775972(v=vs.85).aspx

but I have to idea how to derive "IShellItem *psi" from a char *. Could you give me an example?

Ok, I found out about this method:

IShellItem *psiFolder;                                                                    
WCHAR szFilePath[PATH_MAX+1];                                                             
MultiByteToWideChar(CP_ACP, MB_COMPOSITE, dir_org, strlen(dir_org), szFilePath, PATH_MAX);
hr = SHCreateItemFromParsingName (szFilePath, NULL, IID_PPV_ARGS(&psiFolder));            
if ( SUCCEEDED(hr) )                                                                      
   hr = pfd->SetDefaultFolder(psiFolder);                                                 

It seems that I have everything I wanted now, thank you both for your help.

Once I'll test my code, I'll post it here in case somebody else wants to replace SHBrowseForFolder() too.

Edited 3 Years Ago by BobFX

Here is my code, I hope somebody else will find it useful:

//
//   FUNCTION: OpenAFolder(HWND)
//
//   PURPOSE:  Use the common file open dialog to select a folder, with a better UI then SHBrowseForFolder()
//
//   Parameters:    hWnd      handle to a parent Window
//                  dir_sel   Filled with selected folder
//                  dir_org   First displayed folder. Can be NULL
//
//   Returns:           0     dir has been selected
//                      1     Cancellled by user
//                     -1     Error
//
//  Derived from:  http://code.msdn.microsoft.com/CppShellCommonFileDialog-17b20409
//
#define _POSIX_            // PATH_MAX
#include <stdio.h>
#include <windows.h>
#include <shlobj.h>

#ifdef __cplusplus
extern "C" {
#endif

int OpenAFolder(HWND hWnd, char dir_sel[PATH_MAX+1], char *dir_org)
{
    HRESULT hr;
    int rc;

    // Create a new common open file dialog.
    IFileOpenDialog *pfd = NULL;
    hr = CoCreateInstance(CLSID_FileOpenDialog, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&pfd));
    rc = -1;  // Error

    if (SUCCEEDED(hr))
    {   DWORD dwOptions;
        hr = pfd->GetOptions(&dwOptions);
        if (SUCCEEDED(hr))
            hr = pfd->SetOptions(dwOptions | FOS_PICKFOLDERS);

        // Set the title of the dialog.
        if (SUCCEEDED(hr))
          { hr = pfd->SetTitle(L"Select a Folder");
            if (dir_org)
             { hr = pfd->ClearClientData();
               IShellItem *psiFolder;
               int len = strlen(dir_org);
               WCHAR szFilePath[PATH_MAX+1];
               MultiByteToWideChar(CP_ACP, MB_PRECOMPOSED, dir_org, len, szFilePath, PATH_MAX);
               szFilePath[len] = 0;
               hr = SHCreateItemFromParsingName (szFilePath, NULL, IID_PPV_ARGS(&psiFolder));
               if ( SUCCEEDED(hr) )
                  hr = pfd->SetDefaultFolder(psiFolder);
             }
          }

        // Show the open file dialog.
        if (SUCCEEDED(hr))
        { hr = pfd->Show(hWnd);
          if (SUCCEEDED(hr))
           {  // Get the selection from the user.
              IShellItem *psiResult = NULL;
              hr = pfd->GetResult(&psiResult);
              if (SUCCEEDED(hr))
               { PWSTR pszPath = NULL;
                 hr = psiResult->GetDisplayName(SIGDN_FILESYSPATH, &pszPath);
                 int len;
                 if (SUCCEEDED(hr))
                 { if (0 != (len = WideCharToMultiByte(CP_ACP, 0, pszPath, -1, dir_sel, PATH_MAX, NULL, NULL)))
                     { rc = 0;
                       dir_sel[len] = '\0';
                     }
                   else
                     rc = 1;
                   CoTaskMemFree(pszPath);
                 }
                 psiResult->Release();
               }
           }
          else
           { if (hr == HRESULT_FROM_WIN32(ERROR_CANCELLED))
               rc = 1; // User cancelled the dialog...
           }
        }

        pfd->Release();
    }

    // Report the error.
    if (FAILED(hr))
    { if (hr == HRESULT_FROM_WIN32(ERROR_CANCELLED))
        rc = 1;   // user cancelled the dialog
    }

    return(rc);
}

#ifdef __cplusplus
   }
#endif

Edited 3 Years Ago by BobFX

This question has already been answered. Start a new discussion instead.