WPF From PowerShell – Updating Windows

In my last post I wrote about how you could make a WPF Splash screen window in PowerShell, but I stated that: “if the images are remote, the WPF window has to download them, and therefore won’t work” correctly. I had played with downloading images directly by setting the Source attribute of the image to the image URL, and hadn’t been able to find a way to get the threading to work well enough to actually download the image.

Well, I figured that out, so I thought I’d go ahead and share … basically, all you have to do is call Invoke() on the window’s dispatcher. The reason that works is that WPF handles events and “work” in general in the order it needs to be done, so since the most important thing is drawing the UI it will do that first, and since your UI is based on downloading the image, it will take care of that first.

This is PowerShell v2 CTP2 code…

All of this code is written to target the CTP2 release of PowerShell v2 — it will not work in PowerShell v1 or even in v2 CTP1 … and it may not work in later releases, although I expect it will. Also, as with any code which uses WPF from PowerShell, these code examples require you to launch PowerShell with the -STA option.

Having said that, lets just straight to a WPF example


function demo2 {
Add-Type -Assembly PresentationFramework

[xml]$xaml = "<Window xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
   WindowStyle='None' AllowsTransparency='True' Opacity='0.8' Topmost='True'
   SizeToContent='WidthAndHeight' WindowStartupLocation='CenterOwner' ShowInTaskbar='False'>
<Image Height='177' Source='http://dilbert.com/dyn/str_strip/000000000/00000000/0000000/000000/00000/5000/500/5651/5651.strip.print.gif' />
</Window>"


$splash = [Windows.Markup.XamlReader]::Load( (New-Object System.Xml.XmlNodeReader $xaml) )
$splash.Show()
$splash.Dispatcher.Invoke( "Render", [Windows.Input.InputEventHandler]{ $splash.UpdateLayout() }, $null, $null)
Start-Sleep 3   # imagine this is a long script  ... running
$splash.Hide() # and at the end, you just get rid of it
}

That’s almost as simple as it could get, but let me explain a few things which I left out previously, in case you’re new to XAML or even WPF. The Window tag is one of several acceptable root tags in XAML, but it’s the only one which will let you pop up a new window (in PoshConsole, you can display XAML inline using Out-WPF, and you can use almost any XAML nodes, but for now, lets focus on what works in general in the CTP2 console). The xmlns attribute is required in order for the xml to be verifiable to the schema, but in XAML it’s particularly important because without it, this would just be XML —not Xaml— and wouldn’t produce UI.

WPF Windows

There’s several fun tricks you can do in WPF with the Window that you can’t do in Windows Forms without a lot of hacking. ShowInTaskbar is hardly worth mentioning (it just does what it says). The Opacity attribute is kind of cool — it just controls the translucency level (valid values are from 0 to 1, but below the smaller values aren’t really very useful) — but you can animate this pretty easily to cause your windows to fade in and out. But there are a few that are more impressive:

Size To Content

One of my favorite features of WPF windows it that you can use SizeToContent to have the form automatically take the size of whatever is inside it (which is really useful when you’re loading images off the web). You have to be careful with this though, because WPF is resolution independent but it’s also aware of how many pixels-per-inch images have been saved with. Typically, if you have a png file, it’s been saved at something like 200 or 300 pixels per inch to make it printable … but your screen’s resolution is only (approximately) 96 pixels-per-inch. This results in images being displayed, by default … at HUGE sizes compared to what you expect. Caveat emptor.

Transparency

A WPF window with AllowsTransparency set to true can be non-rectangular! It’s very important that you understand this isn’t about allowing translucency (that is, it has nothing to do with the Opacity setting). With this setting on, your window will basically not exist in any spot where there’s no color (See demo4 below). It will be not only be invisible — mouse-clicks won’t register. You’ll also be able to have true per-pixel transparency, with the ability to set the alpha level of each pixel, since WPF colors can be specified as #AARRGGBB quads.

WindowStyle

This wouldn’t merit mentioning, except that it has a tie-in with Transparency. You can set the WindowStyle in Windows Forms too, and just like Windows Forms, the valuesfor WPF are None, SingleBorderWindow, ThreeDBorderWindow, and ToolWindow … however, if you use AllowsTransparency, the only valid value for WindowStyle is “None.”

Startup Location

As with WindowStyle, this attribute is mostly special because of it’s exceptions. You can use WindowStartupLocation to specify where you want the form to appear. Valid values are Manual, CenterScreen, and CenterOwner. CenterScreen is the obvious choice for a splash screen, but I should point out something about the other two as well. CenterOwner means to center the window on it’s parent window … but it only works with other WPF windows. If you don’t set the owner to a WPF window, CenterOwner works just like Manual. The Manual setting allows you to specify the window position with the Top and Left attributes, or just let Windows position you in the default location.

Events and Delegates

