在Powershell中使用升级的FolderBrowserDialog(“Vista风格”)
我正在使用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 for C# Windows Forms 应用程序创建了一个CommonOpenFileDialogwith IsFolderPicker = true,为我OpenFileDialog提供了易于使用的托管文件夹浏览器的功能和可访问性。
在我在这里寻找使用类似内容的方法时,我了解到常规的 FolderBrowserDialog得到了升级,至少在 .Net Core 中。
呵呵,真巧。
有什么方法可以从 PowerShell 脚本访问升级版本?
添加$objForm.AutoUpgradeEnabled = $true到上面的代码不会改变任何东西(并且确实是默认值)
(另外,如果有人知道如何以更直接的方式为 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 dirDir I want[Select this folder]”的路径,即使它不存在,然后我可以修剪回“C:Some dirDir I want”。
优点:
- 全功能浏览器,根据需要
缺点:
- 文件名字段不能为空。尝试使用换行符之类的东西会导致错误对话框抱怨文件名无效并
FileOpenDialog拒绝返回文件名,即使ValidateNames是false. - 用户可以在文件名字段中输入任何内容,这可能会导致混淆。
- 当您向上浏览文件夹树时,单击接受按钮(“打开”)只会浏览回先前选择的子目录,即使您取消选择它
- 您必须修剪掉返回路径的最后一部分,并希望不会因选择不存在的文件而发生进一步的怪异
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路径
- 支持自动完成
缺点:
- 没有带有快速访问项目等的侧面板
- 无法更改视图、排序等
- 没有预览/缩略图
回答
哇,您可以在 PowerShell 中使用 C#!
环顾四周,我羡慕每个人都在 C# 中玩耍并利用我不知道如何在 PowerShell 中访问的酷功能。例如,
我喜欢这种方法,它不依赖于遗留 API,并且对不受支持的系统有一个后备。
然后我看到您可以在 PowerShell 中使用实际的 C#!我将两者放在一起,稍微修改了代码以使其更容易从 PS 调用,然后出现了一种相当轻量级的、希望强大的方法来调用用户可用的最佳文件夹浏览器对话框:
[Revised code below]
我很想听听关于整个方法可能有多可靠的意见。
无法访问某些引用或出现其他问题的可能性有多大?
无论如何,我现在对这种方法很满意:)
编辑:PowerShell“核心”(pwsh.exe;较新的程序集)
因此,正如@mklement0在评论中指出的那样,积极开发的PowerShell(以前(以及以后,为了可读性)被称为“PowerShell Core”;与 Windows 附带的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)