PowerShell Windows 表单包装器

PowerShell Windows Forms Wrapper

在 PowerShell 中,使用 Windows Forms 为小型 cmdlet 构建用户界面是很常见的,但为此所需的语法通常部分冗余且冗长。这就引出了一个问题: 有没有一种方法可以最大限度地减少所需的代码,或者是否存在用于 PowerShell 的 Windows Forms 包装器来减少冗长和冗余的语法?
我不是在寻找 ShowUI as this solution is too heavy considering it based on Windows Presentation Foundation (see also: WPF vs WinForms) 以及它涉及 PowerShell 模块 的事实,这使得它比包装函数更难部署。

在很多情况下,不需要包装器来使您的代码不那么冗长,例如冗长的 WinForms PowerShell 脚本 here。像这样的代码片段:

$System_Windows_Forms_Padding = New-Object System.Windows.Forms.Padding
$System_Windows_Forms_Padding.All = 3
$System_Windows_Forms_Padding.Bottom = 3
$System_Windows_Forms_Padding.Left = 3
$System_Windows_Forms_Padding.Right = 3
$System_Windows_Forms_Padding.Top = 3
$Tab1.Padding = $System_Windows_Forms_Padding

可以在 WinForms 中轻松简化为一行:

$Tab1.Padding = 3

如果每边的填充不同,PowerShell 将自动转换:

$Tab1.Padding = "4, 6, 4, 6"

注意: PowerShell 不转换 $Tab1.Padding = "3"$Tab1.Padding = "4, 6"

然而,创建 windows 表单控件的本机方法远非 DRY (don't repeat yourself) programming. Although (multiple) properties can be added at creation (using:New-Object System.Windows.Forms.Button -Property @{Location = "75, 120"; Size = "75, 23"}) , multiple properties can't be set right away at a later state. Above that, it isn't quick and easy to add events1, child controls and container properties (as e.g. RowSpan), or any combination, intermediately at creation of a windows form control. Bottom line, you have to reference the windows form control over and over again to set its properties and more (with e.g. $OKButton.<property> = ... as in this example) :

$OKButton = New-Object System.Windows.Forms.Button
$OKButton.Location = New-Object System.Drawing.Point(75,120)
$OKButton.Size = New-Object System.Drawing.Size(75,23)
$OKButton.Text = "OK"

这就是为什么我创建了一个可重复使用的 PowerShell Form Control 包装器,它可以让您最大限度地减少 Windows Forms (WinForms) 代码的本质。

1) 除非您使用 On<event> 方法,另请参阅:addEventListener vs onclick

PowerShell Form-Control Wrapper

Function Form-Control {
    [CmdletBinding(DefaultParametersetName='Self')]param(
        [Parameter(Position = 0)]$Control = "Form",
        [Parameter(Position = 1)][HashTable]$Member = @{},
        [Parameter(ParameterSetName = 'AttachChild',  Mandatory = $false)][Windows.Forms.Control[]]$Add = @(),
        [Parameter(ParameterSetName = 'AttachParent', Mandatory = $false)][HashTable]$Set = @{},
        [Parameter(ParameterSetName = 'AttachParent', Mandatory = $false)][Alias("Parent")][Switch]$GetParent,
        [Parameter(ParameterSetName = 'AttachParent', Mandatory = $true, ValueFromPipeline = $true)][Windows.Forms.Control]$Container
    )
    If ($Control -isnot [Windows.Forms.Control]) {Try {$Control = New-Object Windows.Forms.$Control} Catch {$PSCmdlet.WriteError($_)}}
    $Styles = @{RowStyles = "RowStyle"; ColumnStyles = "ColumnStyle"}
    ForEach ($Key in $Member.Keys) {
        If ($Style = $Styles.$Key) {[Void]$Control.$Key.Clear()
            For ($i = 0; $i -lt $Member.$Key.Length; $i++) {[Void]$Control.$Key.Add((New-Object Windows.Forms.$Style($Member.$Key[$i])))}
        } Else {
            Switch (($Control | Get-Member $Key).MemberType) {
                "Property"  {$Control.$Key = $Member.$Key}
                "Method"    {Invoke-Expression "[Void](`$Control.$Key($($Member.$Key)))"}
                "Event"     {Invoke-Expression "`$Control.Add_$Key(`$Member.`$Key)"}
                Default     {Write-Error("The $($Control.GetType().Name) control doesn't have a '$Key' member.")}
            }
        }
    }
    $Add | ForEach {$Control.Controls.Add($_)}
    If ($Container) {$Container.Controls.Add($Control)}
    If ($Set) {$Set.Keys | ForEach {Invoke-Expression "`$Container.Set$_(`$Control, `$Set.`$_)"}}
    If ($GetParent) {$Container} Else {$Control}
}; Set-Alias Form Form-Control

语法

正在创建控件
<System.Windows.Forms.Control> = Form-Control [-Control <String>] [-Member <HashTable>]

修改控件
<Void> = Form-Control [-Control <System.Windows.Forms.Control>] [-Member <HashTable>]

向容器添加(新)控件
<System.Windows.Forms.Control> = Form-Control [-Control <String>|<System.Windows.Forms.Control>] [-Member <HashTable>] [-Add <System.Windows.Forms.Control[]>]

通过管道将容器连接到(新)控件
<System.Windows.Forms.Control> = <System.Windows.Forms.Control> | Form-Control [-Control <String>|<System.Windows.Forms.Control>] [-Member <HashTable>] [-Set <HashTable>] [-PassParent]

参数

