Starting in Windows Vista, Microsoft implemented a new common dialog control for selecting files or folders known as the Common Item Dialog.
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.
1 comment:
Bruce, Thanks so much for this, it works so well in PB 2019 R3 so many thanks for this,
I updated the sample so easily to add a argument to the ShowDialog so that if provided the dialog sets the directory to that folder ; useful when we are editing an already predefined / previously set path so that the control brings up that folder on edits instead of rudely throwing the user in some previous path they were in when last using that dialog.
Here is FolderBrowser2.cs slightly updated, not sure to be honest if this was the right approach but it works so whats the harm? Commments appreciated.
Added:
using System.IO;
Updated:
public int ShowDialog( ulong handle, string strStartPath){
Main Update I made was immediately after the line
IShellItem item;
I added the following to test if the user set the path and if that directory exists.
if (!string.IsNullOrEmpty(strStartPath))
{
if (Directory.Exists(strStartPath))
{
DirectoryPath = strStartPath;
}
}
if (!string.IsNullOrEmpty(strStartPath))
{
if (Directory.Exists(strStartPath))
{
DirectoryPath = strStartPath;
}
}
The rest is all the same.
Changes seem like they work for what I wanted to achieve.
Post a Comment