PowerBoots: The tutorial walkthrough

[new] Updated to PowerBoots 0.1

An introduction to PowerBoots

Please excuse me if I start by just copying the basic ideas of the Shoes Tutorial, but I figured that since PowerBoots is inspired by Shoes, that was as good a place as any to start. PowerBoots (or just “Boots”) is a PowerShell 2.0 module with functions for writing Windows Presentation Framework (WPF) applications in the PowerShell scripting language. You should get the latest version of PowerBoots before continuing, and install it by putting the “PowerBoots” folder in one of your “Modules” folders (list them by typing $Env:PSMODULEPATH in PowerShell v2).

Don’t forget to start PowerShell.exe with the STA parameter (This is no longer required in PowerBoots 0.1).

Did I hear someone ask what is WPF? It was introduced as part of .Net 3.0 (and vastly improved in .Net 3.5), so you can expect to find it preinstalled on computers from Vista on, and of course you can download and install it on XP if it’s not already installed. The only thing you really need to know about WPF for the purposes of this tutorial is that it is the new GUI toolkit for .Net, and that it is container based — you put elements into other elements to control the layout, rather like HTML and Java Swing… you can pick up the rest as we go along.

A simple Boots program


New-BootsWindow -SizeToContent WidthAndHeight -Content {
   Button -Content "Push Me"
}
 

I’ve recently changed this first example to pass a script block to the content instead of using parentheses, because although PowerBoots supports running in an MTA PowerShell host, the only UI-creation command that is MTA-safe is the New-BootsWindow command. All of the “new” functions must then be inside of a scriptblock which is passed to the New-BootsWindow cmdlet and executed on that STA thread. You can use parenthesis ( and ) as a container instead, but that requires the host to be in STA threading mode (run: PowerShell -STA) so the controls can be created:


New-BootsWindow -SizeToContent WidthAndHeight -Content (
   Button -Content "Push Me"
)
 

This first example is a bit uglier than the Shoes syntax, so lets see if we can’t clean it up some. The -Content parameter is positional, so the first non-named argument you pass will be used for that. The same is true for the -Children parameter of panels, and in fact, each of the other similar parameters: Items, Blocks, and Inlines.

We have used a function New-BootsWindow which has an alias Boots. Boots takes all the same parameters as the Window function mentioned previously, but it uses slightly more useful defaults, and has a few other major benefits as well, the first of which is that it automatically “shows” the window, and the second is that it supports an -Async parameter which allows the window to come out in a new thread so that you can continue using PowerShell while the window remains alive and responsive. There is one catch: New-BootsWindow cannot take it’s content on the pipeline (the old function, now renamed “Out-BootsWindow” can take pipeline content, but is a script function, and requires -STA mode) — you have to specify it as a ScriptBlock. So now that we know this, we can rewrite our first example like this:


Boots { Button "Push Me" }
 

Just for the record, the simplest Boots program would just be a simple popup dialog to put some text in a Window, like: Boots { $msg }

We can put controls in a stack


Boots {
   StackPanel {
      Button "A bed of clams"
      Button "A coalition of cheetas"
      Button "A gulp of swallows"
   }
}
 

StackPanels are awesome. So are WrapPanels. Try that code with a WrapPanel instead of a StackPanel and see what the difference is. This brings up another point: those positional parameters we mentioned earlier: Content, Children, Items, Blocks, and Inlines, are also set to accept the value from the pipeline. Not only that, but they are intelligent about whether or not the content model accepts multiple items! So we can actually rewrite that script like this, and get the same results:


Boots { "A bed of clams", "A coalition of cheetas", "A gulp of swallows" | Button | StackPanel }
 

Now we’re really onto something! For most of the rest of these examples, I’m going to stick with the former syntax, because with the indenting and parenthesis, it’s much easier for you to follow, especially if you’re not already familiar with WPF. For instance, check out what this does:


Boots { "A bed of clams", "A coalition of cheetas", "A gulp of swallows" | StackPanel | Button }
 

Ok, lets see some formatting

To scoot the buttons out from the edge we can use margins or padding: margins go on the outside of containers, padding goes on the inside.


Boots {
   StackPanel -Margin 5 -Background Pink $(
      Button -Margin 2 "A bed of clams"
      Button -Margin 2 "A coalition of cheetas"
      Button -Margin 2 "A gulp of swallows"
   )
}

