Weekend Scripter: Create a Holiday Greeting Using PowerShell and WPF

Weekend Scripter: Create a Holiday Greeting Using PowerShell and WPF

  • Comments 4
  • Likes

  

Summary: Microsoft PFE Chris Bellee from Australia shows how to use Windows PowerShell and WPF to create a holiday greeting.

 

Microsoft Scripting Guy Ed Wilson here. It is almost the end of the year and we have decided to devote some posts to the holiday season. We even have guest bloggers from around the world to share some holiday spirit. Today is our last day and we will finish with a bang. Welcome Chris Bellée from Australia.

 

Chris Bellee is a Premier Field Engineer for Microsoft. He is based in Sydney, Australia. He teaches a very popular Windows PowerShell class to Microsoft Premier Customers.

 

In this holiday season blog post, I am going to cover a subject most Windows PowerShell console-based administrators will rarely have a reason to explore. Windows Presentation Foundation (more commonly known as WPF) is the latest graphical sub system for rendering GUI’s in Microsoft Windows and gives a wealth of features for designing advanced graphical user interfaces.

The Microsoft MSDN site describes WPF as:

“…a next-generation presentation system for building Windows client applications with visually stunning user experiences. With WPF, you can create many different stand-alone and browser-hosted applications.”

Because this is a Windows PowerShell article, I will be looking at how to use the WPF object model from Windows PowerShell script and manipulate its objects to create an animated GUI. WPF is a very large topic so I will only cover some core WPF concepts.

  • Adding layout controls to a WPF window
  • Wiring event handlers to objects
  • Creating a basic keyframe animation
  • Working with image resources
  • Positioning & styling controls
  • Updating a control’s contents by using a timer object

The example script creates a form without the ubiquitous window form ‘look and feel’, because WPF enables us to create borderless and non-square forms with ease. Anyone already familiar with WPF will know that a WPF layout can be created completely using the XAML mark-up language, a relative of XML. In fact, most WPF examples on the web use XAML exclusively, but in this post I will focus on leveraging the .NET object model directly through Windows PowerShell script to create the layout, controls, content and styling.

The first problem that you hit when you work with WPF from Windows PowerShell is something called ‘Threaded Apartments’. Which, to my surprise, are not a new form of suspended living accommodation, but how COM components can be accessed. There are two apartment types, single-threaded (STA) & multithreaded (MTA), from here it all becomes a bit technical but all we have to know is that the Windows PowerShell console runs in MTA mode & WPF controls can only run in STA mode. This presented a problem in Windows PowerShell V1, but in V2 we have some options to successfully host WPF objects.

  • Use the Windows PowerShell V2 ISE (Integrated Scripting Environment or ‘Graphical Windows PowerShell’) to run the script. The ISE is written using WPF objects & therefore runs in the correct apartment model by default.
  • Start a new PowerShell.exe console specifying the –STA switch.
  • Create a new PowerShell STA background runspace & execute the script there
    One can find lots of articles detailing how to do this, but I find the easiest way is to just use the Windows PowerShell ISE, as I almost exclusively use it for script development anyway.

 

 

Ok, let’s get WPF-ing.

The first thing we have to do is create a WPF window object. This will be the parent of all the controls we will add later. There are so many members implemented on every WPF object I’ll only demonstrate a few. Here is the code to create a new window object that contains a canvas object surrounded by a web 2.0 style rounded corner border control.  The window doesn’t have the typical modal form controls, is transparent and cannot be resized or moved. The following figure shows the result of the following code.

# create a WPF window object & set some of its properties

$objWindow = New-Object system.windows.window

$objWindow.Name = "Window1"

$objWindow.AllowsTransparency = $true

$objWindow.background = "Transparent"

$objWindow.WindowStyle = "None"

$objWindow.sizeToContent = "WidthAndHeight"

$objWindow.WindowStartupLocation = "CenterScreen"

$objWindow.ResizeMode = "NoResize"

 

# create a border control

$objBorder = New-Object system.windows.controls.border

$objBorder.BorderBrush = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Colors]::White)

$objBorder.borderThickness = 10

$objBorder.width = 710

