如何使用OpenFileDialog选择文件夹?

445

我打算使用如下项目:https://github.com/scottwis/OpenFileOrFolderDialog

然而,存在一个问题:它使用了GetOpenFileName函数和OPENFILENAME结构体。OPENFILENAME有一个名为templateID的成员变量,表示对话框模板的标识符。该项目还包含res1.rc文件和模板化的对话框初始化,但我无法弄清如何将此文件附加到我的C#项目中。

是否有更好的方式来使用OpenFileDialog选择文件夹?


3
有一个 hackish 的解决方案,使用 OpenFileDialog,在此方案中 ValidateNamesCheckFileExists 均设置为 false,而 FileName 则被赋予一个模拟值以表示选择了一个目录。我称其为“hack”,因为用户可能会对如何选择文件夹感到困惑。参见从同一对话框中选择文件或文件夹 - Daniel Ballinger
可能是重复的问题:如何配置 OpenFileDialog 以选择文件夹? - WonderWorker
1
感谢丹指向 OpenFileDialog-Hack!这比 FolderBrowserDialog 好多了,因为 OFD 显示书签文件夹等,所以每个人 - 特别是在大公司中 - 都能找到他们的东西。FBD 在这些地方不会有太大作用。 - JayC667
1
@ComradeJoecool 我已将我的评论转换为答案。我尝试了几次,没有出现“文件未找到”的问题。你是否在重复使用同一个OpenFileDialog实例? - Daniel Ballinger
1
@DanielBallinger 啊,我找到了我的问题,因为我正在使用Powershell创建对话框,将“ValidateNames”和“CheckFileExists”设置为“false”不起作用,我需要将它们设置为“0”(或者学习更好的Powershell)。 - ComradeJoecool
显示剩余5条评论
12个回答

516

基本上,您需要使用FolderBrowserDialog类:

提示用户选择文件夹。 该类不能被继承。

示例:

using(var fbd = new FolderBrowserDialog())
{
    DialogResult result = fbd.ShowDialog();

    if (result == DialogResult.OK && !string.IsNullOrWhiteSpace(fbd.SelectedPath))
    {
        string[] files = Directory.GetFiles(fbd.SelectedPath);

        System.Windows.Forms.MessageBox.Show("Files found: " + files.Length.ToString(), "Message");
    }
}

如果您在使用WPF,则需要添加对System.Windows.Forms的引用。

您还需要添加using System.IO以使用Directory类。


266
FolderBrowserDialog的可用性不足。其主要缺点是它不允许你从Windows资源管理器中复制文件夹路径以进行快速导航,当你需要钻取超过三个级别时,这使其无用。在特定情况下,比如存储速度较慢或者某一层有很多文件夹时,逐层深入会变得不受欢迎。 - mistika
26
这个问题特别涉及如何使用"打开文件对话框"(OFD)来选择一个文件夹,而不是"浏览文件夹对话框"(FBD)。我同意,从用户的角度来看,FBD非常糟糕。 - Michael Paulukonis
44
除了这个带有破损用户界面的对话框,还可以使用一个CommonOpenFileDialognew CommonOpenFileDialog { IsFolderPicker = true }。请注意,我已经尽力使翻译通俗易懂,并保持了原文的意思,没有提供额外的解释。 - ANeves
177
请千万不要使用它!我记得作为用户,我曾指责这些可怜的程序员又做了一个带有可怕“树形视图对话框”(实际上只是FolderBrowserDialog)的应用程序。它完全无法使用:一堆根目录,缺少收藏夹面板,最糟糕的是——甚至无法在其中粘贴路径!现在,作为一名程序员,我看到一个建议使用它...请不要这样做。 - Hi-Angel
12
请注意,FolderBrowserDialog 在幕后使用 SHBrowseForFolder。该函数的文档明确指出:“对于 Windows Vista 或更高版本,建议您使用带有 FOS_PICKFOLDERS 选项的 IFileDialog 而不是 SHBrowseForFolder 函数。这将在选择文件夹模式下使用“打开文件”对话框,并且是首选实现。”因此,除了所有可用性问题之外,自从2006年发布Windows Vista以来,它已经不是推荐的解决方案了! - Herohtar
显示剩余4条评论

