Your Script Can Have a Window Too
On Windows, a script can open a small window of its own with .NET's Windows Forms: a form, a list, a button and a label, with a script block that runs when the button is clicked, so a colleague who would rather click can use your tool without typing a command.
Freelancer · PowerShell · Updated
After this lesson you can
- Build a small Windows Forms window with a list and a button from PowerShell, and show it with ShowDialog()
- Run code when a button is clicked with Add_Click, reading the item chosen in the list
- Choose between objects, a saved file, colors on the screen and a window for what a tool gives back
Key takeaways
Add-Type -AssemblyName System.Windows.Formsloads Windows Forms;[System.Windows.Forms.Form]::new()makes a window you set up through its properties.- A ListBox shows a list, a Button is something to click, and a Label shows text;
$form.Controls.AddRange()puts them in the window. $button.Add_Click({ … })gives the button a script block to run on every click, and it can read$list.SelectedItem.$form.ShowDialog()shows the window and waits until it's closed. Windows Forms is Windows only.- A tool hands back objects for commands, a file for next time, colors for the person watching, and a window for someone who would rather click.
Your recipe box works, and your roommate wants to use it. They don't want to open a terminal, though, and they shouldn't have to. They want what every app gives them: a window, a list to pick from, and a button. The Terminal and the Mouse: Two Ways In promised options, not sides, and here's one of the best: the same script can open a small window of its own. You've had a taste already with Out-GridView, from Reports: ConvertTo-Html and Out-GridView; this lesson builds a window that's yours.
Windows only. The window comes from Windows Forms, part of .NET on Windows. On a Mac or Linux, Add-Type can't find it and stops with an error. It works the same in Windows PowerShell 5.1.
A window is an object too
Windows Forms is the set of window parts that .NET, the toolbox PowerShell is built on, has had for over twenty years. Add-Type loads it, and [System.Windows.Forms.Form]::new() makes a window, a form. The square brackets name a type, as [datetime] did in Numbers, Dates and Types, and ::new() (read "new") makes one. Like everything in PowerShell, the window is an object with properties. Make a file Show-RecipeWindow.ps1 in your recipe-box folder. Each block that builds the window goes into it, in order; paste them at the prompt as well, and the short blocks that only look at the window are for the prompt. Start like this:
Add-Type -AssemblyName System.Windows.Forms
$form = [System.Windows.Forms.Form]::new()
$form.Text = 'Recipe Box'
$form.ClientSize = [System.Drawing.Size]::new(320, 250)
$form.StartPosition = 'CenterScreen'
Text is the title bar, ClientSize the inside's width and height in pixels, and CenterScreen puts it in the middle of the screen. At the prompt, look at the window you made:
$form | Select-Object -Property Text, ClientSize, StartPosition, Visible
Text ClientSize StartPosition Visible
---- ---------- ------------- -------
Recipe Box {Width=320, Height=250} CenterScreen False
Visible is False: the window exists, but nothing has shown it yet.
A list, a button and a label
The parts of a window are controls. A ListBox shows a list to pick from; add the recipes from RecipeBox.ps1, from Where a Tool Keeps Its Data:
. "$HOME\terminal-practice\recipe-box\RecipeBox.ps1"
$list = [System.Windows.Forms.ListBox]::new()
$list.Location = [System.Drawing.Point]::new(10, 10)
$list.Size = [System.Drawing.Size]::new(300, 160)
foreach ($recipe in Get-Recipe) { [void]$list.Items.Add($recipe.Name) }
Location is counted in pixels from the window's top-left corner. Items.Add hands back the new item's position, a number that would print on the screen; [void] in front (read "throw away") drops it. A Button is something to click, and a Label shows a line of text:
$button = [System.Windows.Forms.Button]::new()
$button.Text = 'How long?'
$button.Location = [System.Drawing.Point]::new(10, 185)
$button.Size = [System.Drawing.Size]::new(100, 30)
$label = [System.Windows.Forms.Label]::new()
$label.Location = [System.Drawing.Point]::new(120, 192)
$label.AutoSize = $true
$form.Controls.AddRange(@($list, $button, $label))
The last line puts all three in the window. Check them:
$form.Controls | Select-Object -Property Text, Location
Text Location
---- --------
{X=10,Y=10}
How long? {X=10,Y=185}
{X=120,Y=192}
The list, the button with its text, and the label, empty until there's something to say.
When the button is clicked: `Add_Click`
A click is an event, something that happens while the window is open. Add_Click (read "add to Click") gives the button a script block, code in { } that waits until it's needed, and Windows Forms runs it on every click:
$button.Add_Click({
$recipe = Get-Recipe | Where-Object Name -eq $list.SelectedItem
if ($recipe) { $label.Text = "$($recipe.Minutes) minutes" }
else { $label.Text = 'Pick a recipe first.' }
})
$list.SelectedItem is the name the person picked, or nothing yet. Last, show the window:
$form.ShowDialog() | Out-Null
$form.Dispose()
ShowDialog() (read "show dialog") opens the window and waits: the script stops on that line until the window is closed. It hands back how it was closed, such as Cancel, which Out-Null drops. Dispose() frees the window afterward. Run .\Show-RecipeWindow.ps1, pick Pancakes, click How long?, and the label says 20 minutes:
For a colleague, give the script a desktop shortcut, from Run It from Anywhere, with -WindowStyle Hidden in place of -NoExit, so only your window stays on the screen.
A common mistake. The click block reads $list and $label when it runs, not when you write it. Build the window inside a function that hands it back, show it somewhere else, and by the time of the click those variables are gone: nothing happens, and the console says the property Text cannot be found. Add .GetNewClosure() after the block's closing brace, $button.Add_Click({ … }.GetNewClosure()), and it keeps its own copy of them.
Which way out?
A real tool often gives back more than one thing, and each goes the way that fits its reader:
| Who or what needs it | Give it | Lesson |
|---|---|---|
| The next command: sort, filter, export | Objects, with [PSCustomObject] |
Objects of Your Own: [PSCustomObject] |
| The tool itself, next time | A JSON file in $env:LOCALAPPDATA |
Where a Tool Keeps Its Data |
| The person watching the terminal | Write-Progress, colors with Write-Host |
Friendly Output: Colors and Progress |
| Someone who would rather click | A Windows Forms window | this lesson |
The terminal isn't always the right door, and now your tools can have both.
Check yourself
2 questions to try on the lesson page.
So what
A window is one more object your script can make: a few controls, a click that runs a script block, and ShowDialog(), and the tool you built in the terminal has a door for people who would rather click.