Friday, March 22, 2013

Using the new Common Item Dialog from PowerBuilder

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.

  1. using System;  
  2. using System.Runtime.InteropServices;  
  3. namespace FolderBrowser2  
  4. {  
  5.     public class FolderBrowser2  
  6.     {  
  7.         public string DirectoryPath { getset; }  
  8.         public int ShowDialog( ulong handle){  
  9.             IntPtr hwndOwner = (IntPtr)handle;  
  10.             IFileOpenDialog dialog = (IFileOpenDialog)new FileOpenDialog();  
  11.             try  
  12.             {  
  13.                 IShellItem item;  
  14.                 if (!string.IsNullOrEmpty(DirectoryPath))  
  15.                 {  
  16.                     IntPtr idl;  
  17.                     uint atts = 0;  
  18.                     if (SHILCreateFromPath(DirectoryPath, out idl, ref atts) == 0)  
  19.                     {  
  20.                         if (SHCreateShellItem(IntPtr.Zero, IntPtr.Zero, idl, out item) == 0)  
  21.                         {  
  22.                             dialog.SetFolder(item);  
  23.                         }  
  24.                     }  
  25.                 }  
  26.                 dialog.SetOptions(FOS.FOS_PICKFOLDERS | FOS.FOS_FORCEFILESYSTEM);  
  27.                 uint hr = dialog.Show(hwndOwner);  
  28.                 if (hr == ERROR_CANCELLED)  
  29.                     return FB2_CANCEL;  
  30.                 if (hr != 0)  
  31.                     return FB2_ERROR;  
  32.                 dialog.GetResult(out item);  
  33.                 string path;  
  34.                 item.GetDisplayName(SIGDN.SIGDN_FILESYSPATH, out path);  
  35.                 DirectoryPath = path;  
  36.                 return FB2_SUCCESS;  
  37.             }  
  38.             finally  
  39.             {  
  40.                 Marshal.ReleaseComObject(dialog);  
  41.             }  
  42.         }  
  43.         [DllImport("shell32.dll")]  
  44.         private static extern int SHILCreateFromPath([MarshalAs(UnmanagedType.LPWStr)] string pszPath, out IntPtr ppIdl, ref uint rgflnOut);  
  45.         [DllImport("shell32.dll")]  
  46.         private static extern int SHCreateShellItem(IntPtr pidlParent, IntPtr psfParent, IntPtr pidl, out IShellItem ppsi);  
  47.         [DllImport("user32.dll")]  
  48.         private static extern IntPtr GetActiveWindow();  
  49.         private const uint ERROR_CANCELLED = 0x800704C7;  
  50.         private const int FB2_SUCCESS = 1;  
  51.         private const int FB2_CANCEL = 0;  
  52.         private const int FB2_ERROR = -1;  
  53.         [ComImport]  
  54.         [Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")]  
  55.         private class FileOpenDialog  
  56.         {  
  57.         }  
  58.         [ComImport]  
  59.         [Guid("42f85136-db7e-439c-85f1-e4075d135fc8")]  
  60.         [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]  
  61.         private interface IFileOpenDialog  
  62.         {  
  63.             [PreserveSig]  
  64.             uint Show([In] IntPtr parent); // IModalWindow  
  65.             void SetFileTypes();  // not fully defined  
  66.             void SetFileTypeIndex([In] uint iFileType);  
  67.             void GetFileTypeIndex(out uint piFileType);  
  68.             void Advise(); // not fully defined  
  69.             void Unadvise();  
  70.             void SetOptions([In] FOS fos);  
  71.             void GetOptions(out FOS pfos);  
  72.             void SetDefaultFolder(IShellItem psi);  
  73.             void SetFolder(IShellItem psi);  
  74.             void GetFolder(out IShellItem ppsi);  
  75.             void GetCurrentSelection(out IShellItem ppsi);  
  76.             void SetFileName([In, MarshalAs(UnmanagedType.LPWStr)] string pszName);  
  77.             void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName);  
  78.             void SetTitle([In, MarshalAs(UnmanagedType.LPWStr)] string pszTitle);  
  79.             void SetOkButtonLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszText);  
  80.             void SetFileNameLabel([In, MarshalAs(UnmanagedType.LPWStr)] string pszLabel);  
  81.             void GetResult(out IShellItem ppsi);  
  82.             void AddPlace(IShellItem psi, int alignment);  
  83.             void SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);  
  84.             void Close(int hr);  
  85.             void SetClientGuid();  // not fully defined  
  86.             void ClearClientData();  
  87.             void SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter);  
  88.             void GetResults([MarshalAs(UnmanagedType.Interface)] out IntPtr ppenum); // not fully defined  
  89.             void GetSelectedItems([MarshalAs(UnmanagedType.Interface)] out IntPtr ppsai); // not fully defined  
  90.         }  
  91.         [ComImport]  
  92.         [Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")]  
  93.         [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]  
  94.         private interface IShellItem  
  95.         {  
  96.             void BindToHandler(); // not fully defined  
  97.             void GetParent(); // not fully defined  
  98.             void GetDisplayName([In] SIGDN sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName);  
  99.             void GetAttributes();  // not fully defined  
  100.             void Compare();  // not fully defined  
  101.         }  
  102.         private enum SIGDN : uint  
  103.         {  
  104.             SIGDN_DESKTOPABSOLUTEEDITING = 0x8004c000,  
  105.             SIGDN_DESKTOPABSOLUTEPARSING = 0x80028000,  
  106.             SIGDN_FILESYSPATH = 0x80058000,  
  107.             SIGDN_NORMALDISPLAY = 0,  
  108.             SIGDN_PARENTRELATIVE = 0x80080001,  
  109.             SIGDN_PARENTRELATIVEEDITING = 0x80031001,  
  110.             SIGDN_PARENTRELATIVEFORADDRESSBAR = 0x8007c001,  
  111.             SIGDN_PARENTRELATIVEPARSING = 0x80018001,  
  112.             SIGDN_URL = 0x80068000  
  113.         }  
  114.         [Flags]  
  115.         private enum FOS  
  116.         {  
  117.             FOS_ALLNONSTORAGEITEMS = 0x80,  
  118.             FOS_ALLOWMULTISELECT = 0x200,  
  119.             FOS_CREATEPROMPT = 0x2000,  
  120.             FOS_DEFAULTNOMINIMODE = 0x20000000,  
  121.             FOS_DONTADDTORECENT = 0x2000000,  
  122.             FOS_FILEMUSTEXIST = 0x1000,  
  123.             FOS_FORCEFILESYSTEM = 0x40,  
  124.             FOS_FORCESHOWHIDDEN = 0x10000000,  
  125.             FOS_HIDEMRUPLACES = 0x20000,  
  126.             FOS_HIDEPINNEDPLACES = 0x40000,  
  127.             FOS_NOCHANGEDIR = 8,  
  128.             FOS_NODEREFERENCELINKS = 0x100000,  
  129.             FOS_NOREADONLYRETURN = 0x8000,  
  130.             FOS_NOTESTFILECREATE = 0x10000,  
  131.             FOS_NOVALIDATE = 0x100,  
  132.             FOS_OVERWRITEPROMPT = 2,  
  133.             FOS_PATHMUSTEXIST = 0x800,  
  134.             FOS_PICKFOLDERS = 0x20,  
  135.             FOS_SHAREAWARE = 0x4000,  
  136.             FOS_STRICTFILETYPES = 4  
  137.         }  
  138.     }  
  139. }  

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:

  1. integer          li_rc  
  2. string            ls_folder  
  3. ulong            ll_handle  
  4. oleobject       loo_fb2  
  5. loo_fb2 = Create oleobject  
  6. li_rc = loo_fb2.ConnectToNewObject ( "FolderBrowser2.FolderBrowser2" )  
  7. ll_handle = Handle ( parent )  
  8. li_rc = loo_fb2.ShowDialog ( ll_handle )  
  9. IF li_rc = 1 THEN  
  10.      ls_folder = loo_fb2.DirectoryPath       
  11.      MessageBox ( "Folder", ls_folder )  
  12. END IF  
  13. loo_fb2.DisconnectObject()  
  14. 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:

Joseph Vendra/Software Developer said...

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.