461
作为给未来希望避免使用 FolderBrowserDialog 的用户的提示,Microsoft曾经发布了一个名为WindowsAPICodePack的API,其中包含了一个称为CommonOpenFileDialog的有用对话框,可以设置为IsFolderPicker模式。该API可在Microsoft的NuGet包中获得。
这就是我所需要安装和使用CommonOpenFileDialog的全部内容。(NuGet会处理依赖项)
Install-Package Microsoft.WindowsAPICodePack-Shell

对于包含(include)语句:

using Microsoft.WindowsAPICodePack.Dialogs;

使用方法:

CommonOpenFileDialog dialog = new CommonOpenFileDialog();
dialog.InitialDirectory = "C:\\Users";
dialog.IsFolderPicker = true;
if (dialog.ShowDialog() == CommonFileDialogResult.Ok)
{
    MessageBox.Show("You selected: " + dialog.FileName);
}

62
我认为这是关于“如何使用OpenFileDialog选择文件夹”的最接近的答案。FolderBrowseDialog非常难用。感谢您提供这个答案。 - Koray
61
这应该是答案。请注意,在使用CommonOpenFileDialog之前,您需要通过NuGet安装Microsoft.WindowsAPICodePack.Shell包。 - smwikipedia
4
如果你在 VS2017 中使用这个,它会重新设计你的主窗口样式。 - Lokiare
17
微软似乎已将其重新发布为 WindowsAPICodePack-Shell - NucS
7
在.NET 5中,我试过使用链接的NuGet包,但它无法加载程序集。不过,使用Microsoft-WindowsAPICodePack-Shell是可行的。 - Dan
显示剩余9条评论

70

这是一个纯C#版本,不需要NuGet,适用于所有版本的.NET(包括.NET Core、.NET 5、WPF、Winforms等),并使用Windows Vista(及更高版本)的IFileDialog接口和FOS_PICKFOLDERS选项,因此具有良好的文件夹选择器 Windows 标准 UI。

更新2023/3/17: 现在该类支持多选。

我还添加了WPF的 Window 类型支持,但这是可选的,可以删除带有WPF标记的行。

用法:

var dlg = new FolderPicker();
dlg.InputPath = @"c:\windows\system32";
if (dlg.ShowDialog() == true)
{
    MessageBox.Show(dlg.ResultPath);
}

代码:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Windows;
using System.Windows.Interop;

public class FolderPicker
{
    private readonly List<string> _resultPaths = new List<string>();
    private readonly List<string> _resultNames = new List<string>();

    public IReadOnlyList<string> ResultPaths => _resultPaths;
    public IReadOnlyList<string> ResultNames => _resultNames;
    public string ResultPath => ResultPaths.FirstOrDefault();
    public string ResultName => ResultNames.FirstOrDefault();
    public virtual string InputPath { get; set; }
    public virtual bool ForceFileSystem { get; set; }
    public virtual bool Multiselect { get; set; }
    public virtual string Title { get; set; }
    public virtual string OkButtonLabel { get; set; }
    public virtual string FileNameLabel { get; set; }

    protected virtual int SetOptions(int options)
    {
        if (ForceFileSystem)
        {
            options |= (int)FOS.FOS_FORCEFILESYSTEM;
        }

        if (Multiselect)
        {
            options |= (int)FOS.FOS_ALLOWMULTISELECT;
        }
        return options;
    }

    // for WPF support
    public bool? ShowDialog(Window owner = null, bool throwOnError = false)
    {
        owner = owner ?? Application.Current?.MainWindow;
        return ShowDialog(owner != null ? new WindowInteropHelper(owner).Handle : IntPtr.Zero, throwOnError);
    }

