BrowseForFolder

Sometimes you want to choose a folder but the usual system dialogs require you to choose a file as well. The API function SHBrowseForFolder does not show any files: it does exactly what the name implies.

/* a test/demo program */
DEFINE VARIABLE folder AS CHARACTER NO-UNDO.
DEFINE VARIABLE canceled AS LOGICAL NO-UNDO.
 
RUN BrowseForFolder.p ("choose the directory where you want to dump your data",
                       OUTPUT folder, 
                       OUTPUT canceled).
 
MESSAGE "folder=" folder SKIP
        "canceled=" canceled
        VIEW-AS ALERT-BOX.
 
/* ==========================================================
   file:  BrowseForFolder.p
   ========================================================== */
{windows.i}
 
DEFINE INPUT  PARAMETER DialogTitle AS CHARACTER NO-UNDO.
DEFINE OUTPUT PARAMETER FolderName  AS CHARACTER NO-UNDO.
DEFINE OUTPUT PARAMETER Canceled    AS LOGICAL NO-UNDO.
 
DEFINE VARIABLE MAX_PATH       AS INTEGER INITIAL 260.
DEFINE VARIABLE lpbi           AS MEMPTR.  /* pointer to BROWSEINFO structure */
DEFINE VARIABLE pszDisplayName AS MEMPTR.
DEFINE VARIABLE lpszTitle      AS MEMPTR.
DEFINE VARIABLE lpItemIDList   AS INTEGER NO-UNDO.
DEFINE VARIABLE ReturnValue    AS INTEGER NO-UNDO.
 
SET-SIZE(lpbi)           = 32.
SET-SIZE(pszDisplayName) = MAX_PATH.
SET-SIZE(lpszTitle)      = LENGTH(DialogTitle) + 1.
 
PUT-STRING(lpszTitle,1)  = DialogTitle.
 
PUT-LONG(lpbi, 1) = 0.  /* hwnd for parent */
PUT-LONG(lpbi, 5) = 0.
PUT-LONG(lpbi, 9) = GET-POINTER-VALUE(pszDisplayName).
PUT-LONG(lpbi,13) = GET-POINTER-VALUE(lpszTitle).
PUT-LONG(lpbi,17) = 1. /* BIF_RETURNONLYFSDIRS = only accept a file system directory */
PUT-LONG(lpbi,21) = 0. /* lpfn, callback function */
PUT-LONG(lpbi,25) = 0. /* lParam for lpfn */
PUT-LONG(lpbi,29) = 0.
 
RUN SHBrowseForFolder IN hpApi ( INPUT  GET-POINTER-VALUE(lpbi), 
                                 OUTPUT lpItemIDList ).
 
/* parse the result: */
IF lpItemIDList=0 THEN DO:
   Canceled   = YES.
   FolderName = "".
END.
ELSE DO:
   Canceled = NO.
   FolderName = FILL(" ", MAX_PATH).
   RUN SHGetPathFromIDList IN hpApi(lpItemIDList, 
                                    OUTPUT FolderName,
                                    OUTPUT ReturnValue).
   FolderName = TRIM(FolderName).
END.   
 
/* free memory: */
SET-SIZE(lpbi)=0.
SET-SIZE(pszDisplayName)=0.
SET-SIZE(lpszTitle)=0.
RUN CoTaskMemFree (lpItemIDList).
 
PROCEDURE CoTaskMemFree EXTERNAL "ole32.dll" :
  DEFINE INPUT PARAMETER lpVoid AS LONG.
END PROCEDURE.

Notes

Documentation says that SHBrowseForFolder is not supported on Windows NT. However the above procedure was tested on Windows NT and seemed to work fine.

More Notes

The memory occupied by lpItemIDList can be freed by CoTaskMemFree. This was discovered by Todd G. Nist who explains "This will free the memory the shell allocated for the ITEMIDLIST structure which consists of one or more consecutive ITEMIDLIST structures packed on byte boundaries, followed by a 16-bit zero value. An application can walk a list of item identifiers by examining the size specified in each SHITEMID structure and stopping when it finds a size of zero. A pointer to an item identifier list, is called a PIDL (pronounced piddle.) "

Even More Notes

An different but very interesting approach is to use COM Automation: the "shell.application" interface contains a BrowseForFolder function. There is an example in article 18823 of the Progress Knowledgebase.
There is a different example by Julian Lyndon-Smith on page BrowseForFolder using COM

Initial folder

SHBrowseForFolder supports the use of a callback function from where you can specify an initial folder or perform some validations. Unfortunately, callback functions can not be written in Progress 4GL so you will have to wrap it in a DLL. This has been done by Cyril O'Floinn, see BrowseForFolder with an initial folder