## Or, on one line:

Boots { "A bed of clams", "A coalition of cheetas", "A gulp of swallows" |
   Button -Margin 2 | StackPanel -Margin 5 -Background Pink }
 

So you see, the pink background is on the StackPanel, which has a (white) margin around it. If you wanted the whole background of the window to be pink, you would need to set the background of the Window instead of the StackPanel.

Time for some artwork


Boots { Ellipse -Width 60 -Height 80 -Margin "20,10,60,20" -Fill Black }
 

In Boots, everything always starts out white, and you must position things based on the container. You can see that the Margin can be specified as a single value as in the previous example, or as separate values for Left, Top, Right, Bottom. Oddly, to satisfy PowerShell’s type-casting, you have to quote them so they’re a single comma-separated string, instead of four separate values.

Some more advanced drawing


Boots {
Canvas -Height 100 -Width 100 -Children $(
   Rectangle -Margin "10,10,0,0" -Width 45 -Height 45 -Stroke Purple -StrokeThickness 2 -Fill Red
   Polygon -Stroke Pink -StrokeThickness 2 -Fill DarkRed -Points "10,60", "50,60", "50,50", "65,65",
                                                               "50,80", "50,70", "10,70", "10,60"
) }
 

We use a Canvas for this because it can contain multiple items which are all absolutely positioned. Unlike other containers, it doesn’t automatically expand to contain it’s children, so you typically have to set it’s size.

We also have to set the Stroke and Fill. These are the two colors that make up every object, if we don’t set them, they default to white. The StrokeThickness controls the line thickness. Notice that we positioned the Rectangle by using the Margin, and positioned the arrow, which we built using a Polygon, based purely on the x,y coordinates of the points. The available shapes are Ellipse, Line, Path, Polygon, Polyline, and Rectangle. You can, of course, make any shape you want to with the Polygon.

There are other more advanced shapes available in external libraries, and we can even do 3D, use gradient or image fills…

We can even get images straight off the web


Boots {
   Image -Source http://huddledmasses.org/images/PowerBoots/IMG_3298.jpg -MaxWidth 400 |
} -Title "Now those are some powerful boots!" -Async
 

Boots loads the image on a background thread, and caches it in memory, so the window will show up and be responsive while you’re waiting for the image, and because we’ve specified -Async, you can actually continue using PowerShell while the image loads. Note: it will load much faster the second time you run that script. ;)

Typography

PowerBoots doesn’t try to create a full set of typography-specific top-level elements the way Shoes does, because we are based on WPF, which has a far more powerful typography system available than any we’ve ever used. So instead of having a bunch of named elements like banner, and title, and caption, and para and whatnot, we have controls based on how much text you want to put in them, and how much formatting you want to apply: Label is simplest, TextBlock supports limited text formattings, and FlowDocument supports full rich content. And of course, Hyperlink supports clicking ;)

For the typography elements, the content model changes a bit. There are basically two types: Inline and Block elements. The TextBlock Content Model is similar to that of a FlowDocument, it is actually a type-restricted “Items” container. Instead of being able to have anything as content, it can only contain Inline flow content elements such as AnchoredBlock, Bold, Hyperlink, InlineUIContainer, Italic, LineBreak, Run, Span, and Underline, and it will create a run automatically if you just put a text string in it.


Boots {
   StackPanel -Margin 10 -Children $(
      TextBlock "A Question" -FontSize 42 -FontWeight Bold -Foreground "#FF0088"
      TextBlock -FontSize 24 -Inlines $(
         Bold "Q. "
         "Are you starting to dig "
         Hyperlink "PowerBoots?" -NavigateUri http://huddledmasses.org/tag/powerboots/ `
                                 -On_RequestNavigate { [Diagnostics.Process]::Start( $this.NavigateUri ) }
      )
      TextBlock -FontSize 16 -Inlines $(
         Span -FontSize 24 -FontWeight Bold -Inlines "A. "
         "Leave me alone, I'm hacking here!"
      )
)
}
 

Note: If you want support for the full document model (which allows Paragraphs and Lists), you need to use a FlowDocumentReader, FlowDocumentPageViewer, RichTextBox, or a FlowDocumentScrollViewer ... there’s lots more information about those on msdn.