There’s one line of code in the sample above which bears explaining — the call to Dispatcher.Invoke. The point of this call is to basically transfer the thread control to the window for a moment. In fact, the signature of the Invoke method is that it takes a priority (I use render because it’s basically immediate), a delegate (I pass a scriptblock), and arguments for the scriptblock. PowerShell will let you easily cast a scriptblock to a delegate method which takes two object parameters and doesn’t return a value … but it won’t let you convert them to any other method without using reflection and IL.Emit. Thus, we cast our scriptblock to any handy delegate type, and pass $null for both parameters.

But that Invoke() stuff is ugly

Of course, we don’t like having to manually update the window by calling Invoke(), but I still haven’t figured out how to spin out another thread safely … so the only other option is to just give control to the window (actually I think this could be done with background jobs, but I can’t seem to get the WinRM CTP configured properly).

Giving control to the window is actually the normal way of doing UI programming … and isn’t really a very big deal in PowerShell — you can write your event handlers for the controls on the window, and then call ShowDialog() and you’re off. Just to give you an example, I’ve tweaked an old WPF sample I had and ported it to PowerShell … the xaml file is here and I’ll paste the code (with comments) inline, but if you may also download the PowerShell script. You need both files for it to work, because the clock.xaml file is the only argument to the demo-wpf4.ps1 script :)

Check it out:

A clock, with working CPU and RAM bars


### Import the WPF assemblies
Add-Type -Assembly PresentationFramework
Add-Type -Assembly PresentationCore

Write-Host "Initializing Performance Counters, please have patience" -fore Cyan
$script:cpu = new-object System.Diagnostics.PerformanceCounter "Processor", "% Processor Time", "_Total"
$script:ram = new-object System.Diagnostics.PerformanceCounter "Memory", "Available KBytes"

## get initial values, because the counters don't work until the second call
$null = $script:cpu.NextValue()
$null = $script:ram.NextValue()
$script:maxram = (gwmi Win32_OperatingSystem).TotalVisibleMemorySize

Write-Host "Loading XAML window..." -fore Cyan
## I've removed the xaml to a separate document because it's getting too big for my example :)

$clock = [Windows.Markup.XamlReader]::Load(
         (New-Object System.Xml.XmlNodeReader (
            [Xml](Get-Content "C:\Users\Joel\Documents\WindowsPowerShell\Scripts\Demo\clock.xaml") ) ) )

## Create a script block which will update the UI
$counter = 0;
$updateBlock = {
   # Update the clock
   $clock.Resources["Time"] = [DateTime]::Now.ToString("hh:mm.ss")

   # We only want to update the counters at most once a second
   # Otherwise their values are invalid and ...
   # The CPU counter fluctuates from 0 to the real number
   if( $counter++ -eq 4 ) {
      $counter = 0
      # Update the CPU counter
      $cu = $cpu.NextValue()
      $clock.Resources.CpuP = ($cu / 100)
      $clock.Resources.Cpu = "{0:0.0}%" -f $cu
      #$clock.FindResource("CpuStory").Begin($clock)
      # Update the RAM counter
      $rm = $ram.NextValue()
      $clock.Resources.RamP = ($rm / $maxram)
      $clock.Resources.Ram = "{0:0.00}Mb" -f ($rm/1MB)
      #$clock.FindResource("RamStory").Begin()
      }
}

## Hook up some event handlers
$clock.Add_SourceInitialized( {
   ## Before the window's even displayed ...
   ## We'll create a timer
   $timer = new-object System.Windows.Threading.DispatcherTimer
   ## Which will fire 4 times every second
   $timer.Interval = [TimeSpan]"0:0:0.25"
   ## And will invoke the $updateBlock
   $timer.Add_Tick( $updateBlock )
   ## Now start the timer running
   $timer.Start()
   if( $timer.IsEnabled ) {
      Write-Host "Clock is running. Don't forget: RIGHT-CLICK to close it."
   } else {
      $clock.Close()
      Write-Error "Timer didn't start"
   }
} )

$clock.Add_MouseLeftButtonDown( {
   $_.Handled = $true
   $clock.DragMove() # WPF Magic!
} )
$clock.Add_MouseRightButtonDown( {
   $_.Handled = $true
   $timer.Stop()  # we'd like to stop that timer now, thanks.
   $clock.Close() # and close the windows
} )

## Lets go ahead and invoke that update block
&$updateBlock
## And then show the window
$clock.ShowDialog()

[new] Post Script

I should probably mention that I coded the XAML for that clock by hand, using kaxaml, rather than using Expression Blend — I don’t necessarily recommend it, but it’s good practice once in a while. :) XAML supports a lot of animations, and I spent almost an entire day trying different things to create a smooth animation for the CPU and RAM bars which would let them animate smoothly from one value to the next. In the end I decided it would have to be written in code rather than XAML markup, and would complicate things even more than they already were.

Similar Posts:

2 thoughts on “WPF From PowerShell – Updating Windows”

  1. Great WPF sample(s). I hope you will continue :)
    One note: [DateTime]::Now.ToString(“hh:MM.ss”) should be maybe [DateTime]::Now.ToString(“hh:mm.ss”).

    I think WPF will persuade many people to start with Powershell.

Comments are closed.