Yet Another Countdown Timer Using PowerShell

So I have been literally sitting on this script for about a year now and am finally looking to publish it after seeing Josh Atwell’s awesome post about writing a countdown timer in the PowerShell console and using it as motivation to stop being lazy with it. I figured I would take a break from doing some judging in this years Scripting Games to finish up this little ‘project’ and get it out of the way.

My take on this doesn’t use the console to display a countdown timer, but instead uses WPF to display the countdown timer. Like I said, I actually had this back around July when I wanted to have a countdown of some sort to let me know when the Omaha Half Marathon was happening and whipped it together with the plan of cleaning it up later on and blogging about it. Well, fast forward several months and here we go!

My requirements for this countdown timer is that had to consist of the following things:

  • Allow me to specify an end date
  • Allow a custom message to state what the countdown was for
  • Allow different font colors and sizes for both the countdown numbers and the custom message
  • Allow resizing of the countdown display with everything resizing proportionally
  • Ability to change the opacity of the display
  • Allow display to remain on top or fall behind other windows
  • Some sort of action when the countdown reaches 0

Ok, with that out of the way, I went forth by first coming up with my UI template using XAML (of course) and then verifying that everything is lined up correctly.

    <Window
        xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
        x:Name='Window' ResizeMode = 'NoResize' WindowStartupLocation = 'CenterScreen' Title = '$title' Width = '860' Height = '321' ShowInTaskbar = 'True' WindowStyle = 'None' AllowsTransparency = 'True'>
        <Window.Background>
        <SolidColorBrush Opacity= '0' ></SolidColorBrush>
        </Window.Background>
        <Grid x:Name = 'Grid' HorizontalAlignment="Stretch" VerticalAlignment = 'Stretch' ShowGridLines='false'  Background = 'Transparent'>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="2"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="2"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="2"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>                
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height = '*'/>
                <RowDefinition Height = '*'/>
            </Grid.RowDefinitions>   
            <Viewbox VerticalAlignment = 'Stretch' HorizontalAlignment = 'Stretch' StretchDirection = 'Both' Stretch = 'Fill' Grid.Row = '0' Grid.Column = '0'> 
                <Label x:Name='d_DayLabel' FontSize = '$FontSize' FontWeight = '$FontWeight' Foreground = '$CountDownColor' FontStyle = '$FontStyle' />
            </Viewbox>
            <Viewbox VerticalAlignment = 'Stretch' HorizontalAlignment = 'Stretch' StretchDirection = 'Both' Stretch = 'Fill' Grid.Row = '0' Grid.Column = '1'> 
                <Label x:Name='DayLabel' FontWeight = '$FontWeight' Content = 'Days' FontSize = '$FontSize' FontStyle = '$FontStyle' Foreground = '$MessageColor' />            
            </Viewbox>
            <Viewbox VerticalAlignment = 'Stretch' HorizontalAlignment = 'Stretch' StretchDirection = 'Both' Stretch = 'Fill' Grid.Row = '0' Grid.Column = '2'> 
                <Label Width = '5' /> 
            </Viewbox>
            <Viewbox VerticalAlignment = 'Stretch' HorizontalAlignment = 'Stretch' StretchDirection = 'Both' Stretch = 'Fill' Grid.Row = '0' Grid.Column = '3'> 
                <Label x:Name='d_HourLabel' FontSize = '$FontSize' FontWeight = '$FontWeight' Foreground = '$CountDownColor' FontStyle = '$FontStyle'/>
            </Viewbox>
            <Viewbox VerticalAlignment = 'Stretch' HorizontalAlignment = 'Stretch' StretchDirection = 'Both' Stretch = 'Fill' Grid.Row = '0' Grid.Column = '4'> 
                <Label x:Name='HourLabel' FontWeight = '$FontWeight' Content = 'Hours' FontSize = '$FontSize' FontStyle = '$FontStyle' Foreground = '$MessageColor' />
            </Viewbox>
            <Viewbox VerticalAlignment = 'Stretch' HorizontalAlignment = 'Stretch' StretchDirection = 'Both' Stretch = 'Fill' Grid.Row = '0' Grid.Column = '5'> 
                <Label Width = '5' /> 
            </Viewbox>
            <Viewbox VerticalAlignment = 'Stretch' HorizontalAlignment = 'Stretch' StretchDirection = 'Both' Stretch = 'Fill' Grid.Row = '0' Grid.Column = '6'> 
                <Label x:Name='d_MinuteLabel' FontSize = '$FontSize' FontWeight = '$FontWeight' Foreground = '$CountDownColor' FontStyle = '$FontStyle'/>
            </Viewbox>
            <Viewbox VerticalAlignment = 'Stretch' HorizontalAlignment = 'Stretch' StretchDirection = 'Both' Stretch = 'Fill' Grid.Row = '0' Grid.Column = '7'> 
                <Label x:Name='MinuteLabel' FontWeight = '$FontWeight' Content = 'Minutes' FontSize = '$FontSize' FontStyle = '$FontStyle' Foreground = '$MessageColor' />
            </Viewbox>
            <Viewbox VerticalAlignment = 'Stretch' HorizontalAlignment = 'Stretch' StretchDirection = 'Both' Stretch = 'Fill' Grid.Row = '0' Grid.Column = '8'> 
                <Label Width = '5' />
            </Viewbox>
            <Viewbox VerticalAlignment = 'Stretch' HorizontalAlignment = 'Stretch' StretchDirection = 'Both' Stretch = 'Fill' Grid.Row = '0' Grid.Column = '9'> 
                <Label x:Name='d_SecondLabel' FontSize = '$FontSize' FontWeight = '$FontWeight' Foreground = '$CountDownColor' FontStyle = '$FontStyle' />
            </Viewbox>
            <Viewbox VerticalAlignment = 'Stretch' HorizontalAlignment = 'Stretch' StretchDirection = 'Both' Stretch = 'Fill' Grid.Row = '0' Grid.Column = '10'> 
                <Label x:Name='SecondLabel' FontWeight = '$FontWeight' Content = 'Seconds' FontSize = '$FontSize' FontStyle = '$FontStyle' Foreground = '$MessageColor' />
            </Viewbox>
            <Viewbox VerticalAlignment = 'Stretch' HorizontalAlignment = 'Stretch' StretchDirection = 'Both' Stretch = 'Fill' Grid.Row = '1' Grid.ColumnSpan = '11'> 
                <Label x:Name = 'TitleLabel' FontWeight = '$FontWeight' Content = '$Message' FontSize = '$FontSize' FontStyle = '$FontStyle' Foreground = '$MessageColor' />        
            </Viewbox>
        </Grid>
    </Window>