Events

If you were paying attention to that previous example, you’ll notice we just introduced event handling. Event handlers in PowerBoots are specified in much the same way that Properties are. Their parameter names always start with “On_” and they take a script block. The Hyperlink element in a WPF window doesn’t automatically open a browser (because you can use it to change “pages” in a WPF application), so to make simple web links work, you have to handle the “RequestNavigate” event as shown above.

In order to update your user interface when an event triggers, you’ll need to have a variable that points at the control(s) you want to affect. You get a $this variable for free which points at the object that caused the event (eg: the Hyperlink in our previous example), but otherwise you need to handle this yourself. You can do that one of two ways: you can set a variable the way you normally would, and then use the variable in the form, or you can specify the variable name using the -OutVariable parameter. Personally I prefer the latter, as it messes up the flow of code less, but it has the downside that the output variable is always an array, even when there’s only one item.


Boots {
   $global:Count = 0
   WrapPanel  $(
      Button "Push Me" -On_Click {
         $global:Count++
         $label.Content = "You clicked the button ${global:Count} times!"
      }
      $script:label = Label "Nothing pushed so far"
      $label # You have to actually write-output the label
   )
} -Title "Test App" -On_Closing { $global:BootsOutput = $global:Count; rm variable:Count }
 

I’ve made these examples slightly more complicated than they had to be to demonstrate some best practices. When the Window is closed, the Out-Boots function returns the $BootsOutput variable — so if you want to output something from your gui, you need to set that variable. You can, of course, access global scope variables using the scope prefix $global:variableName, so you can set many different variables which you read later in your script. The catch is, sometimes the variables have to be explicitly set to script or even global scope in order to refer to the same variable in all of the event handlers…

However, if you want to have this block of code actually output something into the pipeline, you’ll always want to use the $BootsOutput variable. You can do that directly, the way the example above does, or you can simply use Write-Output! Inside the Out-Boots function, he Write-Output cmdlet just appends to the $BootsOutput variable … so it works pretty much exactly the way you would expect it to.


Boots {
   WrapPanel -On_Load { $Count = 0 } $(
      Button "Push Me" -On_Click {
         Write-Output (++$count)
         # You have to use array notation ...
         $block[0].Inlines.Clear();
         $block[0].Inlines.Add("You clicked the button $count times!")
      }
      TextBlock "Nothing pushed so far" -OutVariable script:block -VerticalAlignment Center
   )
}
 

The first example outputs just the count of how many times you clicked. The second outputs a series of numbers from 1 to however many times you click. It’s your choice of how to work with it.

We can have fun with colors

Boots gives you access to all the capabilities of the Windows Presentation Framework, but in some cases that comes at a cost, because we haven’t simplified their composability. So we have RadialGradientBrush and LinearGradientBrush, but you have to specify the GradientStops etc …