$objBorder.height = 480

$objBorder.cornerRadius = 7  

 

# create a canvas control

$objCanvas = new-object system.windows.controls.canvas

$objCanvas.background = New-Object ` System.Windows.Media.SolidColorBrush([System.Windows.Media.Colors]::Black)

 

# add the canvas as a child of the border control

$objBorder.child = $objCanvas

 

# add the border control to the WPF window object’s content property

$objWindow.content = $objBorder

 

 

To allow the form to be moved or even closed, we have to add a couple of event handlers, because setting the WindowStyle property set to None removed the familiar minimize, maximize & title bar form controls.

# add an event handler to allow the window to be dragged using the left mouse button

$eventHandler_LeftButtonDown = [Windows.Input.MouseButtonEventHandler]{$this.DragMove()}

$objWindow.Add_MouseLeftButtonDown($eventHandler_LeftButtonDown)

 

# create event handler to close the window when a right-click is detected in the window

$eventHandler_RightButtonDown = [Windows.Input.MouseButtonEventHandler]{$this.close()}

$objWindow.Add_MouseRightButtonDown($eventHandler_RightButtonDown)

 

The [Windows.Input.MouseButtonEventHandler] class is used to respond to mouse events. The script block following the class name uses $this.DragMove() to refer to the object’s behavior when ‘wired-up’ using the window object’s MouseLeftButtonDown event. In this case is to allow the window to be dragged when left-clicked on.

Note that ‘Add_’ is prefixed to the event name, and the event handler object that was created on the previous line is passed as the method argument.

After the form has been defined, it is time to add some actual content. The aim of this example is to cross-fade a series of photographs in an endless loop. To do this we have to add the images to the canvas control & animate them using a basic ‘fade-in / fade-out’ keyframe sequence. Each image is then offset from the previous image in the timeline by a few seconds, to complete the effect.

I created a couple of functions to handle importing the images & creating the 5 keyframe animations as they are the same in every respect other than the image they animate. WPF uses a storyboard object to contain multiple animations and start them in parallel.

There is definitely a lot going on in the CreateAnimation function but it basically consists of the following steps:

 

  • Load a bitmap file into a new Image control object by using the loadBitMap helper function
  • Register the image control name with the canvas object so that we can reference it later on
  • Add the image control to the canvas object’s collection of child objects
  • Create a doubleAnimationUsingKeyframes object which allows us to control the image object’s properties over time using keyframes. The repeatbehavior property is set to Forever to create an endless loop.
  • Create 3 keyframes, specifying the target property’s start value and length.
  • Add the keyframes to the animation
  • Set the storyboard’s target name & property to animate, in this case we are animating the image’s Opacity property.
  • Add the completed animation object to the storyboard object.

Function CreateAnimation {

 

param (

$AnimationName,

$ImagePath,

$AnimationDuration,

$BeginTime,

$Storyboard,

$intIn,

$intHold,

$intOut

)

 

# create a temporary image object

$tmpImage = loadBitMap $ImagePath $AnimationName

 

# register the temporary image so we can find it later on

$objCanvas.RegisterName($tmpImage.Name, $tmpImage)

[void]$objCanvas.Children.Add($tmpImage)

 

# create a double keyframe animation object and set its duration, start time and looping behaviour

$objDoubleKeyFrameAnimation = new-object `

system.windows.media.animation.doubleanimationusingkeyframes

$objDoubleKeyFrameAnimation.duration = $(new-timespan -Seconds $AnimationDuration)

$objDoubleKeyFrameAnimation.repeatbehavior = "Forever"

$objDoubleKeyFrameAnimation.Name = $AnimationName

$objDoubleKeyFrameAnimation.BeginTime = $BeginTime

 

# create 3 keyframes to control the image opacity over time

$objLinearKeyFrameIn = New-Object system.windows.media.animation.LinearDoubleKeyFrame(1, $(new-timespan -Seconds $intIn))

$objLinearKeyFrameHold = New-Object system.windows.media.animation.LinearDoubleKeyFrame(1, $(new-timespan -Seconds $intHold))