After that is done, I can then start writing the backend code to handle all of the events that will make this countdown work like it is supposed to and provide all of the requirements that I listed above.

Some code used for this here:

    ##Connect to controls
    $TitleLabel = $Global:Window.FindName("TitleLabel")
    $d_DayLabel = $Global:Window.FindName("d_DayLabel")
    $DayLabel = $Global:Window.FindName("DayLabel")
    $d_HourLabel = $Global:Window.FindName("d_HourLabel")
    $HourLabel = $Global:Window.FindName("HourLabel")
    $d_MinuteLabel = $Global:Window.FindName("d_MinuteLabel")
    $MinuteLabel = $Global:Window.FindName("MinuteLabel")
    $d_SecondLabel = $Global:Window.FindName("d_SecondLabel")
    $SecondLabel = $Global:Window.FindName("SecondLabel")

    ##Events
    $window.Add_MouseRightButtonUp({
        $this.close()
        })
    $Window.Add_MouseLeftButtonDown({
        $This.DragMove()
        })    
    #Timer Event
    $Window.Add_SourceInitialized({
        #Create Timer object
        Write-Verbose "Creating timer object"
        $Global:timer = new-object System.Windows.Threading.DispatcherTimer 
        #Fire off every 5 seconds
        Write-Verbose "Adding 1 second interval to timer object"
        $timer.Interval = [TimeSpan]"0:0:1.00"
        #Add event per tick
        Write-Verbose "Adding Tick Event to timer object"
        $timer.Add_Tick({
            If ($EndDate -gt (Get-Date)) {
                $d_DayLabel.Content = ([datetime]"$EndDate" - (Get-Date)).Days
                $d_HourLabel.Content = ([datetime]"$EndDate" - (Get-Date)).Hours
                $d_MinuteLabel.Content = ([datetime]"$EndDate" - (Get-Date)).Minutes
                $d_SecondLabel.Content = ([datetime]"$EndDate" - (Get-Date)).Seconds
            } Else {
                $d_DayLabel.Content = $d_HourLabel.Content = $d_MinuteLabel.Content = $d_SecondLabel.Content = 0    
                $d_DayLabel.Foreground = $d_HourLabel.Foreground = $d_MinuteLabel.Foreground = $d_SecondLabel.Foreground = Get-Random -InputObject $Colors
                $DayLabel.Foreground = $HourLabel.Foreground = $MinuteLabel.Foreground = $SecondLabel.Foreground = Get-Random -InputObject $Colors
                If ($EndFlash) {
                    $TitleLabel.Foreground = Get-Random -InputObject $Colors
                }
                If ($EndBeep) {
                    [console]::Beep()
                }
            }
            })
        #Start timer
        Write-Verbose "Starting Timer"
        $timer.Start()
        If (-NOT $timer.IsEnabled) {
            $Window.Close()
            }
        })   
    $Global:Window.Add_KeyDown({
        Switch ($_.Key) {
            {'Add','OemPlus' -contains $_} {
                If ($Window.Opacity -lt 1) {
                    $Window.Opacity = $Window.Opacity + .1
                    $Window.UpdateLayout()
                    }            
                }
            {'Subtract','OemMinus' -contains $_} {
                If ($Window.Opacity -gt .2) {
                    $Window.Opacity = $Window.Opacity - .1
                    $Window.UpdateLayout()
                    }             
                }
            "r" {
                If ($Window.ResizeMode -eq 'NoResize') {
                    $Window.ResizeMode = 'CanResizeWithGrip'
                    }      
                Else {
                    $Window.ResizeMode = 'NoResize'             
                    }       
                }     
            "o" {
                If ($Window.TopMost) {
                    $Window.TopMost = $False
                    }
                Else {
                    $Window.TopMost = $True
                    }
                }     
            }
        })     
    $Window.Topmost = $True   

