Skip to Content

Starting in Windows Vista, Microsoft implemented a new common dialog control for selecting files or folders known as the Common Item Dialog

SelectFolder.PNG

Unlike the earlier common dialog window however, this one isn’t accessible through a simple Windows API call.  Instead, it requires a COM implementation, which makes it a bit trickier to access from PowerBuilder.  Fortunately, this StackOverflow posting provided an example of using the dialog from within C#.  Once we have that, it was only a matter of tweaking it slightly to remove it’s reliance on Windows Forms classes and then creating a COM visible assembly from it that we can then invoke from a PowerBuilder Classic Win32 target via OLE Automation or from one of the .Net target types in PowerBuilder Classic or PowerBuilder.Net by adding the assembly as a reference.  The resulting C# code looks like the following, and the assembly is then made COM Visible and signed so it can be added to the Global Assembly Cache if needed.

using System;
using System.Runtime.InteropServices;
namespace FolderBrowser2
{
    public class FolderBrowser2
    {
        public string DirectoryPath { get; set; }
        public int ShowDialog( ulong handle){
            IntPtr hwndOwner = (IntPtr)handle;
            IFileOpenDialog dialog = (IFileOpenDialog)new FileOpenDialog();
            try
            {
                IShellItem item;
                if (!string.IsNullOrEmpty(DirectoryPath))
                {
                    IntPtr idl;
                    uint atts = 0;
                    if (SHILCreateFromPath(DirectoryPath, out idl, ref atts) == 0)
                    {
                        if (SHCreateShellItem(IntPtr.Zero, IntPtr.Zero, idl, out item) == 0)
                        {
                            dialog.SetFolder(item);
                        }
                    }
                }
                dialog.SetOptions(FOS.FOS_PICKFOLDERS | FOS.FOS_FORCEFILESYSTEM);
                uint hr = dialog.Show(hwndOwner);
                if (hr == ERROR_CANCELLED)
                    return FB2_CANCEL;
                if (hr != 0)
                    return FB2_ERROR;
                dialog.GetResult(out item);
                string path;
                item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out path);
                DirectoryPath = path;
                return FB2_SUCCESS;
            }
            finally
            {
                Marshal.ReleaseComObject(dialog);
            }
        }
        [DllImport("shell32.dll")]
        private static extern int SHILCreateFromPath([MarshalAs(UnmanagedType.LPWStr)] string pszPath, out IntPtr ppIdl, ref uint rgflnOut);
        [DllImport("shell32.dll")]
        private static extern int SHCreateShellItem(IntPtr pidlParent, IntPtr psfParent, IntPtr pidl, out IShellItem ppsi);
        [DllImport("user32.dll")]
        private static extern IntPtr GetActiveWindow();
        private const uint ERROR_CANCELLED = 0x800704C7;
        private const int FB2_SUCCESS = 1;
        private const int FB2_CANCEL = 0;
        private const int FB2_ERROR = -1;
        [ComImport]
        [Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")]
        private class FileOpenDialog
        {
        }
        [ComImport]
        [Guid("42f85136-db7e-439c-85f1-e4075d135fc8")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        private interface IFileOpenDialog
        {
            [PreserveSig]
            uint Show([In] IntPtr parent); // IModalWindow
            void SetFileTypes();  // not fully defined
            void SetFileTypeIndex([In] uint iFileType);
            void GetFileTypeIndex(out uint piFileType);
            void Advise(); // not fully defined
            void Unadvise();
            void SetOptions([In] FOS fos);
            void GetOptions(out FOS pfos);
            void SetDefaultFolder(IShellItem psi);
            void SetFolder(IShellItem psi);
            void GetFolder(out IShellItem ppsi);
            void GetCurrentSelection(out IShellItem ppsi);
            void SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName);
            void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName);
            void SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
            void SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText);
            void SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel);
            void GetResult(out IShellItem ppsi);
            void AddPlace(IShellItem psi, int alignment);
            void SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
            void Close(int hr);
            void SetClientGuid();  // not fully defined
            void ClearClientData();
            void SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter);
            void GetResults([MarshalAs(UnmanagedType.Interface)] out IntPtr ppenum); // not fully defined
            void GetSelectedItems([MarshalAs(UnmanagedType.Interface)] out IntPtr ppsai); // not fully defined
        }
        [ComImport]
        [Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")]
        [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
        private interface IShellItem
        {
            void BindToHandler(); // not fully defined
            void GetParent(); // not fully defined
            void GetDisplayName([In] SIGDN sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName);
            void GetAttributes();  // not fully defined
            void Compare();  // not fully defined
        }
        private enum SIGDN : uint
        {
            SIGDN_DESKTOPABSOLUTEEDITING = 0x8004c000,
            SIGDN_DESKTOPABSOLUTEPARSING = 0x80028000,
            SIGDN_FILESYSPATH = 0x80058000,
            SIGDN_NORMALDISPLAY = 0,
            SIGDN_PARENTRELATIVE = 0x80080001,
            SIGDN_PARENTRELATIVEEDITING = 0x80031001,
            SIGDN_PARENTRELATIVEFORADDRESSBAR = 0x8007c001,
            SIGDN_PARENTRELATIVEPARSING = 0x80018001,
            SIGDN_URL = 0x80068000
        }
        [Flags]
        private enum FOS
        {
            FOS_ALLNONSTORAGEITEMS = 0x80,
            FOS_ALLOWMULTISELECT = 0x200,
            FOS_CREATEPROMPT = 0x2000,
            FOS_DEFAULTNOMINIMODE = 0x20000000,
            FOS_DONTADDTORECENT = 0x2000000,
            FOS_FILEMUSTEXIST = 0x1000,
            FOS_FORCEFILESYSTEM = 0x40,
            FOS_FORCESHOWHIDDEN = 0x10000000,
            FOS_HIDEMRUPLACES = 0x20000,
            FOS_HIDEPINNEDPLACES = 0x40000,
            FOS_NOCHANGEDIR = 8,
            FOS_NODEREFERENCELINKS = 0x100000,
            FOS_NOREADONLYRETURN = 0x8000,
            FOS_NOTESTFILECREATE = 0x10000,
            FOS_NOVALIDATE = 0x100,
            FOS_OVERWRITEPROMPT = 2,
            FOS_PATHMUSTEXIST = 0x800,
            FOS_PICKFOLDERS = 0x20,
            FOS_SHAREAWARE = 0x4000,
            FOS_STRICTFILETYPES = 4
        }
    }
}

