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.
- 20,463
- 10
- 53
- 79
1 Answers
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
propertyon the control, e.g.Size = "50, 50", the value will be assigned to the control property value.If the hash name represents
methodon 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
eventon 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 -Addparameter 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 -Setparameter 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 learn.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 dockproperty 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()
- 20,463
- 10
- 53
- 79
