OpenFolderDialog

I hate FolderBrowserDialog. It is limited, ugly and stuck in Windows 2000 GUI. Whenever I had to select folder, I would resort to using SaveFileDialog instead. User would select any file name and I would just use directory part. It was non-optimal but still better than FolderBrowserDialog.

Vista brought new common file dialogs but I pretty much ignored them – most of my programs need to run under XP so there was not much benefit in using Windows Vista specific controls. However, those controls had special mode that finally brings folder selection in normal FileOpenDialog.

Code (after you define all needed COM interfaces) is actually quite straightforward. Once FOS_PICKFOLDERS is set, half of work is done. All other code is just plumbing to show the form and get user’s selection in form of path.

var frm = (IFileDialog)(new FileOpenDialogRCW());
uint options;
frm.GetOptions(out options);
options |= FOS_PICKFOLDERS;
frm.SetOptions(options);

if (frm.Show(owner.Handle) == S_OK) {
    IShellItem shellItem;
    frm.GetResult(out shellItem);
    IntPtr pszString;
    shellItem.GetDisplayName(SIGDN_FILESYSPATH, out pszString);
    this.Folder = Marshal.PtrToStringAuto(pszString);
}

Full code with Windows XP compatibility and little bit more error checking is available for download. However do notice that all unneeded COM fat has been trimmed in order to have it concise. Short and sweet I say.

[2012-02-12: Fixed bug in source code.]

