private/dialogs/Show-ModernFolderPicker.ps1
|
function Show-ModernFolderPicker { <# .SYNOPSIS Modern folder picker using Windows Shell IFileOpenDialog COM interface. #> param( [string]$Title, [string]$InitialDirectory, [switch]$Multiselect ) # IFileOpenDialog COM interop for Vista-style folder picker (works in PS 5.1 and 7+) $showDialog = { param($dialogTitle, $initialDir, $allowMulti) # Add the COM interop types for IFileOpenDialog $comTypes = @' using System; using System.Runtime.InteropServices; namespace PsUiDialogs { [ComImport, Guid("DC1C5A9C-E88A-4dde-A5A1-60F82A20AEF7")] internal class FileOpenDialog { } [ComImport, Guid("42f85136-db7e-439c-85f1-e4075d135fc8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IFileOpenDialog { [PreserveSig] int Show(IntPtr hwndOwner); void SetFileTypes(uint cFileTypes, IntPtr rgFilterSpec); void SetFileTypeIndex(uint iFileType); void GetFileTypeIndex(out uint piFileType); void Advise(IntPtr pfde, out uint pdwCookie); void Unadvise(uint dwCookie); void SetOptions(uint fos); void GetOptions(out uint pfos); void SetDefaultFolder(IShellItem psi); void SetFolder(IShellItem psi); void GetFolder(out IShellItem ppsi); void GetCurrentSelection(out IShellItem ppsi); void SetFileName([MarshalAs(UnmanagedType.LPWStr)] string pszName); void GetFileName([MarshalAs(UnmanagedType.LPWStr)] out string pszName); void SetTitle([MarshalAs(UnmanagedType.LPWStr)] string pszTitle); void SetOkButtonLabel([MarshalAs(UnmanagedType.LPWStr)] string pszText); void SetFileNameLabel([MarshalAs(UnmanagedType.LPWStr)] string pszLabel); void GetResult(out IShellItem ppsi); void AddPlace(IShellItem psi, int fdap); void SetDefaultExtension([MarshalAs(UnmanagedType.LPWStr)] string pszDefaultExtension); void Close(int hr); void SetClientGuid(ref Guid guid); void ClearClientData(); void SetFilter(IntPtr pFilter); void GetResults(out IShellItemArray ppenum); void GetSelectedItems(out IShellItemArray ppsai); } [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IShellItem { void BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppv); void GetParent(out IShellItem ppsi); void GetDisplayName(uint sigdnName, [MarshalAs(UnmanagedType.LPWStr)] out string ppszName); void GetAttributes(uint sfgaoMask, out uint psfgaoAttribs); void Compare(IShellItem psi, uint hint, out int piOrder); } [ComImport, Guid("b63ea76d-1f85-456f-a19c-48159efa858b"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] internal interface IShellItemArray { void BindToHandler(IntPtr pbc, ref Guid bhid, ref Guid riid, out IntPtr ppvOut); void GetPropertyStore(int flags, ref Guid riid, out IntPtr ppv); void GetPropertyDescriptionList(IntPtr keyType, ref Guid riid, out IntPtr ppv); void GetAttributes(int AttribFlags, uint sfgaoMask, out uint psfgaoAttribs); void GetCount(out uint pdwNumItems); void GetItemAt(uint dwIndex, out IShellItem ppsi); void EnumItems(out IntPtr ppenumShellItems); } public static class FolderPicker { private const uint FOS_PICKFOLDERS = 0x00000020; private const uint FOS_ALLOWMULTISELECT = 0x00000200; private const uint FOS_FORCEFILESYSTEM = 0x00000040; private const uint SIGDN_FILESYSPATH = 0x80058000; [DllImport("shell32.dll", CharSet = CharSet.Unicode, PreserveSig = false)] private static extern void SHCreateItemFromParsingName( [MarshalAs(UnmanagedType.LPWStr)] string pszPath, IntPtr pbc, [MarshalAs(UnmanagedType.LPStruct)] Guid riid, out IShellItem ppv); public static string[] ShowDialog(IntPtr hwnd, string title, string initialDir, bool multiSelect) { IFileOpenDialog dialog = null; try { dialog = (IFileOpenDialog)new FileOpenDialog(); uint options = FOS_PICKFOLDERS | FOS_FORCEFILESYSTEM; if (multiSelect) options |= FOS_ALLOWMULTISELECT; dialog.SetOptions(options); if (!string.IsNullOrEmpty(title)) dialog.SetTitle(title); IShellItem initialFolder = null; if (!string.IsNullOrEmpty(initialDir) && System.IO.Directory.Exists(initialDir)) { SHCreateItemFromParsingName(initialDir, IntPtr.Zero, typeof(IShellItem).GUID, out initialFolder); if (initialFolder != null) dialog.SetFolder(initialFolder); } int hr = dialog.Show(hwnd); if (hr != 0) { if (initialFolder != null) Marshal.ReleaseComObject(initialFolder); return null; } if (multiSelect) { IShellItemArray results; dialog.GetResults(out results); uint count; results.GetCount(out count); string[] paths = new string[count]; for (uint i = 0; i < count; i++) { IShellItem item; results.GetItemAt(i, out item); string path; item.GetDisplayName(SIGDN_FILESYSPATH, out path); paths[i] = path; if (item != null) Marshal.ReleaseComObject(item); } if (results != null) Marshal.ReleaseComObject(results); if (initialFolder != null) Marshal.ReleaseComObject(initialFolder); return paths; } else { IShellItem result; dialog.GetResult(out result); string path; result.GetDisplayName(SIGDN_FILESYSPATH, out path); if (result != null) Marshal.ReleaseComObject(result); if (initialFolder != null) Marshal.ReleaseComObject(initialFolder); return new string[] { path }; } } catch { return null; } finally { if (dialog != null) Marshal.ReleaseComObject(dialog); } } } } '@ # Compile COM interop types if not loaded (SilentlyContinue handles module reload) if (!([System.Management.Automation.PSTypeName]'PsUiDialogs.FolderPicker').Type) { Add-Type -TypeDefinition $comTypes -Language CSharp -ErrorAction SilentlyContinue } $result = [PsUiDialogs.FolderPicker]::ShowDialog([IntPtr]::Zero, $dialogTitle, $initialDir, $allowMulti) if ($result -and $result.Count -gt 0) { if ($allowMulti) { return $result } else { return $result[0] } } return $null } # If we're inside a PsUi window, use its dispatcher to show the dialog $session = Get-UiSession -ErrorAction SilentlyContinue if ($session -and $session.Window) { return $session.Window.Dispatcher.Invoke([Func[object]]{ & $showDialog $Title $InitialDirectory $Multiselect.IsPresent }) } # No UI context - run in STA runspace (COM dialogs require STA) $runspace = [runspacefactory]::CreateRunspace() $runspace.ApartmentState = 'STA' $runspace.ThreadOptions = 'ReuseThread' $runspace.Open() $ps = [powershell]::Create() $ps.Runspace = $runspace [void]$ps.AddScript($showDialog) [void]$ps.AddArgument($Title) [void]$ps.AddArgument($InitialDirectory) [void]$ps.AddArgument($Multiselect.IsPresent) try { $result = $ps.Invoke() return $result } finally { $ps.Dispose() $runspace.Dispose() } } |