FolderBrowserDialog:保持树和文本字段同步

时间:2017-04-23 10:52:55

标签: .net wpf winapi

由于WPF超过10年后仍然没有文件夹浏览器对话框,我不想仅为此对话框设置对System.Windows.Forms的引用,我搜索了一下,找到了一个很好的来源(我在想)符合我的需要。

以下是代码:

原生的东西:

Class NativeMethods

    <DllImport("shell32.dll")>
    Public Shared Function SHCreateShellItem(pidlParent As IntPtr, psfParent As IntPtr, pidl As IntPtr, ByRef ppsi As IShellItem) As Integer
    End Function

    <DllImport("shell32.dll")>
    Public Shared Function SHParseDisplayName(<MarshalAs(UnmanagedType.LPWStr)> ByVal pszPath As String, ByVal bindingContext As IntPtr, <Out()> ByRef pidl As IntPtr, ByVal sfgaoIn As UInteger, <Out()> ByRef sfgaoOut As UInteger) As Integer
    End Function

End Class

<ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)>
Friend Interface IShellItem
    Sub BindToHandler(<[In], MarshalAs(UnmanagedType.[Interface])> pbc As IntPtr, <[In]> ByRef bhid As Guid, <[In]> ByRef riid As Guid, ByRef ppv As IntPtr)
    Sub GetParent(<MarshalAs(UnmanagedType.[Interface])> ByRef ppsi As IShellItem)
    Sub GetDisplayName(<[In]> sigdnName As SIGDN, <MarshalAs(UnmanagedType.LPWStr)> ByRef ppszName As String)
    Sub GetAttributes(<[In]> sfgaoMask As UInteger, ByRef psfgaoAttribs As UInteger)
    Sub Compare(<[In], MarshalAs(UnmanagedType.[Interface])> psi As IShellItem, <[In]> hint As UInteger, ByRef piOrder As Integer)
End Interface

<ComImport, Guid("B63EA76D-1F85-456F-A19C-48159EFA858B"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)>
Friend Interface IShellItemArray
    Sub BindToHandler(<[In], MarshalAs(UnmanagedType.[Interface])> pbc As IntPtr, <[In]> ByRef rbhid As Guid, <[In]> ByRef riid As Guid, ByRef ppvOut As IntPtr)
    Sub GetPropertyStore(<[In]> Flags As Integer, <[In]> ByRef riid As Guid, ByRef ppv As IntPtr)
    Sub GetPropertyDescriptionList(<[In], MarshalAs(UnmanagedType.Struct)> ByRef keyType As IntPtr, <[In]> ByRef riid As Guid, ByRef ppv As IntPtr)
    Sub GetAttributes(<[In], MarshalAs(UnmanagedType.I4)> dwAttribFlags As IntPtr, <[In]> sfgaoMask As UInteger, ByRef psfgaoAttribs As UInteger)
    Sub GetCount(ByRef pdwNumItems As UInteger)
    Sub GetItemAt(<[In]> dwIndex As UInteger, <MarshalAs(UnmanagedType.[Interface])> ByRef ppsi As IShellItem)
    Sub EnumItems(<MarshalAs(UnmanagedType.[Interface])> ByRef ppenumShellItems As IntPtr)
End Interface

