选择文件夹对话框 WPF

我开发了一个 WPF4应用程序,在我的应用程序中,我需要让用户选择一个文件夹,应用程序将存储的东西(文件,生成的报告等)。

我的要求:

  • 能够看到标准文件夹树

  • 能够选择一个文件夹

  • WPF 的外观和感觉,这个对话框必须看起来像为 Windows Vista/7而不是 Windows 2000甚至 Win9x 设计的现代应用程序的一部分。

据我所知,直到2010年(。Net 4.0)不会有一个标准的文件夹对话框,但是也许在4.0版本中会有一些变化?

或者我唯一能做的,就是使用一个老式的 WinForms对话框?如果这是唯一的方法来做我需要的,我怎样才能使它看起来更接近 Vista/7风格,而不是 Win9x?

172628 次浏览

Microsoft.Win32.OpenFileDialog is the standard dialog that any application on Windows uses. Your user won't be surprised by its appearance when you use WPF in .NET 4.0

The dialog was altered in Vista. WPF in .NET 3.0 and 3.5 still used the legacy dialog but that was fixed in .NET 4.0. I can only guess that you started this thread because you are seeing that old dialog. Which probably means you're actually running a program that is targeting 3.5. Yes, the Winforms wrapper did get the upgrade and shows the Vista version. System.Windows.Forms.OpenFileDialog class, you'll need to add a reference to System.Windows.Forms.

Only such dialog is FileDialog. Its part of WinForms, but its actually only wrapper around WinAPI standard OS file dialog. And I don't think it is ugly, its actually part of OS, so it looks like OS it is run on.

Other way, there is nothing to help you with. You either need to look for 3rd party implementation, either free (and I don't think there are any good) or paid.

I wrote about it on my blog a long time ago, WPF's support for common file dialogs is really bad (or at least is was in 3.5 I didn't check in version 4), but it's easy to work around it.

You need to add the correct manifest to your application, that will give you a modern style message boxes and folder browser (WinForms, FolderBrowserDialog) but not WPF file open/save dialogs, this is described in those 3 posts (if you don't care about the explanation and only want the solution go directly to the 3rd):

Fortunately, the open/save dialogs are very thin wrappers around the Win32 API that is easy to call with the right flags to get the Vista/7 style (after setting the manifest)

Windows Presentation Foundation 4.5 Cookbook by Pavel Yosifovich on page 155 in the section on "Using the common dialog boxes" says:

"What about folder selection (instead of files)? The WPF OpenFileDialog does not support that. One solution is to use Windows Forms' FolderBrowseDialog class. Another good solution is to use the Windows API Code Pack described shortly."

I downloaded the API Code Pack from Windows® API Code Pack for Microsoft® .NET Framework Windows API Code Pack: Where is it?, then added references to Microsoft.WindowsAPICodePack.dll and Microsoft.WindowsAPICodePack.Shell.dll to my WPF 4.5 project.

Example:

using Microsoft.WindowsAPICodePack.Dialogs;


var dlg = new CommonOpenFileDialog();
dlg.Title = "My Title";
dlg.IsFolderPicker = true;
dlg.InitialDirectory = currentDirectory;


dlg.AddToMostRecentlyUsedList = false;
dlg.AllowNonFileSystemItems = false;
dlg.DefaultDirectory = currentDirectory;
dlg.EnsureFileExists = true;
dlg.EnsurePathExists = true;
dlg.EnsureReadOnly = false;
dlg.EnsureValidNames = true;
dlg.Multiselect = false;
dlg.ShowPlacesList = true;


if (dlg.ShowDialog() == CommonFileDialogResult.Ok)
{
var folder = dlg.FileName;
// Do something with selected folder string
}

MVVM + WinForms FolderBrowserDialog as behavior

public class FolderDialogBehavior : Behavior<Button>
{
public string SetterName { get; set; }


protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Click += OnClick;
}


protected override void OnDetaching()
{
AssociatedObject.Click -= OnClick;
}


private void OnClick(object sender, RoutedEventArgs e)
{
var dialog = new FolderBrowserDialog();
var result = dialog.ShowDialog();
if (result == DialogResult.OK && AssociatedObject.DataContext != null)
{
var propertyInfo = AssociatedObject.DataContext.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(p => p.CanRead && p.CanWrite)
.Where(p => p.Name.Equals(SetterName))
.First();


propertyInfo.SetValue(AssociatedObject.DataContext, dialog.SelectedPath, null);
}
}
}

Usage

     <Button Grid.Column="3" Content="...">
<Interactivity:Interaction.Behaviors>
<Behavior:FolderDialogBehavior SetterName="SomeFolderPathPropertyName"/>
</Interactivity:Interaction.Behaviors>
</Button>

Blogpost: http://kostylizm.blogspot.ru/2014/03/wpf-mvvm-and-winforms-folder-dialog-how.html

Based on Oyun's answer, it's better to use a dependency property for the FolderName. This allows (for example) binding to sub-properties, which doesn't work in the original. Also, in my adjusted version, the dialog shows selects the initial folder.

Usage in XAML:

<Button Content="...">
<i:Interaction.Behaviors>
<Behavior:FolderDialogBehavior FolderName="{Binding FolderPathPropertyName, Mode=TwoWay}"/>
</i:Interaction.Behaviors>
</Button>

Code:

using System.Windows;
using System.Windows.Forms;
using System.Windows.Interactivity;
using Button = System.Windows.Controls.Button;


