Summary: Boe Prox shows how to use Windows PowerShell to build a clock widget.
Honorary Scripting Guy, Boe Prox, here today filling in for my good friend, The Scripting Guy. Here's a bit about me:
Boe Prox is a Microsoft MVP in Windows PowerShell and a senior Windows system administrator. He has worked in the IT field since 2003, and he supports a variety of different platforms. He is a contributing author in PowerShell Deep Dives with chapters about WSUS and TCP communication. He is a moderator on the Hey, Scripting Guy! forum, and he has been a judge for the Scripting Games since 2010. He recently presented talks on the topic of WSUS and PowerShell at the Mississippi PowerShell User Group. He is an Honorary Scripting Guy, and he has submitted a number of posts as a guest blogger, which discuss a variety of topics. To read more, see these Hey, Scripting Guy! Blog posts.
Boe’s blog: Learn Powershell |Achieve More
CodePlex projects: PoshWSUS, PoshPAIG, PoshChat, and PoshEventUI
I wanted to take the time today to show you how to build a clock widget by using Windows PowerShell. Although we use Windows PowerShell daily to perform great feats of strength in our environments and to provide amazing reports to our managers (among other things), sometimes it is just nice to kick back and make something fun. In this case, the fun thing that I will show you how to build is a clock widget!
Note You can download the full script from the Script Center Repository: Clock Widget.
Building a GUI can be a little time tedious because it usually means writing code to build out the front-end GUI and then more code to connect to controls, handle events, and perform various tasks. But fear not! All it takes is a little bit of time and you can knock this widget out! Plus, there are a variety of tools that can help make the process much simpler depending on your choice of GUI (such as Windows Presentation Foundation (WPF) and WinForms).
With that, let’s get started building this out! The first thing that I want to knock out is how the GUI will look and then work towards building out the back-end script. My preferred choice for GUIs is using WPF, and I usually take the approach of writing out the XAML. It’s a little more time consuming, and it requires some extra testing to make sure that everything lines up, but you can also take advantage of Visual Studio by creating the WPF GUI and then copying the XAML code to the Windows PowerShell ISE to finish the script. Here is the XAML code that will make up the GUI front end:
[xml]$xaml = @"
<Window
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
WindowStyle = 'None' WindowStartupLocation = 'CenterScreen' SizeToContent = 'WidthAndHeight'
ShowInTaskbar = "False" ResizeMode = "NoResize" Title = "Weather" AllowsTransparency = "True"
Background = "Transparent" Opacity = "1" Topmost = "True">
<Grid x:Name = "Grid" Background = "Transparent">
<TextBlock x:Name = "time_txtbox" FontSize = "72" Foreground = "$($Clockhash.TimeColor)"
VerticalAlignment="Top" HorizontalAlignment="Left" Margin="0,-26,0,0">
<TextBlock.Effect>
<DropShadowEffect Color = "Black" ShadowDepth = "1" BlurRadius = "5" />
</TextBlock.Effect>
</TextBlock>
<TextBlock x:Name = "ampm_txtbx" FontSize= "20" Foreground = "$($Clockhash.TimeColor)"
Margin = "133,0,0,0" HorizontalAlignment="Left">
<TextBlock.Effect>
<DropShadowEffect Color = "Black" ShadowDepth = "1" BlurRadius = "2" />
</TextBlock.Effect>
</TextBlock>
<TextBlock x:Name = "day_n_txtbx" FontSize= "38" Foreground = "$($Clockhash.DateColor)"
Margin="5,42,0,0" HorizontalAlignment="Left">
<TextBlock.Effect>
<DropShadowEffect Color = "Black" ShadowDepth = "1" BlurRadius = "2" />
</TextBlock.Effect>
</TextBlock>
<TextBlock x:Name = "month_txtbx" FontSize= "20" Foreground = "$($Clockhash.DateColor)"
Margin="54,48,0,0" HorizontalAlignment="Left">
<TextBlock.Effect>
<DropShadowEffect Color = "Black" ShadowDepth = "1" BlurRadius = "2" />
</TextBlock.Effect>
</TextBlock>
<TextBlock x:Name = "day_txtbx" FontSize= "15" Foreground = "$($Clockhash.DateColor)"
Margin="54,68,0,0" HorizontalAlignment="Left">
<TextBlock.Effect>
<DropShadowEffect Color = "Black" ShadowDepth = "1" BlurRadius = "2" />
</TextBlock.Effect>
</TextBlock>
<TextBlock x:Name = "year_txtbx" FontSize= "38" Foreground = "$($Clockhash.DateColor)"
Margin="0,42,0,0" HorizontalAlignment="Left">
<TextBlock.Effect>
<DropShadowEffect Color = "Black" ShadowDepth = "1" BlurRadius = "2" />
</TextBlock.Effect>
</TextBlock>
</Grid>
</Window>
"@
You may have noticed that I cast this as XML. This will be important later when I configure the initial window control.
I will now set up a runspace, which will have everything running in it (even the XAML code that I already showed), and a couple of synchronized collections that will handle the sharing of variables between my runspaces. Because I also want to provide the option of specifying various colors for the Time and the Date, I will have a couple of parameters to handle that.
Param (
[parameter()]
[string]$TimeColor = "White",
[parameter()]
[string]$DateColor = "White"
)
$Clockhash = [hashtable]::Synchronized(@{})
$Runspacehash = [hashtable]::Synchronized(@{})
$Runspacehash.host = $Host
$Clockhash.TimeColor = $TimeColor
$Clockhash.DateColor = $DateColor
$Runspacehash.runspace = [RunspaceFactory]::CreateRunspace()
$Runspacehash.runspace.ApartmentState = “STA”
$Runspacehash.runspace.ThreadOptions = “ReuseThread”
$Runspacehash.runspace.Open()
$Runspacehash.psCmd = {Add-Type -AssemblyName PresentationCore,
PresentationFramework,WindowsBase}.GetPowerShell()
$Runspacehash.runspace.SessionStateProxy.SetVariable("Clockhash",$Clockhash)
$Runspacehash.runspace.SessionStateProxy.SetVariable("Runspacehash",$Runspacehash)
$Runspacehash.runspace.SessionStateProxy.SetVariable("TimeColor",$TimeColor)
$Runspacehash.runspace.SessionStateProxy.SetVariable("DateColor",$DateColor)
$Runspacehash.psCmd.Runspace = $Runspacehash.runspace
$Runspacehash.Handle = $Runspacehash.psCmd.AddScript({…})
There is a lot happening here, but what I am doing is creating a runspace by using the [RunspaceFactory] static method, which will be used to run this clock widget outside of the Windows PowerShell console. The console still needs to be running, otherwise it will close the widget. This means that I can still use the console without it being tied down to handle the GUI.
My hash tables are added into the runspace via the SetVariable method, and I add the required assemblies to allow Windows PowerShell to create the WPF GUI. The next thing that I will want to do is have a separate script block that will be referenced in a Timer event to run during each tick:
$Script:update = {
$day,$Month,$Day_n,$Year,$Time,$AMPM = (Get-Date).DateTime -split "\s" -replace ","
$Day_n = $Day_n.PadLeft(2,"0")
$Time = $Time -replace '(.*):.*','$1'
$Clockhash.time_txtbox.text = $Time
$Clockhash.day_txtbx.Text = $day
$Clockhash.ampm_txtbx.text = $AMPM
$Clockhash.day_n_txtbx.text = $Day_n
$Clockhash.month_txtbx.text = $Month
$Clockhash.year_txtbx.text = $year
}
Up next is connecting to the created XAML, creating the window control, and using that to make the connection to the other controls so I can begin writing event handlers for the clock:
$reader=(New-Object System.Xml.XmlNodeReader $xaml)
$Clockhash.Window=[Windows.Markup.XamlReader]::Load( $reader )
$Clockhash.time_txtbox = $Clockhash.window.FindName("time_txtbox")
$Clockhash.ampm_txtbx = $Clockhash.Window.FindName("ampm_txtbx")
$Clockhash.day_n_txtbx = $Clockhash.Window.FindName("day_n_txtbx")
$Clockhash.month_txtbx = $Clockhash.Window.FindName("month_txtbx")
$Clockhash.year_txtbx = $Clockhash.Window.FindName("year_txtbx")
$Clockhash.day_txtbx = $Clockhash.Window.FindName("day_txtbx")
Note The names are case sensitive when you use the FindName() method. So take note of what you name the controls in the XAML code.
With that out of the way, we should begin looking at creating the event handlers for the clock. I want to first create a Timer object that will run in the UI thread and update the date and time with each tick. I want to do this as the window is being initialized so it starts updating right away:
#Timer Event
$Clockhash.Window.Add_SourceInitialized({
#Create Timer object
Write-Verbose "Creating timer object"
$Script:timer = new-object System.Windows.Threading.DispatcherTimer
#Fire off every 1 second
$timer.Interval = [TimeSpan]"0:0:1.00"
#Add event per tick
$timer.Add_Tick({
$Update.Invoke()
[Windows.Input.InputEventHandler]{ $Clockhash.Window.UpdateLayout()
})
#Start timer
Write-Verbose "Starting Timer"
$timer.Start()
If (-NOT $timer.IsEnabled) {
$Clockhash.Window.Close()
}
})
You can see here that I invoke the $Update script block to update the date and time of the clock. This is a clock, so I want to have it update every second to ensure accuracy. I want to make sure that I clean up after myself when this widget is closed:
$Clockhash.Window.Add_Closed({
$timer.Stop()
$Runspacehash.PowerShell.Dispose()
})
The next three event handlers satisfies my itch to make sure that everything lines up right away with the clock.:
$Clockhash.month_txtbx.Add_SizeChanged({
[int]$clockhash.length = [math]::Round(($Clockhash.day_txtbx.ActualWidth,$Clockhash.month_txtbx.ActualWidth |
Sort -Descending)[0])
[int]$Adjustment = $clockhash.length + 52 + 10 #Hard coded margin plus white space
$YearMargin = $Clockhash.year_txtbx.Margin
$Clockhash.year_txtbx.Margin = ("{0},{1},{2},{3}" -f ($Adjustment),
$YearMargin.Top,$YearMargin.Right,$YearMargin.Bottom)
})
$Clockhash.month_txtbx.Add_SizeChanged({
[int]$clockhash.length = [math]::Round(($Clockhash.day_txtbx.ActualWidth,$Clockhash.month_txtbx.ActualWidth |
Sort -Descending)[0])
[int]$Adjustment = $clockhash.length + 52 + 10 #Hard coded margin plus white space
$YearMargin = $Clockhash.year_txtbx.Margin
$Clockhash.year_txtbx.Margin = ("{0},{1},{2},{3}" -f ($Adjustment),
$YearMargin.Top,$YearMargin.Right,$YearMargin.Bottom)
})
$Clockhash.time_txtbox..Add_SizeChanged({
If ($Clockhash.time_txtbox.text.length -eq 4) {
$Clockhash.ampm_txtbx.Margin = "133,0,86,0"
} Else {
$Clockhash.ampm_txtbx.Margin = "172,0,48,0"
}
})
Had I thrown this into my $Update script block, the initial second or so would have the clock looking a little awkward by listing the year and having the A.M./P.M. text boxes crossing over the rest of the controls. Instead, this will make sure that the clock looks great at startup!
Because this is a widget, it will not have a button to close it or any visible window edges to grab to move around. This is where the following two events come into play:
$Clockhash.Window.Add_MouseRightButtonUp({
$This.close()
})
$Clockhash.Window.Add_MouseLeftButtonDown({
$This.DragMove()
})
The $This variable references the current object—in this case, the window control. By using the left button, I can move the widget around and I can close the widget by right-clicking it.
The final part is to run the script block to update the GUI and then show the widget. This is done by using ShowDialog() on the window control. That will actually happen when we use the BeginInvoke() method to kick off the runspace:
$Update.Invoke()
$Clockhash.Window.ShowDialog() | Out-Null
}).BeginInvoke()
In the end, we get something that looks like this:
That black and white color just isn’t cutting it for me. I want something a little different for the date and time. I will create a clock widget that is more colorful:
.\ClockWidget.ps1 –TimeColor DarkRed –DateColor Gold
I can even use hex values for the colors:
.\ClockWidget.ps1 –TimeColor "#669999" –DateColor "#334C4C"
And that is all there is to building a clock widget by using Windows PowerShell and WPF.
I invite you to follow the Scripting Guys on Twitter and Facebook. If you have any questions, send email to me at scripter@microsoft.com, or post your questions on the Official Scripting Guys Forum. See you tomorrow.
Boe Prox, Honorary Scripting Guy