<ComImport, Guid("d57c7288-d4ad-4768-be02-9d969532d960"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), CoClass(GetType(FileOpenDialog))>
Friend Interface IFileOpenDialog
    <PreserveSig>
    Function Show(<[In]> parent As IntPtr) As Integer
    Sub SetFileTypes(<[In]> cFileTypes As UInteger, <[In], MarshalAs(UnmanagedType.Struct)> ByRef rgFilterSpec As IntPtr)
    Sub SetFileTypeIndex(<[In]> iFileType As UInteger)
    Sub GetFileTypeIndex(ByRef piFileType As UInteger)
    Sub Advise(<[In], MarshalAs(UnmanagedType.[Interface])> pfde As IntPtr, ByRef pdwCookie As UInteger)
    Sub Unadvise(<[In]> dwCookie As UInteger)
    Sub SetOptions(<[In]> fos As FOS)
    Sub GetOptions(ByRef pfos As FOS)
    Sub SetDefaultFolder(<[In], MarshalAs(UnmanagedType.[Interface])> psi As IShellItem)
    Sub SetFolder(<[In], MarshalAs(UnmanagedType.[Interface])> psi As IShellItem)
    Sub GetFolder(<MarshalAs(UnmanagedType.[Interface])> ByRef ppsi As IShellItem)
    Sub GetCurrentSelection(<MarshalAs(UnmanagedType.[Interface])> ByRef ppsi As IShellItem)
    Sub SetFileName(<[In], MarshalAs(UnmanagedType.LPWStr)> pszName As String)
    Sub GetFileName(<MarshalAs(UnmanagedType.LPWStr)> ByRef pszName As String)
    Sub SetTitle(<[In], MarshalAs(UnmanagedType.LPWStr)> pszTitle As String)
    Sub SetOkButtonLabel(<[In], MarshalAs(UnmanagedType.LPWStr)> pszText As String)
    Sub SetFileNameLabel(<[In], MarshalAs(UnmanagedType.LPWStr)> pszLabel As String)
    Sub GetResult(<MarshalAs(UnmanagedType.[Interface])> ByRef ppsi As IShellItem)
    Sub AddPlace(<[In], MarshalAs(UnmanagedType.[Interface])> psi As IShellItem, fdcp As Object)
    Sub SetDefaultExtension(<[In], MarshalAs(UnmanagedType.LPWStr)> pszDefaultExtension As String)
    Sub Close(<MarshalAs(UnmanagedType.[Error])> hr As Integer)
    Sub SetClientGuid(<[In]> ByRef guid As Guid)
    Sub ClearClientData()
    Sub SetFilter(<MarshalAs(UnmanagedType.[Interface])> pFilter As IntPtr)
    Sub GetResults(<MarshalAs(UnmanagedType.[Interface])> ByRef ppenum As IShellItemArray)
    Sub GetSelectedItems(<MarshalAs(UnmanagedType.[Interface])> ByRef ppsai As IShellItemArray)
End Interface

<ComImport, Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")>
Class FileOpenDialog
End Class

Enum SIGDN As UInteger
    DESKTOPABSOLUTEEDITING = &H8004C000UI
    DESKTOPABSOLUTEPARSING = &H80028000UI
    FILESYSPATH = &H80058000UI
    NORMALDISPLAY = 0
    PARENTRELATIVE = &H80080001UI
    PARENTRELATIVEEDITING = &H80031001UI
    PARENTRELATIVEFORADDRESSBAR = &H8007C001UI
    PARENTRELATIVEPARSING = &H80018001UI
    URL = &H80068000UI
End Enum

<Flags>
Enum FOS
    ALLNONSTORAGEITEMS = &H80
    ALLOWMULTISELECT = &H200
    CREATEPROMPT = &H2000
    DEFAULTNOMINIMODE = &H20000000
    DONTADDTORECENT = &H2000000
    FILEMUSTEXIST = &H1000
    FORCEFILESYSTEM = &H40
    FORCESHOWHIDDEN = &H10000000
    HIDEMRUPLACES = &H20000
    HIDEPINNEDPLACES = &H40000
    NOCHANGEDIR = 8
    NODEREFERENCELINKS = &H100000
    NOREADONLYRETURN = &H8000
    NOTESTFILECREATE = &H10000
    NOVALIDATE = &H100
    OVERWRITEPROMPT = 2
    PATHMUSTEXIST = &H800
    PICKFOLDERS = &H20
    SHAREAWARE = &H4000
    STRICTFILETYPES = 4
End Enum

对话类:

Public NotInheritable Class FolderBrowserDialog
    Private _selectedPath As String
    Private _selectedElementName As String
    Private _selectedPaths As String()
    Private _selectedElementNames As String()
    Private _allowNonStoragePlaces As Boolean
    Private _multiSelect As Boolean

    Public Property SelectedPath As String
        Get
            Return _selectedPath
        End Get
        Set(ByVal value As String)
            _selectedPath = value
        End Set
    End Property

    Public Property SelectedElementName As String
        Get
            Return _selectedElementName
        End Get
        Private Set(value As String)
            _selectedElementName = value
        End Set
    End Property

    Public Property SelectedPaths As String()
        Get
            Return _selectedPaths
        End Get
        Private Set(value As String())
            _selectedPaths = value
        End Set
    End Property

    Public Property SelectedElementNames As String()
        Get
            Return _selectedElementNames
        End Get
        Private Set(value As String())
            _selectedElementNames = value
        End Set
    End Property

    Public Property AllowNonStoragePlaces As Boolean
        Get
            Return _allowNonStoragePlaces
        End Get
        Set(ByVal value As Boolean)
            _allowNonStoragePlaces = value
        End Set
    End Property

    Public Property MultiSelect As Boolean
        Get
            Return _multiSelect
        End Get
        Set(ByVal value As Boolean)
            _multiSelect = value
        End Set
    End Property

    Public Function ShowDialog(ByVal owner As Window) As Boolean
        Return Me.ShowDialog(If(owner Is Nothing, IntPtr.Zero, New Interop.WindowInteropHelper(owner).Handle))
    End Function

    Public Function ShowDialog(ByVal owner As IntPtr) As Boolean
        Dim dialog As IFileOpenDialog

        If (Environment.OSVersion.Version.Major < 6) Then
            Throw New InvalidOperationException("The dialog needs at least Windows Vista to work.")
        End If

        dialog = Me.CreateNativeDialog

        Try
            Call Me.SetInitialFolder(dialog)
            Call Me.SetOptions(dialog)

            If (dialog.Show(owner) <> 0) Then
                Return False
            End If

            Call Me.SetDialogResults(dialog)

            Return True
        Finally
            Marshal.ReleaseComObject(dialog)
        End Try
    End Function

    Private Function CreateNativeDialog() As IFileOpenDialog
        Return DirectCast(New FileOpenDialog, IFileOpenDialog)
    End Function

    Private Sub SetInitialFolder(ByVal dialog As IFileOpenDialog)
        Dim item As IShellItem = Nothing
        Dim idl As IntPtr
        Dim atts As UInteger

        If Not String.IsNullOrEmpty(Me.SelectedPath) Then
            If (NativeMethods.SHParseDisplayName(Me.SelectedPath, IntPtr.Zero, idl, 0, atts) = 0) AndAlso (NativeMethods.SHCreateShellItem(IntPtr.Zero, IntPtr.Zero, idl, item) = 0) Then
                dialog.SetDefaultFolder(item)
            End If
        End If
    End Sub

    Private Sub SetOptions(ByVal dialog As IFileOpenDialog)
        dialog.SetOptions(Me.GetDialogOptions)
    End Sub

    Private Function GetDialogOptions() As FOS
        Dim options As FOS

        options = FOS.PICKFOLDERS
        If Me.MultiSelect Then
            options = options Or FOS.ALLOWMULTISELECT
        End If

        If Not Me.AllowNonStoragePlaces Then
            options = options Or FOS.FORCEFILESYSTEM
        End If

        Return options
    End Function

    Private Sub SetDialogResults(ByVal dialog As IFileOpenDialog)
        Dim item As IShellItem = Nothing
        Dim path As String = Nothing
        Dim value As String = Nothing

        If Not Me.MultiSelect Then
            dialog.GetResult(item)

            Call Me.GetPathAndElementName(item, path, value)

            Me.SelectedPath = path
            Me.SelectedPaths = New String() {path}
            Me.SelectedElementName = value
            Me.SelectedElementNames = New String() {value}
        Else
            Dim items As IShellItemArray = Nothing
            Dim count As UInteger

            dialog.GetResults(items)
            items.GetCount(count)

            Me.SelectedPaths = New String(count - 1) {}
            Me.SelectedElementNames = New String(count - 1) {}

            For i As UInteger = 0 To count - 1
                items.GetItemAt(i, item)

                Call Me.GetPathAndElementName(item, path, value)
                Me.SelectedPaths(i) = path
                Me.SelectedElementNames(i) = value
            Next i

            Me.SelectedPath = Nothing
            Me.SelectedElementName = Nothing
        End If
    End Sub

    Private Sub GetPathAndElementName(ByVal item As IShellItem, ByRef path As String, ByRef elementName As String)
        item.GetDisplayName(SIGDN.PARENTRELATIVEFORADDRESSBAR, elementName)

        Try
            item.GetDisplayName(SIGDN.FILESYSPATH, path)
        Catch ex As ArgumentException When (ex.HResult = -2147024809)
            path = Nothing
        End Try
    End Sub

End Class

除了一个小缺陷之外,这可以像预期的那样工作。当我在WPF窗口中使用这个类时......

    Dim dialog As FolderBrowserDialog

    dialog = New FolderBrowserDialog
    dialog.SelectedPath = "C:\Windows\System32"

    If dialog.ShowDialog(Me) Then
        MsgBox(String.Join(", ", dialog.SelectedPaths))
    End If

...显示文件夹浏览器对话框,对话框顶部的文本字段设置为我的初始文件夹C:\Windows\System32,但对话框左侧的树视图仅设置为drive {{ 1}}。因此,当我想通过鼠标选择我的初始文件夹的子文件夹时,我需要先点击几次导航到初始文件夹,然后才能选择我真正想要选择的文件夹。

有没有办法让文本字段和对话框的树视图保持同步?

我知道有第三方的东西,比如ookii.dialogs,但我想让我的大部分代码都在我的控制之下。

0 个答案:

没有答案