Boots -Background (
   RadialGradientBrush $(
      GradientStop -Offset 0 -Color "#F00"
      GradientStop -Offset 1 -Color "#F90"
   )
) {
   Label "Boots" -HorizontalAlignment Center `
                 -VerticalAlignment Center `
                 -Foreground White -Margin 80 `
                 -FontWeight Bold  -FontSize 40
}
 

We also have

So what does an InputBox look like?

PowerBoots11.png

Well, the simplest possible input box is just a TextBox, with the Width set (if you don’t set the Width, a TextBox will adjust to fit it’s contents, which can be really distracting). All you need to do is this:


Boots {
   TextBox -Width 220
} -Title "Enter your name" -On_Close {
   Write-Output $BootsWindow.Content.Text
}
 

PowerBoots12.png

Of course, the problem with that is that it pops up this rather confusing window, when what we really wanted was a prompt, and an “Ok” button, and some event handling to make the thing behave the way we expect it to. So lets try our first complicated form.

I’ll warn you ahead of time of one thing I’m going to do here. I’m using the “Border” element to apply a colored border to the StackPanel (because it doesn’t have it’s own border parameters), but then I’m also using the WindowStyle and AllowsTransparency properties to remove the normal window chrome, creating the bare little popup you see in the screenshot. I handle the mouse down event on the main window to allow the user to drag the window around by clicking anywhere on it (except in the TextBox or on the Button, of course). Now it looks slick, and —you know— it works!


function Get-BootsInput {
   Param([string]$Prompt = "Please enter your name:")
   
   Remove-Variable textBox -ErrorAction SilentlyContinue
   Boots {
      Border -BorderThickness 4 -BorderBrush "#BE8" -Background "#EFC" (
         StackPanel -Margin 10  $(
            Label $Prompt
            StackPanel -Orientation Horizontal $(
               TextBox -OutVariable global:textbox -Width 150 -On_KeyDown {
                  if($_.Key -eq "Return") {
                     Write-Output $textbox[0].Text
                     $BootsWindow.Close()
                  }
               }
               Button "Ok" -On_Click {
                  Write-Output $textbox[0].Text
                  $BootsWindow.Close()
               }
            )
         )
      )
   } -On_Load { $textbox[0].Focus() } `
   -WindowStyle None -AllowsTransparency $true `
   -On_PreviewMouseLeftButtonDown {
      if($_.Source -notmatch ".*\.(TextBox|Button)")
      {
         $BootsWindow.DragMove()
      }
   }
}
 

Hopefully you can follow that, although it’s obviously over the top :) . We handle the KeyDown event on the TextBox (if the Key is the Return key), and we also handle the click on the Button. In both cases, we’ll write out the text that was entered, and use the special $BootsWindow variable to close the window. We also handle the Load event for the window, to make sure the focus is on the TextBox, so you can just start typing.

A final example

PowerBoots13.png

I’ve got quite a few more examples I want to show off in part two of this tutorial, but to get you thinking about ways to integrate this with your routine tasks, and give you some ideas of what you can do with what you know already, let me give you this example of browsing photos, with a visual indication of how big the image file is. Of course, the point is that you could be visualizing, you know … anything.


add-type -Assembly System.Windows.Forms  # To get the Double-Click time

function New-GraphLabel {
[CmdletBinding()]
   PARAM (
      [Parameter(Position=0)][String]$Label = "Name",
      [Parameter(Position=1)][String]$Value = "Length",
      [Parameter(Position=2)][ScriptBlock]$DoubleClickAction = $null,
      [Parameter()][Int]$max = $null,
      [Parameter()][Int]$width = 200,
      [Parameter()][double]$margin = 2,
      [Parameter()][Int]$DoubleClickTime = $([System.Windows.Forms.SystemInformation]::DoubleClickTime),
      [Parameter(ValueFromPipeline=$true)][Alias("IO")][PSObject[]]$InputObject
   )
   BEGIN { $maxx = $max }
   PROCESS {
      if(!$maxx){ $maxx=@($InputObject)[0].$Value }

      foreach($io in $InputObject) {
         ## This is the core part of the script ...
         ## For each input, generate a grid panel with a label and a rectangle in the background
     
         GridPanel -tag @{item=$io; action=$DoubleClickAction} -width $Width -margin $margin $(
            Label $io.$Label
            Rectangle -HorizontalAlignment Left -Fill "#9F00" `
                      -Width ($Width * ($io."$Value" / $maxx))
         ) -On_MouseLeftButtonDown {
            if($this.Tag.Action) { # They passed in a doubleclick action, so lets handle it
               if($global:ClickTime -and
                  ([DateTime]::Now - $ClickTime).TotalMilliseconds -lt $global:DoubleClickTime) {
                  # We invoke the scriptblock
                  # and pass it the original input object
                  # and the grid panel object
                  &$This.Tag.Action $this.Tag.Item $this
               } else {
                  $global:ClickTime = [DateTime]::Now
               }
            }
         }
      }
   }
}

Set-Alias GraphLabel New-GraphLabel

## Example 1: list of processes with most RAM usage
## DoubleClickAction is `kill`
Boots {
   ps | sort PM -Desc | Select -First 20 |
      GraphLabel ProcessName PM {
         Kill $Args[0].Id
         $global:panel[0].Children.Remove($Args[1])
      } |
   StackPanel -ov global:panel
}
## Example 2: list of images, with file size indicated
## DoubleClickAction is `open`
Boots {
   ls ~/Pictures/ -recurse -Include *.jpg | Sort Length -Desc | % {
      if(!$Max){$Max=$_.Length}

      StackPanel -Width 200 -Margin 5 $(
         Image -Source $_.FullName
         GraphLabel Name Length -Max $Max -IO $_ {
            [Diagnostics.Process]::Start( $args[0].FullName )
         }
      )
   } | WrapPanel
} -Width 800
 

