In PowerShell it is quiet common to use Windows Forms to build a User Interface for small cmdlets but the syntaxis required for this are often partly redundant and quiet verbose. This leads to the question:
Is there a way to minimize the code required or does there exist a Windows Forms wrapper for PowerShell to reduce the verbose and redundant syntaxis?
I am not looking for the ShowUI as this solution is too heavy considering it based on Windows Presentation Foundation (see also: WPF vs WinForms) and the fact that it concerns a PowerShell module which makes it more difficult to deploy it than a wrapper function.
问题:
回答1:
In a lot of cases a wrapper in not required to make your code less verbose, take e.g. the lengthy WinForms PowerShell script here. Code pieces like this:
$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
Can easily be simplified in WinForms to a single line:
$Tab1.Padding = 3
And if the padding would be different for each side, PowerShell will automatically convert:
$Tab1.Padding = "4, 6, 4, 6"
Note: PowerShell does not convert $Tab1.Padding = "3"
or $Tab1.Padding = "4, 6"
Nevertheless, the native way to create a windows form control is far from 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"
That's why I have created a reusable PowerShell Form Control wrapper that let's you minimize Windows Forms (WinForms) code to it's essence.
1) unless you use On<event>
methods, see also: 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
Syntax
Creating a control
<System.Windows.Forms.Control> = Form-Control [-Control <String>] [-Member <HashTable>]
Modifying a control
<Void> = Form-Control [-Control <System.Windows.Forms.Control>] [-Member <HashTable>]
Adding a (new) control to a container
<System.Windows.Forms.Control> = Form-Control [-Control <String>|<System.Windows.Forms.Control>] [-Member <HashTable>] [-Add <System.Windows.Forms.Control[]>]
Piping a container to a (new) control
<System.Windows.Forms.Control> = <System.Windows.Forms.Control> | Form-Control [-Control <String>|<System.Windows.Forms.Control>] [-Member <HashTable>] [-Set <HashTable>] [-PassParent]
Parameters
-Control <String>|<System.Windows.Forms.Control>
(position 0, default: Form
)
The -Control
parameter accepts either a Windows form control type name ([String]
) or an existing form control ([System.Windows.Forms.Control]
). Windows form control type names are like Form
, Label
, TextBox
, Button
, Panel
, ...
, etc.
If a Windows form control type name ([String]
) is supplied, the wrapper will create and return a new Windows form control with properties and settings as defined by the rest of the parameters.
If an existing Windows form control ([System.Windows.Forms.Control]
) is supplied, the wrapper will update the existing Windows form control using the properties and settings as defined by the rest of the parameters.
-Member <HashTable>
(position 1)
Sets property values, invokes methods and add events on a new or existing object.
If the hash name represents
property
on the control, e.g.Size = "50, 50"
, the value will be assigned to the control property value.If the hash name represents
method
on the control, e.g.Scale = {1.5, 1.5}
, the control method will be invoked using the value for arguments .If the hash name represents
event
on the control, take e.g.Click = {$Form.Close()}
, the value ([ScriptBlock]
) will be added to the control events.
Two collection properties, ColumnStyles
and RowStyles
, are simplified especially for the TableLayoutPanel control which is considered a general substitute for the WPF Grid control:
- The ColumnStyles
property, clears all column widths and reset them with the ColumnStyle
array supplied by the hash value.
- The RowStyles
property, clears all row Heigths and reset them with the RowStyle
array supplied by the hash value.
Note: If want to add or insert a single specific ColumnStyle or RowStyle item, you need to fallback on the native statement, as e.g.: [Void]$Control.Control.ColumnStyles.Add((New-Object Windows.Forms.ColumnStyle("Percent", 100))
.
-Add <Array>
The -Add
parameter adds one or more child controls to the current control.
Note: the -add
parameter cannot be used if container is piped to the control.
-Container <System.Windows.Forms.Control>
(from pipeline)
The parent container is usually provided from the pipeline: $ParentContainer | Form $ChildControl
and attached a (new) child control to the concerned container.
-Set <HashTable>
The -Set
parameter sets (SetCellPosition
, SetColumn
, SetColumnSpan
, SetRow
, SetRowSpan
and SetStyle
) the specific child control properties related its parent panel container, e.g. .Set RowSpan = 2
Note: the -set
column - and row parameters can only be used if a container is piped to the control.
-GetParent
By default the (child) control will be returned by the form-control
function unless the -GetParent
switch is supplied which will return the parent container instead.
Note: the -set
column - and row parameters can only be used if a container is piped to the control.
Examples
There are two way to setup the Windows Forms hierarchy:
- Adding a (new) control to a container
- Piping a container to a (new) control
Adding a (new) control to a container
For this example I have reworked the Creating a Custom Input Box at docs.microsoft.com using the PowerShell Form-Control wrapper:
$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
}
Note 1: Although the adding controls appears more structured especially for small forms, the drawback is that can't invoke methods that relate to both the parent container and child control (like -Set RowSpan
).
Note 2: You might easily get lost in open and close parenthesis if try build child (or even grandchild) controls directly in a parent container (like the above Label
control). Besides it more difficult to reference such a child (e.g. $OKButton
vs. $Form.Controls["OKButton"]
, presuming you have set the button property Name = "OKButton
)
Piping a container to a (new) control
For this example, I have created a user interface to test the dock
property behavior. The form looks like this:
The PowerShell Form-Control code required for this:
$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()