Once that was done, I decided that I wanted to make sure that I kept the console available for other commands (when you typically run a UI it will take over the console, preventing you from doing anything else), I decided to create a background runspace and send all of the parameters to that runspace and call the code. By doing this, the countdown timer will display AND I can still run commands and whatever else from the console.

Some of the code is here that I used to make this happen:

$rs = [RunspaceFactory]::CreateRunspace()
$rs.ApartmentState = “STA”
$rs.ThreadOptions = “ReuseThread”
$rs.Open() 
$rs.SessionStateProxy.SetVariable('EndDate',$EndDate) 
$rs.SessionStateProxy.SetVariable('Message',$Message) 
$rs.SessionStateProxy.SetVariable('Title',$Title) 
If ($PSBoundParameters['EndFlash']) {
    $rs.SessionStateProxy.SetVariable('EndBeep',$EndBeep)
}
If ($PSBoundParameters['EndBeep']) {
    $rs.SessionStateProxy.SetVariable('EndFlash',$EndFlash)
}
$rs.SessionStateProxy.SetVariable('FontWeight',$FontWeight) 
$rs.SessionStateProxy.SetVariable('FontStyle',$FontStyle) 
$rs.SessionStateProxy.SetVariable('FontSize',$FontSize) 
$rs.SessionStateProxy.SetVariable('CountDownColor',$CountDownColor) 
$rs.SessionStateProxy.SetVariable('MessageColor',$MessageColor) 
$psCmd = {Add-Type -AssemblyName PresentationCore,PresentationFramework,WindowsBase}.GetPowerShell() 
$psCmd.Runspace = $rs 
$psCmd.Invoke() 
$psCmd.Commands.Clear() 
$psCmd.AddScript({ 
...
}).BeginInvoke() | out-null

So lets see this thing in action:

.\Start-CountDownTimer.ps1 -EndDate (Get-Date "06/11/2012 12:00:00 AM") `
-Message "Time Until Tech-Ed 2012" -MessageColor Green `
-CountDownColor Black -EndBeep -EndFlash

image

And there you go! You can also change the opacity by using the + and – keys:

image

As well as resizing by clicking ‘r’ and simply resizing by dragging on the lower right hand corner.

image

image

Move the timer by holding down the left mouse button and dragging it around. Close the countdown timer by right-clicking on the countdown timer.

Now to show off using the –EndFlash parameter to watch the countdown ui show different colors once it reaches 0. I would show the –EndBeep, but you might not be able to hear it through the internet Smile.

.\Start-CountDownTimer.ps1 -EndDate (Get-Date) -EndBeep -EndFlash

image

image

As you can see, the colors for both the timer and message will change with each tick.

With that, I met all of my requirements that I wanted to have with this script. I know that there are a lot more things that I could add to this, but in this case, I am satisfied with the end results.

Download

You can download the script here.

Key Input Tips:
r: Toggles the resize mode of the clock so you can adjust the size.
o: Toggles whether the countdown remains on top of windows or not.
+: Increases the opacity of the clock so it is less transparent.
-: Decreases the opacity of the clock so it appears more transparent.

Let me know what you think of it!

About Boe Prox

Microsoft Cloud and Datacenter MVP working as a SQL DBA.
This entry was posted in GUI, powershell, scripts and tagged , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s