Boost your shell with reusable code stripped to its essentials


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 far from DRY (don’t repeat yourself) programming.

In a lot of cases it not required to created any functions 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 that PowerShell does not convert $Tab1.Padding = "3" or $Tab1.Padding = "4, 6"

Nevertheless, the native way to create a windows form control is quiet lengthy and 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, there isn’t a quick and easy way to add events*1, 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


Add-Type -AssemblyName System.Windows.Forms
Function Set-FormControl {
        [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 New-FormControl Set-FormControl; Set-Alias Form Set-FormControl


Creating a control

<System.Windows.Forms.Control> = New-FormControl [-Control <String>] [-Member <HashTable>]

Modifying a control

<Void> = Set-FormControl [-Control <System.Windows.Forms.Control>] [-Member <HashTable>]

Adding a (new) control to a container

<System.Windows.Forms.Control> = Set-FormControl [-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> | New-FormControl [-Control <String>|<System.Windows.Forms.Control>] [-Member <HashTable>] [-Set <HashTable>] [-PassParent]

Note that Set-FormControl is simply an alias for New-FormControl, there is no functional difference between the commands. Whether a new form control is created or update, depends object type supplied to the -Control parameter.


-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 needto 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 -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 Set-FormControl function unless the -GetParent switch is supplied which will return the parent container instead.

Note: the -GetParentparameter can only be used if a container is piped to the control.


There are two way to setup the Windows Forms hierarchy:

  1. Adding a (new) control to a container
  2. 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 for 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 = (New-FormControl 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
if ($result -eq [System.Windows.Forms.DialogResult]::OK)
    $x = $TextBox.Text

Piping a container to a (new) control

For this I have created a small for to test the docking behavior:

$Form    = New-FormControl Form @{Text = "Dock test"; StartPosition = "CenterScreen"; Padding = 4}
$Table   = $Form  | Form TableLayoutPanel @{RowCount = 2; ColumnCount = 3; ColumnStyles = ("Percent", 50), "AutoSize", "AutoSize"; Dock = "Fill"}
$Panel   = $Table | Form Panel @{Dock = "Fill"; BorderStyle = "FixedSingle"; BackColor = "Teal"} -Set @{RowSpan = 2}
$Dock = ForEach ($i in 1..2) {
    $Button = $Panel | Form Button @{Location = "25, $(75 * $i - 50)";  Size = "50, 50"; BackColor = "Silver"; Enabled = $False; Text = $i}
    $Group  = $Table | Form GroupBox @{Text = "Dock $i"; AutoSize = $True}
    $Flow   = $Group | Form FlowLayoutPanel @{AutoSize = $True; FlowDirection = "TopDown"; Dock = "Fill"; Padding = 4}
    $Radio  = "None", "Top", "Left", "Bottom", "Right", "Fill" | ForEach {
        $Flow | Form RadioButton @{Text = $_; AutoSize = $True; Click = ([ScriptBlock]::Create("`$Dock[$($i - 1)].Button.Dock = `$This.Text"))}
    New-Object PSObject -Property @{Button = $Button; Group = $Group; Flow = $Flow; Radio = $Radio}
$Close  = $Table | Form Button @{Text = "Close"; Dock = "Bottom"; Click = {$Form.Close()}} -Set @{ColumnSpan = 2}

(See StackOverflow for an example with two docked items)

The Set-ControlForm cmdlet was originally published at StackOverflow

Last updated on 29 Jun 2020
Published on 6 Nov 2017