    // for all .NET
    public virtual bool? ShowDialog(IntPtr owner, bool throwOnError = false)
    {
        var dialog = (IFileOpenDialog)new FileOpenDialog();
        if (!string.IsNullOrEmpty(InputPath))
        {
            if (CheckHr(SHCreateItemFromParsingName(InputPath, null, typeof(IShellItem).GUID, out var item), throwOnError) != 0)
                return null;

            dialog.SetFolder(item);
        }

        var options = FOS.FOS_PICKFOLDERS;
        options = (FOS)SetOptions((int)options);
        dialog.SetOptions(options);

        if (Title != null)
        {
            dialog.SetTitle(Title);
        }

        if (OkButtonLabel != null)
        {
            dialog.SetOkButtonLabel(OkButtonLabel);
        }

        if (FileNameLabel != null)
        {
            dialog.SetFileName(FileNameLabel);
        }

        if (owner == IntPtr.Zero)
        {
            owner = Process.GetCurrentProcess().MainWindowHandle;
            if (owner == IntPtr.Zero)
            {
                owner = GetDesktopWindow();
            }
        }

        var hr = dialog.Show(owner);
        if (hr == ERROR_CANCELLED)
            return null;

        if (CheckHr(hr, throwOnError) != 0)
            return null;

        if (CheckHr(dialog.GetResults(out var items), throwOnError) != 0)
            return null;

        items.GetCount(out var count);
        for (var i = 0; i < count; i++)
        {
            items.GetItemAt(i, out var item);
            CheckHr(item.GetDisplayName(SIGDN.SIGDN_DESKTOPABSOLUTEPARSING, out var path), throwOnError);
            CheckHr(item.GetDisplayName(SIGDN.SIGDN_DESKTOPABSOLUTEEDITING, out var name), throwOnError);
            if (path != null || name != null)
            {
                _resultPaths.Add(path);
                _resultNames.Add(name);
            }
        }
        return true;
    }

    private static int CheckHr(int hr, bool throwOnError)
    {
        if (hr != 0 && throwOnError) Marshal.ThrowExceptionForHR(hr);
        return hr;
    }

    [DllImport("shell32")]
    private static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string pszPath, IBindCtx pbc, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IShellItem ppv);

    [DllImport("user32")]
    private static extern IntPtr GetDesktopWindow();

#pragma warning disable IDE1006 // Naming Styles
    private const int ERROR_CANCELLED = unchecked((int)0x800704C7);