-Control <String>|<System.Windows.Forms.Control> (位置0,默认:Form
-Control 参数接受 Windows 表单控件类型名称 ([String]) 或现有表单控件 ([System.Windows.Forms.Control] )。 Windows 表单控件类型名称如 Form, Label, TextBox, Button, Panel, ... 等。 如果提供了 Windows 表单控件类型名称 ([String]),包装器将创建并 return 一个新的 Windows 表单控件,其属性和设置由其余部分定义参数。
如果提供了现有的 Windows 表单控件 ([System.Windows.Forms.Control] ),包装器将使用其余参数定义的属性和设置更新现有的 Windows 表单控件。

-Member <HashTable> (位置 1)
设置 属性 值、调用方法并在新对象或现有对象上添加事件。

  • 如果哈希名称在控件上表示 property,例如Size = "50, 50",将值赋给控件属性值。

  • 如果哈希名称在控件上表示 method,例如Scale = {1.5, 1.5},将使用参数值调用控制方法。

  • 如果哈希名称在控件上表示 event,例如Click = {$Form.Close()},值([ScriptBlock])将添加到控件事件中。

两个集合属性,ColumnStylesRowStyles,特别针对 TableLayoutPanel control which is considered a general substitute for the WPF Grid 控件进行了简化: - 哈希值提供的 ColumnStyles property, clears all column widths and reset them with the ColumnStyle 数组。 - 哈希值提供的 RowStyles property, clears all row Heigths and reset them with the RowStyle 数组。
注意:如果要添加或插入单个特定的ColumnStyle或RowStyle项,您需要回退到原生语句,例如:[Void]$Control.Control.ColumnStyles.Add((New-Object Windows.Forms.ColumnStyle("Percent", 100)).

-Add <Array>
-Add参数向当前控件添加一个或多个子控件。
注意:如果容器通过管道传输到控件,则-add参数不能使用。

-Container <System.Windows.Forms.Control> (来自管道)
父容器通常由管道提供:$ParentContainer | Form $ChildControl 并将(新的)子控件附加到相关容器。

-Set <HashTable>
-Set参数设置(SetCellPositionSetColumnSetColumnSpanSetRowSetRowSpanSetStyle)特定的子控件属性与其父面板容器相关,例如.Set RowSpan = 2
注意: -set 列 - 和行参数只能在容器通过管道传输到控件时使用。

-GetParent
默认情况下,(子)控件将由 form-control 函数 return 编辑,除非提供了 -GetParent 开关,这将 return 父容器。 注意: -set 列 - 和行参数只能在容器通过管道传输到控件时使用。

例子

设置 Windows 表单层次结构的方法有两种:

  1. 向容器添加(新)控件
  2. 通过管道将容器连接到(新)控件

向容器添加(新)控件
对于此示例,我使用 PowerShell Form-Control 包装器在 docs.microsoft.com 处修改了 Creating a Custom Input Box

$TextBox      = Form TextBox @{Location = "10, 40";   Size = "260, 20"}
$OKButton     = Form Button  @{Location = "75, 120";  Size = "75, 23"; Text = "OK";     DialogResult = "OK"}
$CancelButton = Form Button  @{Location = "150, 120"; Size = "75, 23"; Text = "Cancel"; DialogResult = "Cancel"}
$Result = (Form-Control Form @{
        Size = "300, 200"
        Text = "Data Entry Form"
        StartPosition = "CenterScreen"
        KeyPreview = $True
        Topmost = $True
        AcceptButton = $OKButton
        CancelButton = $CancelButton
    } -Add (
        (Form Label    @{Text = "Please enter the information below:"; Location = "10, 20"; Size = "280, 20"}),
        $TextBox, $OKButton, $CancelButton
    )
).ShowDialog()

if ($result -eq [System.Windows.Forms.DialogResult]::OK)
{
    $x = $TextBox.Text
    $x
}

注1:虽然添加控件显得更加结构化,尤其是对于小窗体,缺点是不能调用与父级都相关的方法容器和子控件(如 -Set RowSpan)。
注意 2: 如果尝试直接在父容器中构建子(甚至孙)控件(如上所示 Label 控制)。此外,更难引用这样的子项(例如 $OKButton$Form.Controls["OKButton"],假设您已设置按钮 属性 Name = "OKButton

通过管道将容器连接到(新)控件
对于此示例,我创建了一个用户界面来测试 dock属性 行为。表格如下所示:

为此所需的 PowerShell Form-Control 代码:

$Form   = Form-Control Form @{Text = "Dock test"; StartPosition = "CenterScreen"; Padding = 4; Activated = {$Dock[0].Select()}}
$Table  = $Form  | Form TableLayoutPanel @{RowCount = 2; ColumnCount = 2; ColumnStyles = ("Percent", 50), "AutoSize"; Dock = "Fill"}
$Panel  = $Table | Form Panel @{Dock = "Fill"; BorderStyle = "FixedSingle"; BackColor = "Teal"} -Set @{RowSpan = 2}
$Button = $Panel | Form Button @{Location = "50, 50"; Size = "50, 50"; BackColor = "Silver"; Enabled = $False}
$Group  = $Table | Form GroupBox @{Text = "Dock"; AutoSize = $True}
$Flow   = $Group | Form FlowLayoutPanel @{AutoSize = $True; FlowDirection = "TopDown"; Dock = "Fill"; Padding = 4}
$Dock   = "None", "Top", "Left", "Bottom", "Right", "Fill" | ForEach {
    $Flow | Form RadioButton @{Text = $_; AutoSize = $True; Click = {$Button.Dock = $This.Text}}
}
$Close  = $Table | Form Button @{Text = "Close"; Dock = "Bottom"; Click = {$Form.Close()}}
$Form.ShowDialog()