Feedback

C# - FolderBrowserDialog im Vista Style

Veröffentlicht von am 09.01.2016
(0 Bewertungen)
Seit Windows Vista gibt es für den Öffnen- und den Speichern-Dialog ein neues Design. Dieses hat sich nicht grundlegend geändert, bietet aber doch einige Vorteile.
Windows Forms FolderBrowserDialog ist dagegen das alte Fenster mit der TreeView geblieben.

Die WinAPI bietet jedoch auch die Möglichkeit eine Art Folder Browser im Stil des Datei Öffnen Dialogs anzuzeigen. Das ist etwas kompliziert wenn man sich nicht damit auskennt, meine hier gezeigte Klasse kapselt das jedoch, wie gewohnt, vollständig ab.

Tipp: Die ganzen WinAPI/InterOp Typen liegen alle innerhalb der Klasse. Wer größere Projekte mit mehreren nativen Zugriffen hat sollte diese der Übersichtlichtkeit wegen vielleicht besser zu den anderen verschieben. Zu beachten sei, dass jeweils nur das vollständig implementiert ist, was auch benötigt wird.

Hinweis: Ich habe diese Klasse im WPF Stil erstellt. Trotzdem kann man auch Forms als Owner übergeben, da auch ein IWin32Window akzeptiert wird.
Ggf. muss jedoch die ShowDialog(Window) Methode entfernt werden, um die zusätzlichen Verweise für diese Klasse nicht hinzufügen zu müssen.

Benötigte Namespaces
System
System.Runtime.InteropServices
System.Windows
System.Windows.Interop
Microsoft.Win32
/// <summary>
/// Stellt einen Auswahldialog für Ordner und Systemelemente ab Windows Vista bereit.
/// </summary>
public sealed class VistaFolderBrowserDialog
{
    #region Properties

    /// <summary>
    /// Ruft den ausgewählten Ordnerpfad ab bzw. legt diesen fest.
    /// </summary>
    public string SelectedPath { get; set; }
    /// <summary>
    /// Ruft den Anzeigenamen eines einzelnen, ausgewählten Elements ab.
    /// </summary>
    public string SelectedElementName { get; private set; }
    /// <summary>
    /// Ruft ein Array mit Ordnerpfaden der ausgewählten Ordner ab.
    /// </summary>
    public string[] SelectedPaths { get; private set; }
    /// <summary>
    /// Ruft ein Array mit den Namen der ausgewählten Elemente ab.
    /// </summary>
    public string[] SelectedElementNames { get; private set; }

    /// <summary>
    /// Ruft einen Wert ab der angibt ob auch Elemente ausgewählt werden können, die keine Ordner sind oder legt diesen fest.
    /// </summary>
    public bool AllowNonStoragePlaces { get; set; }
    /// <summary>
    /// Ruft einen Wert ab der angibt ob mehrere Elemente ausgewählt werden können oder legt diesen fest.
    /// </summary>
    public bool Multiselect { get; set; }

    #endregion

    #region Public Methods

    /// <summary>
    /// Zeigt den Auswahldialog an.
    /// </summary>
    /// <returns><c>true</c> wenn der Benutzer die Ordnerauswahl bestätigte; andernfalls <c>false</c></returns>
    public bool ShowDialog() => ShowDialog(IntPtr.Zero);

    /// <summary>
    /// Zeigt den Auswahldialog an.
    /// </summary>
    /// <param name="owner">Der Besitzer des Fensters</param>
    /// <returns><c>true</c> wenn der Benutzer die Ordnerauswahl bestätigte; andernfalls <c>false</c></returns>
    public bool ShowDialog(Window owner) => ShowDialog(owner == null ? IntPtr.Zero : new WindowInteropHelper(owner).Handle);

    /// <summary>
    /// Zeigt den Auswahldialog an.
    /// </summary>
    /// <param name="owner">Der Besitzer des Fensters</param>
    /// <returns><c>true</c> wenn der Benutzer die Ordnerauswahl bestätigte; andernfalls <c>false</c></returns>
    public bool ShowDialog(IWin32Window owner) => ShowDialog(owner == null ? IntPtr.Zero : owner.Handle);

    /// <summary>
    /// Zeigt den Auswahldialog an.
    /// </summary>
    /// <param name="owner">Der Besitzer des Fensters</param>
    /// <returns><c>true</c> wenn der Benutzer die Ordnerauswahl bestätigte; andernfalls <c>false</c></returns>
    public bool ShowDialog(IntPtr owner)
    {
        if (Environment.OSVersion.Version.Major < 6)
        {
            throw new InvalidOperationException("The dialog need at least Windows Vista to work.");
        }

        var dialog = CreateNativeDialog();
        try
        {
            SetInitialFolder(dialog);
            SetOptions(dialog);

            if (dialog.Show(owner) != 0)
            {
                return false;
            }

            SetDialogResults(dialog);

            return true;
        }
        finally
        {
            Marshal.ReleaseComObject(dialog);
        }
    }