#pragma warning restore IDE1006 // Naming Styles

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

    [ComImport, Guid("d57c7288-d4ad-4768-be02-9d969532d960"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IFileOpenDialog
    {
        [PreserveSig] int Show(IntPtr parent); // IModalWindow
        [PreserveSig] int SetFileTypes();  // not fully defined
        [PreserveSig] int SetFileTypeIndex(int iFileType);
        [PreserveSig] int GetFileTypeIndex(out int piFileType);
        [PreserveSig] int Advise(); // not fully defined
        [PreserveSig] int Unadvise();
        [PreserveSig] int SetOptions(FOS fos);
        [PreserveSig] int GetOptions(out FOS pfos);
        [PreserveSig] int SetDefaultFolder(IShellItem psi);
        [PreserveSig] int SetFolder(IShellItem psi);
        [PreserveSig] int GetFolder(out IShellItem ppsi);
        [PreserveSig] int GetCurrentSelection(out IShellItem ppsi);
        [PreserveSig] int SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName);
        [PreserveSig] int GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName);
        [PreserveSig] int SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle);
        [PreserveSig] int SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string pszText);
        [PreserveSig] int SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string pszLabel);
        [PreserveSig] int GetResult(out IShellItem ppsi);
        [PreserveSig] int AddPlace(IShellItem psi, int alignment);
        [PreserveSig] int SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension);
        [PreserveSig] int Close(int hr);
        [PreserveSig] int SetClientGuid();  // not fully defined
        [PreserveSig] int ClearClientData();
        [PreserveSig] int SetFilter([MarshalAs(UnmanagedType.IUnknown)] object pFilter);
        [PreserveSig] int GetResults(out IShellItemArray ppenum);
        [PreserveSig] int GetSelectedItems([MarshalAs(UnmanagedType.IUnknown)] out object ppsai);
    }

    [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IShellItem
    {
        [PreserveSig] int BindToHandler(); // not fully defined
        [PreserveSig] int GetParent(); // not fully defined
        [PreserveSig] int GetDisplayName(SIGDN sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName);
        [PreserveSig] int GetAttributes();  // not fully defined
        [PreserveSig] int Compare();  // not fully defined
    }

    [ComImport, Guid("b63ea76d-1f85-456f-a19c-48159efa858b"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    private interface IShellItemArray
    {
        [PreserveSig] int BindToHandler();  // not fully defined
        [PreserveSig] int GetPropertyStore();  // not fully defined
        [PreserveSig] int GetPropertyDescriptionList();  // not fully defined
        [PreserveSig] int GetAttributes();  // not fully defined
        [PreserveSig] int GetCount(out int pdwNumItems);
        [PreserveSig] int GetItemAt(int dwIndex, out IShellItem ppsi);
        [PreserveSig] int EnumItems();  // not fully defined
    }

#pragma warning disable CA1712 // Do not prefix enum values with type name
    private enum SIGDN : uint
    {
        SIGDN_DESKTOPABSOLUTEEDITING = 0x8004c000,
        SIGDN_DESKTOPABSOLUTEPARSING = 0x80028000,
        SIGDN_FILESYSPATH = 0x80058000,
        SIGDN_NORMALDISPLAY = 0,
        SIGDN_PARENTRELATIVE = 0x80080001,
        SIGDN_PARENTRELATIVEEDITING = 0x80031001,
        SIGDN_PARENTRELATIVEFORADDRESSBAR = 0x8007c001,
        SIGDN_PARENTRELATIVEPARSING = 0x80018001,
        SIGDN_URL = 0x80068000
    }

    [Flags]
    private enum FOS
    {
        FOS_OVERWRITEPROMPT = 0x2,
        FOS_STRICTFILETYPES = 0x4,
        FOS_NOCHANGEDIR = 0x8,
        FOS_PICKFOLDERS = 0x20,
        FOS_FORCEFILESYSTEM = 0x40,
        FOS_ALLNONSTORAGEITEMS = 0x80,
        FOS_NOVALIDATE = 0x100,
        FOS_ALLOWMULTISELECT = 0x200,
        FOS_PATHMUSTEXIST = 0x800,
        FOS_FILEMUSTEXIST = 0x1000,
        FOS_CREATEPROMPT = 0x2000,
        FOS_SHAREAWARE = 0x4000,
        FOS_NOREADONLYRETURN = 0x8000,
        FOS_NOTESTFILECREATE = 0x10000,
        FOS_HIDEMRUPLACES = 0x20000,
        FOS_HIDEPINNEDPLACES = 0x40000,
        FOS_NODEREFERENCELINKS = 0x100000,
        FOS_OKBUTTONNEEDSINTERACTION = 0x200000,
        FOS_DONTADDTORECENT = 0x2000000,
        FOS_FORCESHOWHIDDEN = 0x10000000,
        FOS_DEFAULTNOMINIMODE = 0x20000000,
        FOS_FORCEPREVIEWPANEON = 0x40000000,
        FOS_SUPPORTSTREAMABLEITEMS = unchecked((int)0x80000000)
    }
#pragma warning restore CA1712 // Do not prefix enum values with type name
}

结果:

enter image description here


10
最佳答案明显! - Insert Clever Username
5
太棒了!谢谢你! - gurrenm3
3
@LesFerch,应该像这样写:if (dlg.ShowDialog(yourForm.Handle) == true)或者if (dlg.ShowDialog(someControl.Handle) == true),否则用户可能会将焦点放在其中一个窗体/窗口上(即弹出浏览对话框的窗体之外的另一个窗口)。 - Maris B.
1
这段代码帮助我在 C++ 中实现了同样的想法。 - Nick Deguillaume
2
真是太神了!你是已经有这段代码准备好了还是确实用了10分钟来进行改进?如果是后者,那么你的功夫确实了得。我要花费更长时间来进行集成和测试。顺便说一句,它完美地运行了!谢谢! - LesFerch
显示剩余10条评论

64

使用OpenFileDialog的一个巧妙解决方案是将ValidateNamesCheckFileExists都设置为false,然后给FileName赋予一个模拟值以指示选择了目录。

我说这是巧妙的,因为它让用户对如何选择文件夹感到困惑。他们需要在所需文件夹中,然后只需按“打开”按钮,尽管文件名显示“选择文件夹”。

C# Folder selection dialog

这是基于Denis Stankovski的同一对话框中选择文件或文件夹的文章。

OpenFileDialog folderBrowser = new OpenFileDialog();
// Set validate names and check file exists to false otherwise windows will
// not let you select "Folder Selection."
folderBrowser.ValidateNames = false;
folderBrowser.CheckFileExists = false;
folderBrowser.CheckPathExists = true;
// Always default to Folder Selection.
folderBrowser.FileName = "Folder Selection.";
if (folderBrowser.ShowDialog() == DialogResult.OK)
{
    string folderPath = Path.GetDirectoryName(folderBrowser.FileName);
    // ...
}

1
这对我不起作用。它不允许我选择文件夹。它只是打开它们。 - Lokiare
2
@Lokiare 当我说这是一个hack时,我的意思就是这个。请看第二段的说明。 - Daniel Ballinger
2
@ComradeJoecool 你不必手动删除那个。这就是代码中最后一行的作用:string folderPath = Path.GetDirectoryName(folderBrowser.FileName); - Heriberto Lugo
2
哦!这个方法有问题:如果用户在浏览文件夹时按下“上”或“返回”按钮,对话框的主“打开”按钮将无法按预期工作!它会导致您跳回到上一个文件夹!但是,当您只需双击文件夹以选择它们或选择每个文件夹中的某些文件(如果有任何可选文件)时,它可以正常工作。 - S.Serpooshan
这可能很有用:folderBrowser.Title = "选择目录"; folderBrowser.InitialDirectory = iInitialDirectory; - Roberto Mutti
显示剩余2条评论

13

奇怪的是有这么多的回答/投票,但没有人将以下代码作为答案添加:

using (var opnDlg = new OpenFileDialog()) //ANY dialog
{ 
    //opnDlg.Filter = "Png Files (*.png)|*.png";
    //opnDlg.Filter = "Excel Files (*.xls, *.xlsx)|*.xls;*.xlsx|CSV Files (*.csv)|*.csv"

    if (opnDlg.ShowDialog() == DialogResult.OK)
    {
        //opnDlg.SelectedPath -- your result
    }
}

12
你的回答和@Ionică Bizău的回答有什么不同?(原文链接:https://dev59.com/EWgu5IYBdhLWcg3wDS0G#11624322) - Chetan Mehra
2
逻辑相同,但我的答案更短,您不需要为DialogResult创建额外的变量。 - Andrew_STOP_RU_WAR_IN_UA
10
如前所述,该方法存在一些大问题:这是一个糟糕的树形视图对话框!你无法将路径复制粘贴到其中,必须逐级从根文件夹进行查找,并且没有收藏夹面板! - S.Serpooshan
4
关于你对"S.Serpooshan"的评论回复,实际上确实很重要。根据"Michael Paulukonis"在"Apr 28 '15 at 15:09"(在你的回答之前大约1.5年)对"Ionică Bizău"的回答中所说:“问题是关于使用OpenFileDialog (OFD)来选择文件夹,而不是FolderBrowserDialog(FBD)。我同意从用户角度来看,FBD非常糟糕。” (强调添加)。 此外,“Joe”在“Jan 6 '17 at 18:03”(在你的评论之前大约2.5年)已经提供了正是OP所要求的内容(即使用OFD的所有功能进行文件夹选择而不是FBD)。 - Tom
不要采用这个答案,显然是无效的。 - Dr.Escher
显示剩余2条评论

11

14
我猜这个被 downvote 是因为 (正如 mistika 已经指出的那样) FolderBrowserDialog 的可用性很差,而且 OP 明确想使用 OpenFileDialog。 - mbx
3
@mbx 或许吧。公平地说,原帖并没有说“我只能使用 OpenFileDialog”。在我最初回答这个问题时(已经是4年半前的事了...),我们假设OP只是不知道如何让用户打开文件夹。我发表完回答后就没有再回来看过这个问题,所以我没有看到任何关于可用性的讨论,也没有在回答时考虑过这个问题。 - Simon Whitehead

9

注意:Ookii.Dialogs 需要 Microsoft .NET Framework 4.5 或更高版本。(无法在 WinXP 中使用) - S.Serpooshan
8
@S.Serpooshan -- 我猜我的 Windows 3.1电脑也无法运行它,对吧?但说真的,现在已经是2018年了,没人应该考虑 Windows XP 了,因为它早已死亡。 - rory.ap
@rory.ap 实际上,对于我来说,这个解决方案的主要问题是在浏览文件夹时它不显示文件。当用户想要选择文件夹时,能够查看文件(例如要处理的图像)有时非常有用! - S.Serpooshan
1
不幸的是,如果禁用了 Application.VisualStyleState,这种方法就行不通了:Application.VisualStyleState = System.Windows.Forms.VisualStyles.VisualStyleState.NoneEnabled;。你会遇到 这个问题... - Rotax
@Rotax 我很想了解更多 - 例如,为什么您需要首先禁用 VisualStyleState,是否存在有效的用例,也许可以调查是否有解决方法。您是否想在存储库中打开一个问题?https://github.com/augustoproiete/ookii-dialogs-winforms/issues - C. Augusto Proiete
1
@AugustoProiete 当然,禁用 VisualStyleState 的原因是它使得在拥有大量控件的 WinForms 表单上的实际速度更快。请参阅我的 Stack Overflow 帖子这里。我很担心这个问题很深且长时间无法解决(如果有的话)... 我的解决方法是 FolderBrowserDialogEx,而且我希望尽快转换到 WPF。 - Rotax

9

这里有另一种解决方案,所有源代码都在一个简单的ZIP文件中。

它使用附加的窗口标志来呈现OpenFileDialog,使其像Windows 7+文件夹选择对话框一样工作。

根据该网站,它是公共领域:“没有许可证,您可以自由地获取并使用代码。”

Archive.org链接:


1
完美工作!您可以通过在“FolderSelectDialog.cs”中添加以下行来使其选择多个文件夹:public string[] FileNames { get { return ofd.FileNames; } } 并在构造函数中更改ofd.Multiselect = true; - Maxter
1
不幸的是,如果禁用了Application.VisualStyleState,这种方法就行不通了:Application.VisualStyleState = System.Windows.Forms.VisualStyles.VisualStyleState.NoneEnabled;。你会遇到这个问题... - Rotax
不幸的是,我还没有机会研究这个问题(也不会在短时间内),但是从这里得到的信息:https://medium.com/lextm/openfiledialog-crashes-with-comexception-0x80040111-f51e18d1ab89,建议设置 FileDialog.AutoUpgradeEnabled 为 false。 - Ben Keene

1

0

考虑到 OP 的问题,Simon Mourier 给出的答案应该是最好的答案。它不涉及 NuGET 包,因此在选择文件夹方法时将来不会有任何依赖性问题。

如果遇到与 "...is not available in C# 7.3" 相关的错误,请在您的 .csproj 中添加 <LangVersion>8.0</LangVersion>(在使用 Visual Studio 进行构建和运行时不会产生任何错误)

如果您无法更改项目语言,则只需将 owner ??= Application.Current.MainWindow 替换为

owner = owner ?? Application.Current.MainWindow


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接