【问题标题】:Use the upgraded FolderBrowserDialog ("Vista style") in Powershell在 Powershell 中使用升级后的 FolderBrowserDialog(“Vista 风格”)
【发布时间】:2021-06-23 16:11:11
【问题描述】:

我正在使用 PowerShell 让用户能够浏览 Node.js 应用程序的文件/文件夹路径(因为到目前为止我还没有找到更好的轻量级替代方案),并且我在处理不支持的可怕、糟糕的可用性FolderBrowserDialog 时遇到了老麻烦:

  • 过去的路径
  • 访问快速访问项目
  • 改变视图
  • 排序或过滤项目
  • 等等……

标准脚本如下所示:

Function Select-FolderDialog($Description="Select Folder", $RootFolder="MyComputer"){
    [System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null     
        
    $objForm = New-Object System.Windows.Forms.FolderBrowserDialog
    $objForm.RootFolder = $RootFolder
    $objForm.ShowNewFolderButton = $true
    $objForm.Description = "Please choose a folder"
    $Show = $objForm.ShowDialog()
    If ($Show -eq "OK")
    {
        Return $objForm.SelectedPath
    }
    Else
    {
        Write-Error "Operation cancelled by user."
    }
}
$folder = Select-FolderDialog
write-host $folder

我过去曾将Windows API CodePack 用于C# Windows 窗体应用程序,以使用IsFolderPicker = true 创建CommonOpenFileDialog,为我提供OpenFileDialog 的功能和可访问性以及易于使用的托管文件夹浏览器。

在我在这里寻找使用类似方法的方法时,我了解到常规的 FolderBrowserDialog got an upgrade, at least in .Net Core
嗯,不错哦。

我有什么方法可以从 PowerShell 脚本访问升级版本

在上面的代码中添加$objForm.AutoUpgradeEnabled = $true 不会改变任何东西(实际上是the default
(另外,如果有人知道如何以更直接的方式为 Node.js 应用程序提供一个像样的文件夹浏览器对话框,请给我留言^^)

目前的解决方法:

1:滥用 OpenFileDialog

Function Select-FolderDialog($Description="Select Folder", $RootFolder="MyComputer"){
    [System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms") | Out-Null     

    $objForm = New-Object System.Windows.Forms.OpenFileDialog
    $objForm.DereferenceLinks = $true
    $objForm.CheckPathExists = $true
    $objForm.FileName = "[Select this folder]"
    $objForm.Filter = "Folders|`n"
    $objForm.AddExtension = $false
    $objForm.ValidateNames = $false
    $objForm.CheckFileExists = $false
    $Show = $objForm.ShowDialog()
    If ($Show -eq "OK")
    {
        Return $objForm.FileName
    }
    Else
    {
        Write-Error "Operation cancelled by user."
    }
}

$folder = Select-FolderDialog
write-host $folder

这会创建一个继承自更好的FileDialog Class 的对话框,该对话框仅显示文件夹并允许您返回类似“C:\Some dir\Dir I want\[Select this folder]”之类的路径,即使它没有'不存在,然后我可以将其修剪回“C:\Some dir\Dir I want”。

优点:

  • 全功能浏览器,根据需要

缺点:

  • 文件名字段不能为空。尝试使用类似的东西 换行符导致错误对话框抱怨 无效的文件名和FileOpenDialog 拒绝返回 文件名,即使 ValidateNamesfalse
  • 用户可以在文件名字段中输入任何内容,这可能会导致混淆。
  • 当您向上浏览文件夹树时,单击接受按钮(“打开”)只会浏览回之前选择的子目录,即使您取消选择它也是如此
  • 您必须修剪掉返回路径的最后一部分,并希望不会因为选择不存在的文件而发生进一步的怪异事件

2:给标准对话框一个编辑控件

使用Shell.BrowseForFolder 方法

# Included the options values from https://docs.microsoft.com/en-us/windows/win32/api/shlobj_core/ns-shlobj_core-browseinfoa
$BIF_RETURNONLYFSDIRS = [uint32]"0x00000001"
$BIF_DONTGOBELOWDOMAIN = [uint32]"0x00000002"
$BIF_STATUSTEXT = [uint32]"0x00000004"
$BIF_RETURNFSANCESTORS = [uint32]"0x00000008"
$BIF_EDITBOX = [uint32]"0x00000010" # <-- this is the important one
$BIF_VALIDATE = [uint32]"0x00000020"
$BIF_NEWDIALOGSTYLE = [uint32]"0x00000040" # <-- this sounds nice, but somehow changes nothing
$BIF_BROWSEINCLUDEURLS = [uint32]"0x00000080"
$BIF_USENEWUI = $BIF_NEWDIALOGSTYLE
$BIF_UAHINT = [uint32]"0x00000100"
$BIF_NONEWFOLDERBUTTON = [uint32]"0x00000200"
$BIF_NOTRANSLATETARGETS = [uint32]"0x00000400"
$BIF_BROWSEFORCOMPUTER = [uint32]"0x00001000"
$BIF_BROWSEFORPRINTER = [uint32]"0x00002000"
$BIF_BROWSEINCLUDEFILES = [uint32]"0x00004000"
$BIF_SHAREABLE = [uint32]"0x00008000"
$BIF_BROWSEFILEJUNCTIONS = [uint32]"0x00010000"

$options = 0
$options += $BIF_STATUSTEXT
$options += $BIF_EDITBOX
$options += $BIF_VALIDATE
$options += $BIF_NEWDIALOGSTYLE
$options += $BIF_BROWSEINCLUDEURLS
$options += $BIF_SHAREABLE
$options += $BIF_BROWSEFILEJUNCTIONS

$shell = new-object -comobject Shell.Application
$folder = $shell.BrowseForFolder(0, "Select a folder", $options)
if($folder){
    write-host $folder.Self.Path()
}

为了清楚起见,我包含了选项,但您可以将上述所有内容硬编码到 $folder = $shell.BrowseForFolder(0, "Select a folder", 98548),这很简洁。

优点:

  • 按预期使用文件夹浏览器对话框
  • 强大的用户体验
  • 可以粘贴路径
  • 支持 UNC 路径
  • 支持自动完成

缺点:

  • 没有带有快速访问项目等的侧面板
  • 无法更改视图、排序等
  • 没有预览/缩略图

【问题讨论】:

  • 您拥有的磁盘存储量令人印象深刻
  • @stuartd :D 我不幸遭遇了想要保存所有内容对不完整的事情的FOMO。拥有一切感觉很好,但它会变得昂贵,尤其是当您自己进行备份时>:(

标签: .net winforms powershell


【解决方案1】:

哇,你可以在 PowerShell 中使用 C#!

环顾四周,我羡慕每个在 C# 中玩耍并利用我不知道如何在 PowerShell 中访问的酷炫功能的人。
例如,我喜欢 this approach,它不依赖旧版 API,并且可以回退到不受支持的系统。
然后我看到您可以在 PowerShell 中使用实际的 C#! 我将两者放在一起,稍微修改了代码以使其更容易从 PS 中调用,结果出现了一种相当轻量级、希望稳健的方式来召唤用户可用的最佳文件夹浏览器对话框:

[Revised code below]

我很想听听有关整个方法可能有多可靠的意见。
某些参考资料无法访问或出现其他问题的可能性有多大?

无论如何,我现在对这种方法很满意 :)

编辑:PowerShell“核心”(pwsh.exe;较新的程序集)

因此,正如@mklement0 在 cmets 中指出的那样,积极开发的 PowerShell(以前(为了便于阅读,为了便于阅读)称为“PowerShell Core”;与 Windows PowerShell)似乎不能很好地使用它。 在查看了 PS Core 无用地仅报告为“The type initializer for 'VistaDialog' threw an exception.”的内容(并添加了对 System.ComponentModel.Primitives 的引用)之后,事实证明 PS Core 倾向于使用更新版本的 System.Windows.Forms,在我的情况下是 5.0.4.0,它不包含类型FileDialogNative,更不用说它的嵌套类型IFileDialog
我试图强制它使用 Windows PS 引用的版本 (4.0.0.0),但它不符合。

好吧,我终于得到了一分钱,让 PS Core 简单地使用默认对话框,这已经是我一开始就想要的升级版 ?

因此,随着引入带有可选参数的构造函数方法的概念变化,我为失败的“Vista 对话框”添加了一个后备。
我没有检查 PS 版本或单个程序集和/或类/类型,而是简单地将调用包装在 try/catch 块中。

$path = $args[0]
$title = $args[1]
$message = $args[2]

$source = @'
using System;
using System.Diagnostics;
using System.Reflection;
using System.Windows.Forms;
/// <summary>
/// Present the Windows Vista-style open file dialog to select a folder. Fall back for older Windows Versions
/// </summary>
#pragma warning disable 0219, 0414, 0162
public class FolderSelectDialog {
    private string _initialDirectory;
    private string _title;
    private string _message;
    private string _fileName = "";
    
    public string InitialDirectory {
        get { return string.IsNullOrEmpty(_initialDirectory) ? Environment.CurrentDirectory : _initialDirectory; }
        set { _initialDirectory = value; }
    }
    public string Title {
        get { return _title ?? "Select a folder"; }
        set { _title = value; }
    }
    public string Message {
        get { return _message ?? _title ?? "Select a folder"; }
        set { _message = value; }
    }
    public string FileName { get { return _fileName; } }

    public FolderSelectDialog(string defaultPath="MyComputer", string title="Select a folder", string message=""){
        InitialDirectory = defaultPath;
        Title = title;
        Message = message;
    }
    
    public bool Show() { return Show(IntPtr.Zero); }

    /// <param name="hWndOwner">Handle of the control or window to be the parent of the file dialog</param>
    /// <returns>true if the user clicks OK</returns>
    public bool Show(IntPtr? hWndOwnerNullable=null) {
        IntPtr hWndOwner = IntPtr.Zero;
        if(hWndOwnerNullable!=null)
            hWndOwner = (IntPtr)hWndOwnerNullable;
        if(Environment.OSVersion.Version.Major >= 6){
            try{
                var resulta = VistaDialog.Show(hWndOwner, InitialDirectory, Title, Message);
                _fileName = resulta.FileName;
                return resulta.Result;
            }
            catch(Exception){
                var resultb = ShowXpDialog(hWndOwner, InitialDirectory, Title, Message);
                _fileName = resultb.FileName;
                return resultb.Result;
            }
        }
        var result = ShowXpDialog(hWndOwner, InitialDirectory, Title, Message);
        _fileName = result.FileName;
        return result.Result;
    }

    private struct ShowDialogResult {
        public bool Result { get; set; }
        public string FileName { get; set; }
    }

    private static ShowDialogResult ShowXpDialog(IntPtr ownerHandle, string initialDirectory, string title, string message) {
        var folderBrowserDialog = new FolderBrowserDialog {
            Description = message,
            SelectedPath = initialDirectory,
            ShowNewFolderButton = true
        };
        var dialogResult = new ShowDialogResult();
        if (folderBrowserDialog.ShowDialog(new WindowWrapper(ownerHandle)) == DialogResult.OK) {
            dialogResult.Result = true;
            dialogResult.FileName = folderBrowserDialog.SelectedPath;
        }
        return dialogResult;
    }

    private static class VistaDialog {
        private const string c_foldersFilter = "Folders|\n";
        
        private const BindingFlags c_flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
        private readonly static Assembly s_windowsFormsAssembly = typeof(FileDialog).Assembly;
        private readonly static Type s_iFileDialogType = s_windowsFormsAssembly.GetType("System.Windows.Forms.FileDialogNative+IFileDialog");
        private readonly static MethodInfo s_createVistaDialogMethodInfo = typeof(OpenFileDialog).GetMethod("CreateVistaDialog", c_flags);
        private readonly static MethodInfo s_onBeforeVistaDialogMethodInfo = typeof(OpenFileDialog).GetMethod("OnBeforeVistaDialog", c_flags);
        private readonly static MethodInfo s_getOptionsMethodInfo = typeof(FileDialog).GetMethod("GetOptions", c_flags);
        private readonly static MethodInfo s_setOptionsMethodInfo = s_iFileDialogType.GetMethod("SetOptions", c_flags);
        private readonly static uint s_fosPickFoldersBitFlag = (uint) s_windowsFormsAssembly
            .GetType("System.Windows.Forms.FileDialogNative+FOS")
            .GetField("FOS_PICKFOLDERS")
            .GetValue(null);
        private readonly static ConstructorInfo s_vistaDialogEventsConstructorInfo = s_windowsFormsAssembly
            .GetType("System.Windows.Forms.FileDialog+VistaDialogEvents")
            .GetConstructor(c_flags, null, new[] { typeof(FileDialog) }, null);
        private readonly static MethodInfo s_adviseMethodInfo = s_iFileDialogType.GetMethod("Advise");
        private readonly static MethodInfo s_unAdviseMethodInfo = s_iFileDialogType.GetMethod("Unadvise");
        private readonly static MethodInfo s_showMethodInfo = s_iFileDialogType.GetMethod("Show");

        public static ShowDialogResult Show(IntPtr ownerHandle, string initialDirectory, string title, string description) {
            var openFileDialog = new OpenFileDialog {
                AddExtension = false,
                CheckFileExists = false,
                DereferenceLinks = true,
                Filter = c_foldersFilter,
                InitialDirectory = initialDirectory,
                Multiselect = false,
                Title = title
            };

            var iFileDialog = s_createVistaDialogMethodInfo.Invoke(openFileDialog, new object[] { });
            s_onBeforeVistaDialogMethodInfo.Invoke(openFileDialog, new[] { iFileDialog });
            s_setOptionsMethodInfo.Invoke(iFileDialog, new object[] { (uint) s_getOptionsMethodInfo.Invoke(openFileDialog, new object[] { }) | s_fosPickFoldersBitFlag });
            var adviseParametersWithOutputConnectionToken = new[] { s_vistaDialogEventsConstructorInfo.Invoke(new object[] { openFileDialog }), 0U };
            s_adviseMethodInfo.Invoke(iFileDialog, adviseParametersWithOutputConnectionToken);

            try {
                int retVal = (int) s_showMethodInfo.Invoke(iFileDialog, new object[] { ownerHandle });
                return new ShowDialogResult {
                    Result = retVal == 0,
                    FileName = openFileDialog.FileName
                };
            }
            finally {
                s_unAdviseMethodInfo.Invoke(iFileDialog, new[] { adviseParametersWithOutputConnectionToken[1] });
            }
        }
    }

    // Wrap an IWin32Window around an IntPtr
    private class WindowWrapper : IWin32Window {
        private readonly IntPtr _handle;
        public WindowWrapper(IntPtr handle) { _handle = handle; }
        public IntPtr Handle { get { return _handle; } }
    }
    
    public string getPath(){
        if (Show()){
            return FileName;
        }
        return "";
    }
}
'@
Add-Type -Language CSharp -TypeDefinition $source -ReferencedAssemblies ("System.Windows.Forms", "System.ComponentModel.Primitives")
([FolderSelectDialog]::new($path, $title, $message)).getPath()

这应该适用于 Windows PowerShell(最终版本 ~5.1)和当前的 PS“核心”(pwsh.exe ~7.1.3

【讨论】:

  • 做得很好。由于您不需要字符串插值,我建议使用single-quoted here-string,以防止PowerShell 意外扩展$ 前缀标记。 ` -Language CSharp ` 是 default (坦率地说,我还没有看到基于非 C# 的示例),唯一需要明确引用的程序集是 -ReferencedAssemblies System.Windows.Forms
  • 至于可靠性:编译基于 PowerShell 版本底层的 .NET 框架,随着时间的推移而变化,但不再适用于基于 . NET Framework - 两者都不会看到新版本(关键错误修复除外)。相比之下,基于 .NET Core / .NET 5+ 的跨平台版本 PowerShell (Core) 将看到版本,不幸的是,您的代码在那里工作:它失败并显示@987654340 @,出于我不知道的原因。
  • @mklement0 哇,太有用了!而且我什至不知道有 PS Core。我会尝试添加一个构造方法,因为原来没有。我不想改变太多,但现在我将取消我添加的一次性方法,而是使用适当的构造函数重新组织设置。感谢您的洞察力和测试!
  • @mklement0 呃,所以...原来 PS Core 喜欢引用更新版本的 System.Windows.Forms,它不再包含 FileDialogNative,更不用说它的嵌套类型 IFileDialog。好消息是:它没有,因为它已经使用升级后的文件夹浏览器对话框我在 :D ...这需要几个小时的调试并试图强制 PSC 使用旧的引用如果没有^^,我本来可以做到的
  • 已更新。感谢您提供进一步的见解,我正在学习很多关于 PowerShell 的知识:)
猜你喜欢
  • 2010-11-30
  • 1970-01-01
  • 2011-05-26
  • 1970-01-01
  • 2010-11-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-21
相关资源
最近更新 更多