Once you have the assembly, if you want to call it from a PowerBuilder Classic Win32 application you’ll need to take the following steps.

1.  Run regasm on it to create the registry entries that PowerBuilder needs to use it via OLE Automation.  If you are on a 64 bit system, you’ll want to generate a reg file using the /regfile: argument and then edit it so that the entries are created in the Wow6432Node/CLSID portoin of the registry rather than the default (64bit) CLSID section.

2.  Run gacutil on it to load it into the Global Assembly Cache (GAC).

To call it from a PowerBuilder Classic Win32 application, you would then only need to do the following:

integer          li_rc
string            ls_folder
ulong            ll_handle
oleobject       loo_fb2
loo_fb2 = Create oleobject
li_rc = loo_fb2.ConnectToNewObject ( "FolderBrowser2.FolderBrowser2" )
ll_handle = Handle ( parent )
li_rc = loo_fb2.ShowDialog ( ll_handle )
IF li_rc = 1 THEN
     ls_folder = loo_fb2.DirectoryPath    
     MessageBox ( "Folder", ls_folder )
END IF
loo_fb2.DisconnectObject()
Destroy loo_fb2

This particular example looks for the user to select a folder.  Slight modification of the sample would allow you to select items instead.

The sample code (both C# and PowerBuilder Classic) is available on my Goggle Drive.  Simply run the FolderBrowser2_32.reg file on a 32 bit system or the FolderBrowser2_64.reg file on a 64 bit system to add the registry entries from REGASM and then run gacutil on the assembly to add it to the GAC.  At that point the PowerBuilder Classic demo should run for you and you’ll see a window like shown above.

To report this post you need to login first.

7 Comments

You must be Logged on to comment or reply to a post.

  1. James Reese

    Hi Bruce, I’m trying to find/implement a replacement for GetFolder() that recognizes network paths that aren’t mapped to a drive letter.  Do you think this would be the best solution?  You mention ‘starting with Windows Vista…’, would this be compatible with XP?

    Regards,

    Jim Reese

    (0) 
    1. Bruce Armstrong Post author

      You mention ‘starting with Windows Vista…’, would this be compatible with XP?

      No, it wouldn’t.  This solution would only work for Vista or later (e.g., Windows 7).  XP came before Vista.

      You might look at the GetFileName replacement that Roland put together  It’s referenced in Dan’s response above.  That’s the older Common File Dialog that is supported on XP.

      (0) 
      1. James Reese

        Thanks, I did look at that, but I only want directory paths, not files.

        But I did find a working example using SHBrowseForFolder function here: powerbuilder.hyderabad-colleges.com/Advanced-PowerBuilder-8-15-240.html

        I had to change some of the flag settings to get it to recognize the unmapped network paths, and the only limitation I have now is that it doesn’t pre-select the previously selected subdirectory in the tree, you can’t pass in a path to the function.  I just found an article discussing that, not sure if I’ll be able to implement it, it uses the function’s callback interface.

        (0) 
        1. Bruce Armstrong Post author

          For something that uses a callback, you could construct a PBNI extension that handles it.  You create the code in C++ and create a wrapper that PB can use.

          See this thread, which references an example I created for the GetOpenFileName call, adding capability not supported in PB:  Common Item Dialog & PB Classic

          (0) 

Leave a Reply