$objLinearKeyFrameOut = New-Object system.windows.media.animation.LinearDoubleKeyFrame(0, $(new-timespan -Seconds $intOut))

 

# add the keyframes to the animation

[void]$objDoubleKeyFrameAnimation.keyframes.add($objLinearKeyFrameIn)

[void]$objDoubleKeyFrameAnimation.keyframes.add($objLinearKeyFrameHold)

[void]$objDoubleKeyFrameAnimation.keyframes.add($objLinearKeyFrameOut)

 

# set the animation target object

[System.Windows.Media.Animation.Storyboard]::SetTargetName($objDoubleKeyFrameAnimation,$tmpImage.Name)

 

# set the property to animate - in this case the opacity property

[System.Windows.Media.Animation.Storyboard]::SetTargetProperty($objDoubleKeyFrameAnimation, `

$(New-Object System.Windows.PropertyPath([System.Windows.Controls.Image]::OpacityProperty)))

 

# add the animation to the storyboard

$Storyboard.Children.Add($objDoubleKeyFrameAnimation)

}

The next step is to actually use the CreateAnimation function to, well, create some animations! This is achieved by using the Get-ChildIten cmdlet to return the .jpg image paths which are then piped to a ForEach-Object cmdlet. Each animation is identical other than the source image and timeline offset values supplied.

# create the animations & add them to the storyboard

# set a counter variable to control where each successive animation begins in the timeline

$i = 0

 

# return the .jpg image files

(Get-ChildItem -Path ".\*" -Filter "*.jpg") |

 

# loop through each image, calling the CreateAnimation function

ForEach-Object {

 

CreateAnimation -storyboard $objStoryboard -AnimationName $_.baseName -imagePath $_.fullName -animationDuration 16 -beginTime "0:0:$i" -intIn 2 -intHold 3 -intOut 6

 

# increment each animation start point by 3 seconds

$i += 3

 

}

 

Next, we have to create a grid control to hold the weather data returned from the Get-WeatherWebService function. As the name suggests, this function calls a web service by using the New-WebServiceProxy cmdlet to retrieve current weather information. The function parses the returned XML into a custom PSObject which is used to add the weather data to label controls on the WPF form.

 

A WPF grid control is similar to an HTML table. It enables the structured layout of child elements in a row/column manner. The following code creates a new grid object and adds 2 rows and 2 columns.

 

# create a grid control

$grd = New-Object system.windows.controls.grid

$grd.Width = 140

 

# create grid rows & columns

$row1 = new-object system.windows.controls.rowdefinition

$row1.height = "Auto"

$row2 = new-object system.windows.controls.rowdefinition

$row2.height = "Auto"

$col1 = new-object system.windows.controls.columndefinition

$row2.height = "Auto"

$col2 = new-object system.windows.controls.columndefinition

$row2.height = "Auto"

 

# add rows & columns to the grid

$grd.RowDefinitions.add($row1)

$grd.RowDefinitions.add($row2)

$grd.ColumnDefinitions.add($col1)

$grd.ColumnDefinitions.add($col2)

 

We can now add child elements to a particular row or column in the grid. Here, a label control is created to hold the current temperature and an image control holds an icon representing the current sky conditions. A grid’s child items can also be configured to span several rows or columns by calling a child item’s SetValue() method. In the extract below, the $lblWeather label control is set to span 2 columns.

 

# get the latest weather data for Sydney from the web service

$conditions = Get-WeatherWebService -strCountry "Australia" -strCity "Sydney"

 

# create a label control to hold the weather data

$lblWeather = New-Object System.Windows.Controls.Label

 

# fill the label content with the current temperature

$lblWeather.content = $Conditions.temp.tolower()

$lblWeather.FontSize = 34

$lblWeather.FontWeight = "Bold"

$lblWeather.opacity = 1

$lblWeather.Foreground = New-Object System.Windows.Media.SolidColorBrush([System.Windows.Media.Colors]::White)

 

# set the weather data to span 2 columns of the grid control

$lblWeather.SetValue([system.windows.controls.grid]::ColumnSpanProperty,2)

 

Child controls can also be positioned relative to their parents. The [system.windows.controls.canvas] class’s SetRight(), SetTop(), SetBottom() & SetLeft() static methods are used to position the label relative to its parent, the canvas object.

 

# position the weather label control

[system.windows.controls.canvas]::SetRight($lblWeather, 20)

[system.windows.controls.canvas]::SetTop($lblWeather, 20)

[system.windows.controls.grid]::SetColumn($lblWeather,0)

 

# add the weather label to the grid control

$grd.Children.add($lblWeather)

 

WPF objects can also have many styling options configured. Here we create a new color gradient object and add it to the label control’s ‘background’ property, creating an orange to yellow gradient fade effect.

 

 

The final interesting piece of code is how we can update the content of a WPF object continually. The script uses this ability to display the number of days, hours, minutes & seconds until the New Year. To do this, we create a $countDownUpdate scriptblock that contains a New-Timespan cmdlet. This calculates the difference between now and midnight on 31st December 2010, then sets this value on the content property of a label control.

 

# scriptblock that gets called every second by the timer object

# updates the 'content' property of the countdownlabel

$countDownUpdate = {

$ts = New-TimeSpan -Start $([datetime]::now) -End $([datetime]"12/31/2010 00:00:00")

 

# find the countdown label control

$lbl = $objCanvas.FindName("countDown")

$lbl.content = "$($ts.Days) . $($ts.Hours) : $($ts.Minutes) : $($ts.Seconds)"

}

 

To update the countdown every second, we add code to the canvas control’s Loaded event to add a new DispatcherTimer object with a Tick interval of 1 second and set each Tick event to call the $countDownUpdate scriptblock. Note the use of the call (&) operator to execute the script block using Windows PowerShell’s command mode.

 

# add event handler to the canvas

# when the canvas's 'loaded' event is fired, start the timer

$objCanvas.Add_Loaded({

$timer = new-object System.Windows.Threading.DispatcherTimer

$timer.Interval = [TimeSpan]"0:0:1"

 

# on each tick of the timer, execute the scriptblock using the call operator (&)

$timer.Add_Tick({&$CountDownUpdate})

 

# start the timer

$timer.Start()

})

 

Finally, there are a couple of important lines of code needed to complete the script. The first starts the animation objects that are contained within the storyboard object. The second displays the WPF window.

 

# start the animation storyboard

$objStoryboard.begin($objCanvas)

 

 

# show the WPF window

$objWindow.ShowDialog()

 

I hope this post has demonstrated a little of the huge amount of power the Windows Presentation Foundation system provides to Windows forms and how Windows PowerShell can leverage it. All that remains is to wish you all a Happy New Year from ‘Down Under’!

 

Thank you, Chris, for sharing with us. I have included all the files that Chris used to produce his WPF post card on the Scripting Guys Sky Drive.  This concludes holiday guest blogger week. Join me tomorrow as I start the final week of 2010.

I invite you to follow me on Twitter or Facebook. If you have any questions, send email to me at scripter@microsoft.com or post them on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

 

Ed Wilson, Microsoft Scripting Guy

Your comment has been posted.   Close
Thank you, your comment requires moderation so it may take a while to appear.   Close
Leave a Comment
  • It would be nice post if some of helper functions wouldn't be defined in narration only... (loadbitmap, Get-WeatherWebService). Currently I'm trying to guess what should be returned by those functions and re-write them... Educational? Probably. But not quite what I expected... ;)

  • It's a good article ,and I want to know the datails about Get-WeatherWebService and loadbitmap

  • @BartekB and nzhlx, thanks for the suggestion. I have just added a hyperlink to the files Chris used.

  • Thanks! It looks and works brilliant now! :)

    BUT I changed Get-WeatherWebService a bit (it's clear that Chris is from Australia... ;) ):

    # use a regular expression group to extract the celcius temperature value

       if ($_.currentweather.temperature -match "(?<Temperature>-{0,1}[0-9]{1,2} C)") {

    In original version it would omit '-', and there is slight difference between -9C (current temperature in Warsaw, Poland) and +9C (I wish....) ;)

    Thanks for create sample. I will try to dig it more now that it works. :)