    #endregion

    #region Helper

    void GetPathAndElementName(IShellItem item, out string path, out string elementName)
    {
        item.GetDisplayName(SIGDN.PARENTRELATIVEFORADDRESSBAR, out elementName);
        try
        {
            item.GetDisplayName(SIGDN.FILESYSPATH, out path);
        }
        catch (ArgumentException ex) when (ex.HResult == -2147024809)
        {
            path = null;
        }
    }

    IFileOpenDialog CreateNativeDialog()
    {
        return new FileOpenDialog() as IFileOpenDialog;
    }
    void SetInitialFolder(IFileOpenDialog dialog)
    {
        IShellItem item;
        if (!string.IsNullOrEmpty(this.SelectedPath))
        {
            IntPtr idl;
            uint atts = 0;
            if (NativeMethods.SHILCreateFromPath(this.SelectedPath, out idl, ref atts) == 0
                && NativeMethods.SHCreateShellItem(IntPtr.Zero, IntPtr.Zero, idl, out item) == 0)
            {
                dialog.SetFolder(item);
            }
        }
    }
    void SetOptions(IFileOpenDialog dialog)
    {
        dialog.SetOptions(GetDialogOptions());
    }
    FOS GetDialogOptions()
    {
        var options = FOS.PICKFOLDERS;
        if (this.Multiselect)
        {
            options |= FOS.ALLOWMULTISELECT;
        }
        if (!AllowNonStoragePlaces)
        {
            options |= FOS.FORCEFILESYSTEM;
        }
        return options;
    }
    void SetDialogResults(IFileOpenDialog dialog)
    {
        IShellItem item;
        if (!this.Multiselect)
        {
            dialog.GetResult(out item);
            string path, value;
            GetPathAndElementName(item, out path, out value);
            this.SelectedPath = path;
            this.SelectedPaths = new[] { path };
            this.SelectedElementName = value;
            this.SelectedElementNames = new[] { value };
        }
        else
        {
            IShellItemArray items;
            dialog.GetResults(out items);

            uint count;
            items.GetCount(out count);

            this.SelectedPaths = new string[count];
            this.SelectedElementNames = new string[count];

            for (uint i = 0; i < count; ++i)
            {
                items.GetItemAt(i, out item);
                string path, value;
                GetPathAndElementName(item, out path, out value);
                this.SelectedPaths[i] = path;
                this.SelectedElementNames[i] = value;
            }

            this.SelectedPath = null;
            this.SelectedElementName = null;
        }
    }

    #endregion

    #region Types

    class NativeMethods
    {
        [DllImport("shell32.dll")]
        public static extern int SHILCreateFromPath([MarshalAs(UnmanagedType.LPWStr)] string pszPath, out IntPtr ppIdl, ref uint rgflnOut);

        [DllImport("shell32.dll")]
        public static extern int SHCreateShellItem(IntPtr pidlParent, IntPtr psfParent, IntPtr pidl, out IShellItem ppsi);

        [DllImport("user32.dll")]
        public static extern IntPtr GetActiveWindow();

    }

