Microsoft's FolderBrowseDialog implementation in .Net Windows Forms is, like any other Microsoft's implementation of Windows shell or common controls, partially implemented and the part which is implemented is partially broken.
Not only it doesn't support half of the functionality of the native shell dialog, but it also doesn't initialize the control properly in case RootFolder
is set to anything other than Environment.SpecialFolder.Desktop
.
Here is the solution I wrote using marshaling which should work properly when starting from My Computer
(which is probably what most people landing on this question wanted in the first place):
//
// MIT NO-AI License
//
// Copyright © 2023 by Igor Levicki
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// Permission is not granted to use this software or any of the associated files
// as sample data for the purposes of building machine learning models.
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
namespace NativeMethods
{
using System;
using System.Runtime.InteropServices;
internal static class SHELL32
{
private delegate int BrowseCallBackProc(IntPtr hWnd, int msg, IntPtr lParam, IntPtr wParam);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct BROWSEINFO
{
public IntPtr Owner;
public IntPtr Root;
public string SelectedPath;
public string Description;
public UInt32 Flags;
public BrowseCallBackProc Callback;
public IntPtr lParam;
public Int32 Image;
}
private const Int32 MAX_PATH = 260;
private const Int32 BFFM_INITIALIZED = 1;
private const Int32 BFFM_SETEXPANDED = 0x046A;
private const string FOLDERID_ComputerFolder = "{0AC0837C-BBF8-452A-850D-79D08E667CA7}";
[Flags]
private enum BIF : UInt32
{
RETURNONLYFSDIRS = 0x00000001,
DONTGOBELOWDOMAIN = 0x00000002,
STATUSTEXT = 0x00000004,
RETURNFSANCESTORS = 0x00000008,
EDITBOX = 0x00000010,
VALIDATE = 0x00000020,
NEWDIALOGSTYLE = 0x00000040,
USENEWUI = NEWDIALOGSTYLE | EDITBOX,
BROWSEINCLUDEURLS = 0x00000080,
UAHINT = 0x00000100,
NONEWFOLDERBUTTON = 0x00000200,
NOTRANSLATETARGETS = 0x00000400,
BROWSEFORCOMPUTER = 0x00001000,
BROWSEFORPRINTER = 0x00002000,
BROWSEINCLUDEFILES = 0x00004000,
SHAREABLE = 0x00008000,
BROWSEFILEJUNCTIONS = 0x00010000
}
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr SHBrowseForFolder(ref BROWSEINFO lpbi);
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern bool SHGetPathFromIDList(IntPtr pidl, IntPtr pszPath);
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern Int32 SHILCreateFromPath(string pszPath, ref IntPtr pidl, IntPtr rgfInOut);
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern Int32 SHGetKnownFolderIDList(Guid rfid, UInt32 Flags, IntPtr Token, ref IntPtr pidl);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr SendMessage(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
private static int OnBrowseEvent(IntPtr hWnd, int msg, IntPtr lParam, IntPtr lpData)
{
switch (msg) {
case BFFM_INITIALIZED:
SendMessage(hWnd, BFFM_SETEXPANDED, IntPtr.Zero, lpData);
break;
}
return 0;
}
private static IntPtr ThisPC()
{
IntPtr pidl = IntPtr.Zero;
Int32 hr = SHGetKnownFolderIDList(new Guid(FOLDERID_ComputerFolder), 0, IntPtr.Zero, ref pidl);
return pidl;
}
private static IntPtr PIDLFromPath(string Path)
{
IntPtr pidl = IntPtr.Zero;
Int32 hr = SHILCreateFromPath(Path, ref pidl, IntPtr.Zero);
return pidl;
}
public static string SelectFolder(string Description, string InitialPath, IntPtr Owner)
{
BROWSEINFO bi = new BROWSEINFO();
bi.Owner = Owner;
bi.Description = Description;
bi.Root = ThisPC();
bi.Flags = (UInt32)(BIF.NONEWFOLDERBUTTON | BIF.USENEWUI);
bi.Callback = OnBrowseEvent;
bi.lParam = PIDLFromPath(InitialPath);
IntPtr Buffer = Marshal.AllocHGlobal(MAX_PATH * 2);
IntPtr pidl = IntPtr.Zero;
try {
pidl = SHBrowseForFolder(ref bi);
if (pidl != IntPtr.Zero && SHGetPathFromIDList(pidl, Buffer)) {
return Marshal.PtrToStringUni(Buffer);
} else {
return null;
}
} finally {
if (pidl != IntPtr.Zero) {
Marshal.FreeCoTaskMem(pidl);
}
if (bi.lParam != IntPtr.Zero) {
Marshal.FreeCoTaskMem(bi.lParam);
}
if (bi.Root != IntPtr.Zero) {
Marshal.FreeCoTaskMem(bi.Root);
}
}
}
}
}
Usage example:
// InitialPath will be selected and expanded if it exists and is accessible to the user
string InitialPath = @"D:\Downloads";
// Handle is hwnd of your owner form
string SelectedPath = SHELL32.SelectFolder("Select foobar folder", InitialPath, Handle);
// SelectedPath will contain path or null on error / cancel
Full error checking as well as support for starting from known folders other than My Computer
are left as an exercise for the reader.
Note that your program's Main()
has to be marked with [STAThread]
or the dialog will not work.
For more details about COM initialization and allowed flag combinations see SHBrowseForFolderW API documentation.