Notice that I had to manually handle the concept of a double click, because StackPanels don’t have a Click or DoubleClick event, just MouseDown and MouseUp. I could have stuck the stackpanel into something that does, but there’s really no need. Also, the [Diagnostics.Process]::Start is the equivalent of typing the name into the run dialog. I’m just executing the jpg, which makes it open in the default editor. It’s just a sample, after all.

End note

The current version of Boots does not add threading support, which means that when you run something through Boots, execution of your script stops until the window is closed. You can get around this somewhat in various different ways, but future releases will support running the WPF window in a separate thread, and even communicating to it … at the expense of a slightly different syntax. If you need that functionality, feel free to let me know … I could use some motivation.

I hope you’ve enjoyed this tour through PowerBoots, and will be able to start applying it for fun and profit.

Similar Posts:

14 thoughts on “PowerBoots: The tutorial walkthrough”

  1. This is great stuff and I plan to play with it quite a bit.

    However, I ran into some problems.

    It doesn't seem to work well under strict mode (Set-StrictMode -Version 2). The Events examples, for example, fail since $label and $block haven't been set yet so I get errors like:

    The variable $block cannot be retrieved because it has not been set yet.
    At line:5 char:13
    + $block <<<< [0].Inlines.Add("You clicked the button $count times!")
    + CategoryInfo : InvalidOperation: (block:Token) [], RuntimeException
    + FullyQualifiedErrorId : VariableIsUndefined

    There was also an error loading the PowerBoots.psm1 module whenever there was an error loading a type or setting an error. After this line:

    $ErrorList = $ErrorList | ? { $_.InvocationInfo.BoundParameters.Name -ne "Grid" }

    $ErrorList ended up null and the following check for $ErrorList.count failed since it had no such property.

    1. Well, you're right about that last one. I'll post a new build in a few … just change the line to:

      $ErrorList = @($ErrorList | ? { $_.InvocationInfo.BoundParameters.Name -ne "Grid" })

      However, even with StrictMode on, I didn't get any errors on those other scripts — certainly if $block isn't set, it should fail on the line before that, where it calls .Clear() — but they should be set before they get used, because they get set as -OutVariable from the initialization … and the event scriptblock doesn't get called until later.

      Honestly, I wrote the whole thing in strict mode originally, but I had to comment out the Set-StrictMode at the top, because it affects your event handlers and I certainly don't want to force everyone else to write in strict mode ;-)

  2. Wow Boots seams easer for adding small dialogs than PrimalForms.
    But needing the -sta parameter I can't use it with ISE for custom menue extensions.

    1. I&#039;m not sure what you mean about ISE. ISE is running in -STA mode by default — since it&#039;s a WPF application, it has to.

      1. Think I was confused while trying to download Windows 7. Yes PowerBoots is easy from ISE. I just posted http://poshcode.org/800,where I use it to querry two parameters for a custom menue function. I tried the same task using PrimalForms, it was more time consuming, the layout was crude, but it showed up a faster. As ISE is WPF I' ll proceed the easy way and focus on PowerBoots. I'm determinded to use and misuse ISE to its limits. Your posts came just in time.

        1. Oh, I see. Yes: PowerBoots is slower to render due to it’s dynamic code generation beneath the covers. That will probably continue to be true for a while. I’ve got some brain-cells working on ways to trade disk space and even RAM for snappier results (I already cache the forms you use, but only for the session — so rerunning the same script should be faster). For the time being I think loading XAML via XamlReader is the fastest way to get a UI — but PowerBoots is a lot more pipeline and console oriented ;)

  3. Could you show an example of adding a stylesheet i.e. adding a reference to a resource dictionary in another file to the windows ressources and how to pick up those styles in Boots syntax? I would expect the latter to be along the lines of : "A gulp of swallows" | Button -Style SuperButtonStyle | Boots. But how do you handle static vs. dynamic resources and so forth.
    thanks

  4. I like the -async very much. How is this done? I haven’t got background jobs (start-job) working, so I wondered whether I could add multithreading into powershell like you did with -async.

Comments are closed.