    [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    interface IShellItem
    {
        void BindToHandler([In, MarshalAs(UnmanagedType.Interface)] IntPtr pbc, [In] ref Guid bhid, [In] ref Guid riid, out IntPtr ppv);
        void GetParent([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
        void GetDisplayName([In] SIGDN sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName);
        void GetAttributes([In] uint sfgaoMask, out uint psfgaoAttribs);
        void Compare([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, [In] uint hint, out int piOrder);
    }

    [ComImport, Guid("B63EA76D-1F85-456F-A19C-48159EFA858B"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    interface IShellItemArray
    {
        void BindToHandler([In, MarshalAs(UnmanagedType.Interface)] IntPtr pbc, [In] ref Guid rbhid, [In] ref Guid riid, out IntPtr ppvOut);
        void GetPropertyStore([In] int Flags, [In] ref Guid riid, out IntPtr ppv);
        void GetPropertyDescriptionList([In, MarshalAs(UnmanagedType.Struct)] ref IntPtr keyType, [In] ref Guid riid, out IntPtr ppv);
        void GetAttributes([In, MarshalAs(UnmanagedType.I4)] IntPtr dwAttribFlags, [In] uint sfgaoMask, out uint psfgaoAttribs);
        void GetCount(out uint pdwNumItems);
        void GetItemAt([In] uint dwIndex, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
        void EnumItems([MarshalAs(UnmanagedType.Interface)] out IntPtr ppenumShellItems);
    }

    [ComImport, Guid("d57c7288-d4ad-4768-be02-9d969532d960"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), CoClass(typeof(FileOpenDialog))]
    interface IFileOpenDialog //: IFileDialog
    {
        [PreserveSig]
        int Show([In] IntPtr parent);
        void SetFileTypes([In] uint cFileTypes, [In, MarshalAs(UnmanagedType.Struct)] ref IntPtr rgFilterSpec);
        void SetFileTypeIndex([In] uint iFileType);
        void GetFileTypeIndex(out uint piFileType);
        void Advise([In, MarshalAs(UnmanagedType.Interface)] IntPtr pfde, out uint pdwCookie);
        void Unadvise([In] uint dwCookie);
        void SetOptions([In] FOS fos);
        void GetOptions(out FOS pfos);
        void SetDefaultFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi);
        void SetFolder([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi);
        void GetFolder([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
        void GetCurrentSelection([MarshalAs(UnmanagedType.Interface)] 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([MarshalAs(UnmanagedType.Interface)] out IShellItem ppsi);
        void AddPlace([In, MarshalAs(UnmanagedType.Interface)] IShellItem psi, FileDialogCustomPlace fdcp);
        void SetDefaultExtension([In, MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
        void Close([MarshalAs(UnmanagedType.Error)] int hr);
        void SetClientGuid([In] ref Guid guid);
        void ClearClientData();
        void SetFilter([MarshalAs(UnmanagedType.Interface)] IntPtr pFilter);
        void GetResults([MarshalAs(UnmanagedType.Interface)] out IShellItemArray ppenum);
        void GetSelectedItems([MarshalAs(UnmanagedType.Interface)] out IShellItemArray ppsai);
    }

    [ComImport, Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")]
    class FileOpenDialog { }

    enum SIGDN : uint
    {
        DESKTOPABSOLUTEEDITING = 0x8004c000,
        DESKTOPABSOLUTEPARSING = 0x80028000,
        FILESYSPATH = 0x80058000,
        NORMALDISPLAY = 0,
        PARENTRELATIVE = 0x80080001,
        PARENTRELATIVEEDITING = 0x80031001,
        PARENTRELATIVEFORADDRESSBAR = 0x8007c001,
        PARENTRELATIVEPARSING = 0x80018001,
        URL = 0x80068000
    }

    [Flags]
    enum FOS
    {
        ALLNONSTORAGEITEMS = 0x80,
        ALLOWMULTISELECT = 0x200,
        CREATEPROMPT = 0x2000,
        DEFAULTNOMINIMODE = 0x20000000,
        DONTADDTORECENT = 0x2000000,
        FILEMUSTEXIST = 0x1000,
        FORCEFILESYSTEM = 0x40,
        FORCESHOWHIDDEN = 0x10000000,
        HIDEMRUPLACES = 0x20000,
        HIDEPINNEDPLACES = 0x40000,
        NOCHANGEDIR = 8,
        NODEREFERENCELINKS = 0x100000,
        NOREADONLYRETURN = 0x8000,
        NOTESTFILECREATE = 0x10000,
        NOVALIDATE = 0x100,
        OVERWRITEPROMPT = 2,
        PATHMUSTEXIST = 0x800,
        PICKFOLDERS = 0x20,
        SHAREAWARE = 0x4000,
        STRICTFILETYPES = 4
    }

    #endregion
}

4 Kommentare zum Snippet

harborsiem schrieb am 22.01.2016:
Die Properties SeletcedPath, SeletcedPaths sollten auf korrekten Namen korrigiert werden.
SelectedPath, SelectedPaths
Koopakiller schrieb am 23.01.2016:
Danke für den Hinweis. Ich habe die beiden Namen angepasst.
harborsiem schrieb am 04.02.2016:
Der Multiselect bringt eine Exception. Schuld ist eine falsche GUID.
Die GUID für die Interface Definition IFileOpenDialog muss wie folgt lauten:
[ComImport, Guid("d57c7288-d4ad-4768-be02-9d969532d960"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), CoClass(typeof(FileOpenDialog))]
Die verwendete GUID ist das Interface IFileDialog.

In der Funktion SetDialogResults wäre es besser über eine if Abfrage von Multiselect das korrekte Interface zu holen, statt über die COMException das IShellItemArray zu holen.
Koopakiller schrieb am 06.02.2016:
Irgendwie habe ich mit dieser Klasse echt kein Glück. Danke für die Hinweise @harborsiem, ich habe sie entsprechend umgesetzt.
 

Logge dich ein, um hier zu kommentieren!