public class FolderDialogBehavior : Behavior<Button>
{
#region Attached Behavior wiring
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Click += OnClick;
}


protected override void OnDetaching()
{
AssociatedObject.Click -= OnClick;
base.OnDetaching();
}
#endregion


#region FolderName Dependency Property
public static readonly DependencyProperty FolderName =
DependencyProperty.RegisterAttached("FolderName",
typeof(string), typeof(FolderDialogBehavior));


public static string GetFolderName(DependencyObject obj)
{
return (string)obj.GetValue(FolderName);
}


public static void SetFolderName(DependencyObject obj, string value)
{
obj.SetValue(FolderName, value);
}
#endregion


private void OnClick(object sender, RoutedEventArgs e)
{
var dialog = new FolderBrowserDialog();
var currentPath = GetValue(FolderName) as string;
dialog.SelectedPath = currentPath;
var result = dialog.ShowDialog();
if (result == DialogResult.OK)
{
SetValue(FolderName, dialog.SelectedPath);
}
}
}

Add The Windows API Code Pack-Shell to your project

using Microsoft.WindowsAPICodePack.Dialogs;


...


var dialog = new CommonOpenFileDialog();
dialog.IsFolderPicker = true;
CommonFileDialogResult result = dialog.ShowDialog();

If you don't want to use Windows Forms nor edit manifest files, I came up with a very simple hack using WPF's SaveAs dialog for actually selecting a directory.

No using directive needed, you may simply copy-paste the code below !

It should still be very user-friendly and most people will never notice.

The idea comes from the fact that we can change the title of that dialog, hide files, and work around the resulting filename quite easily.

It is a big hack for sure, but maybe it will do the job just fine for your usage...

In this example I have a textbox object to contain the resulting path, but you may remove the related lines and use a return value if you wish...

// Create a "Save As" dialog for selecting a directory (HACK)
var dialog = new Microsoft.Win32.SaveFileDialog();
dialog.InitialDirectory = textbox.Text; // Use current value for initial dir
dialog.Title = "Select a Directory"; // instead of default "Save As"
dialog.Filter = "Directory|*.this.directory"; // Prevents displaying files
dialog.FileName = "select"; // Filename will then be "select.this.directory"
if (dialog.ShowDialog() == true) {
string path = dialog.FileName;
// Remove fake filename from resulting path
path = path.Replace("\\select.this.directory", "");
path = path.Replace(".this.directory", "");
// If user has changed the filename, create the new directory
if (!System.IO.Directory.Exists(path)) {
System.IO.Directory.CreateDirectory(path);
}
// Our final value is in path
textbox.Text = path;
}

The only issues with this hack are :

  • Acknowledge button still says "Save" instead of something like "Select directory", but in a case like mines I "Save" the directory selection so it still works...
  • Input field still says "File name" instead of "Directory name", but we can say that a directory is a type of file...
  • There is still a "Save as type" dropdown, but its value says "Directory (*.this.directory)", and the user cannot change it for something else, works for me...

Most people won't notice these, although I would definitely prefer using an official WPF way if microsoft would get their heads out of their asses, but until they do, that's my temporary fix.

The Ookii Dialogs for WPF has a VistaFolderBrowserDialog class that provides a complete implementation of a folder browser dialog for WPF.

https://github.com/augustoproiete/ookii-dialogs-wpf

Ookii Folder Browser dialog

There's also a version that works with Windows Forms.

Just to say one thing, WindowsAPICodePack can not open CommonOpenFileDialog on Windows 7 6.1.7600.

The FolderBrowserDialog class from System.Windows.Forms is the recommended way to display a dialog that allows a user to select a folder.

Until recently, the appearance and behaviour of this dialog was not in keeping with the other file system dialogs, which is one of the reasons why people were reluctant to use it.

The good news is that FolderBrowserDialog was "modernized" in NET Core 3.0, so is now a viable option for those writing either Windows Forms or WPF apps targeting that version or later.

In .NET Core 3.0, Windows Forms users [sic] a newer COM-based control that was introduced in Windows Vista: FolderBrowserDialog in NET Core 3.0

To reference System.Windows.Forms in a NET Core WPF app, it is necessary to edit the project file and add the following line:

<UseWindowsForms>true</UseWindowsForms>

This can be placed directly after the existing <UseWPF> element.

Then it's just a case of using the dialog:

using System;
using System.Windows.Forms;


...


using var dialog = new FolderBrowserDialog
{
Description = "Time to select a folder",
UseDescriptionForTitle = true,
SelectedPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory)
+ Path.DirectorySeparatorChar,
ShowNewFolderButton = true
};


if (dialog.ShowDialog() == DialogResult.OK)
{
...
}

FolderBrowserDialog has a RootFolder property that supposedly "sets the root folder where the browsing starts from" but whatever I set this to it didn't make any difference; SelectedPath seemed to be the better property to use for this purpose, however the trailing backslash is required.

Also, the ShowNewFolderButton property seems to be ignored as well, the button is always shown regardless.

A comment on the original question from C. Augusto Proiete suggested Ookii dialogs (https://github.com/ookii-dialogs/ookii-dialogs-wpf). That's what I ended up using in my situation. Here's how I used it in my app.

var dialog = new VistaFolderBrowserDialog()
{
Description = "Select Folder",
RootFolder = Environment.SpecialFolder.Desktop,
ShowNewFolderButton = true,
UseDescriptionForTitle = true
};


var result = dialog.ShowDialog();


if (result.HasValue && result.Value)
{
_mySelectedFolder = dialog.SelectedPath;
}