14 thoughts to “OpenFolderDialog”

    1. Added
      public const uint OFN_ALLOWMULTISELECT = 0x00000200;
      But failed in
      if (frm.GetResult(out shellItem) == NativeMethods.S_OK)

  1. Works good with Windows 7 but with XP it just opens the standard SaveFileDialog showing the box for the file name and even [Save] on the button.

    I thought this was supposed to work on XP too?

  2. Here is the vb.net version

    Imports System
    Imports System.IO
    Imports System.Runtime.CompilerServices
    Imports System.Runtime.InteropServices
    Imports System.Windows.Forms
    
    Public Class OpenFolderDialog
        Implements IDisposable
    
        ''' 
        ''' Gets/sets folder in which dialog will be open.
        ''' 
        Public Property InitialFolder() As String
            Get
                Return m_InitialFolder
            End Get
            Set(ByVal value As String)
                m_InitialFolder = value
            End Set
        End Property
        Private m_InitialFolder As String
    
        ''' 
        ''' Gets/sets directory in which dialog will be open if there is no recent directory available.
        ''' 
        Public Property DefaultFolder() As String
            Get
                Return m_DefaultFolder
            End Get
            Set(ByVal value As String)
                m_DefaultFolder = value
            End Set
        End Property
        Private m_DefaultFolder As String
    
        ''' 
        ''' Gets selected folder.
        ''' 
        Public Property Folder() As String
            Get
                Return m_Folder
            End Get
            Private Set(ByVal value As String)
                m_Folder = value
            End Set
        End Property
        Private m_Folder As String
    
    
        Public Function ShowDialog(ByVal owner As IWin32Window) As DialogResult
            If Environment.OSVersion.Version.Major >= 6 Then
                Return ShowVistaDialog(owner)
            Else
                Return ShowLegacyDialog(owner)
            End If
        End Function
    
        Private Function ShowVistaDialog(ByVal owner As IWin32Window) As DialogResult
            Dim frm = DirectCast(New NativeMethods.FileOpenDialogRCW(), NativeMethods.IFileDialog)
            Dim options As UInteger
            frm.GetOptions(options)
            options = options Or NativeMethods.FOS_PICKFOLDERS Or NativeMethods.FOS_FORCEFILESYSTEM Or NativeMethods.FOS_NOVALIDATE Or NativeMethods.FOS_NOTESTFILECREATE Or NativeMethods.FOS_DONTADDTORECENT
            frm.SetOptions(options)
            If Me.InitialFolder IsNot Nothing Then
                Dim directoryShellItem As NativeMethods.IShellItem
                Dim riid = New Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")
                'IShellItem
                If NativeMethods.SHCreateItemFromParsingName(Me.InitialFolder, IntPtr.Zero, riid, directoryShellItem) = NativeMethods.S_OK Then
                    frm.SetFolder(directoryShellItem)
                End If
            End If
            If Me.DefaultFolder IsNot Nothing Then
                Dim directoryShellItem As NativeMethods.IShellItem
                Dim riid = New Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE")
                'IShellItem
                If NativeMethods.SHCreateItemFromParsingName(Me.DefaultFolder, IntPtr.Zero, riid, directoryShellItem) = NativeMethods.S_OK Then
                    frm.SetDefaultFolder(directoryShellItem)
                End If
            End If
    
            If frm.Show(owner.Handle) = NativeMethods.S_OK Then
                Dim shellItem As NativeMethods.IShellItem
                If frm.GetResult(shellItem) = NativeMethods.S_OK Then
                    Dim pszString As IntPtr
                    If shellItem.GetDisplayName(NativeMethods.SIGDN_FILESYSPATH, pszString) = NativeMethods.S_OK Then
                        If pszString  IntPtr.Zero Then
                            Try
                                Me.Folder = Marshal.PtrToStringAuto(pszString)
                                Return DialogResult.OK
                            Finally
                                Marshal.FreeCoTaskMem(pszString)
                            End Try
                        End If
                    End If
                End If
            End If
            Return DialogResult.Cancel
        End Function
    
        Private Function ShowLegacyDialog(ByVal owner As IWin32Window) As DialogResult
            Using frm = New SaveFileDialog()
                frm.CheckFileExists = False
                frm.CheckPathExists = True
                frm.CreatePrompt = False
                frm.Filter = "|" & Guid.Empty.ToString()
                frm.FileName = "any"
                If Me.InitialFolder IsNot Nothing Then
                    frm.InitialDirectory = Me.InitialFolder
                End If
                frm.OverwritePrompt = False
                frm.Title = "Select Folder"
                frm.ValidateNames = False
                If frm.ShowDialog(owner) = DialogResult.OK Then
                    Me.Folder = Path.GetDirectoryName(frm.FileName)
                    Return DialogResult.OK
                Else
                    Return DialogResult.Cancel
                End If
            End Using
        End Function
    
    
        Public Sub Dispose() Implements IDisposable.Dispose
        End Sub
        'just to have possibility of Using statement.
    End Class
    
    Friend Module NativeMethods
    
    
    #Region "Constants"
    
        Public Const FOS_PICKFOLDERS As UInteger = &H20
        Public Const FOS_FORCEFILESYSTEM As UInteger = &H40
        Public Const FOS_NOVALIDATE As UInteger = &H100
        Public Const FOS_NOTESTFILECREATE As UInteger = &H10000
        Public Const FOS_DONTADDTORECENT As UInteger = &H2000000
    
        Public Const S_OK As UInteger = &H0
    
        Public Const SIGDN_FILESYSPATH As UInteger = &H80058000UI
    
    #End Region
    
    
    #Region "COM"
    
         _
        Friend Class FileOpenDialogRCW
        End Class
    
    
         _
        Friend Interface IFileDialog
             _
             _
            Function Show( ByVal hwndOwner As IntPtr) As UInteger
            'IModalWindow 
    
             _
            Function SetFileTypes( ByVal cFileTypes As UInteger,  ByVal rgFilterSpec As IntPtr) As UInteger
    
             _
            Function SetFileTypeIndex( ByVal iFileType As UInteger) As UInteger
    
             _
            Function GetFileTypeIndex(ByRef piFileType As UInteger) As UInteger
    
             _
            Function Advise( ByVal pfde As IntPtr, ByRef pdwCookie As UInteger) As UInteger
    
             _
            Function Unadvise( ByVal dwCookie As UInteger) As UInteger
    
             _
            Function SetOptions( ByVal fos As UInteger) As UInteger
    
             _
            Function GetOptions(ByRef fos As UInteger) As UInteger
    
             _
            Sub SetDefaultFolder( ByVal psi As IShellItem)
    
             _
            Function SetFolder( ByVal psi As IShellItem) As UInteger
    
             _
            Function GetFolder( ByRef ppsi As IShellItem) As UInteger
    
             _
            Function GetCurrentSelection( ByRef ppsi As IShellItem) As UInteger
    
             _
            Function SetFileName( ByVal pszName As String) As UInteger
    
             _
            Function GetFileName( ByRef pszName As String) As UInteger
    
             _
            Function SetTitle( ByVal pszTitle As String) As UInteger
    
             _
            Function SetOkButtonLabel( ByVal pszText As String) As UInteger
    
             _
            Function SetFileNameLabel( ByVal pszLabel As String) As UInteger
    
             _
            Function GetResult( ByRef ppsi As IShellItem) As UInteger
    
             _
            Function AddPlace( ByVal psi As IShellItem, ByVal fdap As UInteger) As UInteger
    
             _
            Function SetDefaultExtension( ByVal pszDefaultExtension As String) As UInteger
    
             _
            Function Close( ByVal hr As UInteger) As UInteger
    
             _
            Function SetClientGuid( ByRef guid As Guid) As UInteger
    
             _
            Function ClearClientData() As UInteger
    
             _
            Function SetFilter( ByVal pFilter As IntPtr) As UInteger
        End Interface
    
    
         _
        Friend Interface IShellItem
             _
            Function BindToHandler( ByVal pbc As IntPtr,  ByRef rbhid As Guid,  ByRef riid As Guid,  ByRef ppvOut As IntPtr) As UInteger
    
             _
            Function GetParent( ByRef ppsi As IShellItem) As UInteger
    
             _
            Function GetDisplayName( ByVal sigdnName As UInteger, ByRef ppszName As IntPtr) As UInteger
    
             _
            Function GetAttributes( ByVal sfgaoMask As UInteger, ByRef psfgaoAttribs As UInteger) As UInteger
    
             _
            Function Compare( ByVal psi As IShellItem,  ByVal hint As UInteger, ByRef piOrder As Integer) As UInteger
        End Interface
    
    #End Region
    
         _
         Public Function SHCreateItemFromParsingName( _
              ByVal pszPath As String, _
             ByVal pbc As IntPtr, _
              ByVal riid As Guid, _
              ByRef ppv As IShellItem) As Integer
        End Function
    End Module
    
    
    

Leave a Reply

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.