Creating a Battery Indicator Watch Face for Garmin

Today, I want to show you how to create a very simple battery indicator in Garmin. I want to create a label that shows the current battery percentage which also changes colour depending on the percentage level.

So, I’ll start by creating a new label in the watch that I will use. This is my layout.xml:

<layout id="WatchFace">
<label id="BatteryLabel" x="center" y="center" font="Graphics.FONT_NUMBER_THAI_HOT" justification="Graphics.TEXT_JUSTIFY_CENTER" color="Graphics.COLOR_WHITE" />
</layout>

Now, in the View.mc file I will use this label to add the battery percentage and assign a colour based on that percentage.

First, I will paste in my updateDisplayObject method to update the label text:

//Update the display object text
function updateDisplayObject(updateObject, updateText){
var _viewObject = View.findDrawableById(updateObject) as Text;
_viewObject.setText(updateText);
}

I will then create a method to update the battery label’s text colour based on the battery percentage. This will need to take in the battery percentage as a parameter:

//Update the battery text color
function updateBatteryTextColor(updateBatPerc){
var _color = Graphics.COLOR_GREEN;
//Setting battery label color based on percentage
if (updateBatPerc > 75){
_color = Graphics.COLOR_DK_GREEN;
}
if (updateBatPerc < 75 && updateBatPerc >= 25){
_color = Graphics.COLOR_YELLOW;
}
if (updateBatPerc < 25){
_color = Graphics.COLOR_RED;
}
var _viewObject = View.findDrawableById("BatteryLabel");
_viewObject.setColor(_color);
}

From the above code, you can see I am assigning a specific color based on the batter percentage on a three tier system. The label colour will be based on the following rules:

ValuesColor
Greater than 75%Green
Lower than 75% and higher than or equal to 25%Yellow
Lower than 25%Red
Battery label colour table

I can now update the onUpdate method to use the above methods to show the battery indicator correctly. This is what the onUpdate method looks like:

// Update the view
function onUpdate(dc as Dc) as Void {
//Get and show the current battery percentage
var pwr = System.getSystemStats().battery;
var batStr = Lang.format( "$1$%", [ pwr.format( "%2d" ) ] );
updateDisplayObject("BatteryLabel", batStr);
//Update battery text color
updateBatteryTextColor(pwr);
// Call the parent onUpdate function to redraw the layout
View.onUpdate(dc);
}

After all that, you can see the watch face showing the correct three colours below:

Red battery indicator
Yellow battery indicator
Green battery indicator

Enjoy! 🎉

Creating a Simple Garmin Watch Face

So, I recently got into running and picked up a bargain Garmin watch. I looked on the marketplace for a simple watch face that had the attributes I wanted. I found nothing that suited my needs.

The apps from the app store were often too complex looking or lacked functionality.

This is where I started researching how to create a custom watch face and maybe even post it to the marketplace if possible. I learnt that Garmin uses Monkey C along with their SDK to create everything from watch faces to apps.

First things first, you need to setup the environment and download the SDK: https://developer.garmin.com/connect-iq/sdk/

Once setup, lets open Visual Studio Code and open the command palette using Ctrl+Alt+P. In this prompt, we want to search for the Monkey C: New Project option. It will then ask for the name of the project and the type. For this example, I will select a Simple Watch Face and set the API level to the lowest value.

This will build the project structure and create a base watch face to work from.

We now need to add supported products to the project. My watch is the Garmin Forerunner 735XT but you can use whichever watch you have. Use the command palette again and use the Monkey C: Edit Products option. In that dropdown, select all the watches you want to support and build for.

Now we can hit F5 and run the watch face in the virtual watch:

The prebuilt project from Garmin

In this post, I’m going to build a watch face similar to the below design:

Simple watch face design concept

First, lets edit the layout.xml to build the design of the watch face. This file is in Resources -> Layouts.

Remove the default label from the XML file and lets build the 4 new labels required for our battery percentage, steps, current time and todays date.

The layouts are fairly easy to follow, each label requires the following attributes:

AttributeExplanationPossible Values
idThe name of the label. This will be used in the code to update the value.Any string value starting with a capital letter. E.g. TestLabel
xThe horizontal positioning of the label. This can be a pixel value or the predefined values.left, center, right, pixel value, or a percentage
yThe vertical positioning of the label. This can be a pixel value or the predefined values.top, center, bottom, pixel value, or a percentage
fontThe font to use on the label. These are predefined.All options are documented in the API docs. We are only using values from API level 1.0.0 for now:

https://developer.garmin.com/connect-iq/api-docs/Toybox/Graphics.html
justificationHow the text should position itself in the space provided.TEXT_JUSTIFY_RIGHT,
TEXT_JUSTIFY_CENTER,
TEXT_JUSTIFY_LEFT,
TEXT_JUSTIFY_VCENTER
colorThe colour of the textAll options are documented in the API docs:

https://developer.garmin.com/connect-iq/api-docs/Toybox/Graphics.html

So my new layout.xml file will look like this:

<layout id="WatchFace">
<label id="TimeLabel" x="center" y="center" font="Graphics.FONT_NUMBER_THAI_HOT" justification="Graphics.TEXT_JUSTIFY_CENTER" color="Graphics.COLOR_WHITE" />
<label id="BatteryPercentageLabel" x="center" y="top" font="Graphics.FONT_MEDIUM" justification="Graphics.TEXT_JUSTIFY_CENTER" color="Graphics.COLOR_LT_GRAY"/>
<label id="StepsLabel" x="center" y="10%" font="Graphics.FONT_MEDIUM" justification="Graphics.TEXT_JUSTIFY_CENTER" color="Graphics.COLOR_LT_GRAY"/>
<label id="DateLabel" x="center" y="bottom" font="Graphics.FONT_MEDIUM" justification="Graphics.TEXT_JUSTIFY_CENTER" color="Graphics.COLOR_LT_GRAY"/>
</layout>

Now that the layout is built, we need to build the functionality in the backend to update the view. Open the watch face _View.mc file. This can be found in Source -> watchface_View.mc.

First, lets add a required using statement at the top. Add the following line:

using Toybox.Time.Gregorian as Calendar;

I will also create a new method for updating the view. This method will take in the string ID of the label as well as the new value:

//Update the display object text
function updateDisplayObject(updateObject, updateText){
var _viewObject = View.findDrawableById(updateObject) as Text;
_viewObject.setText(updateText);
}

We can now use the above method to update the view.

We will now head over to the onUpdate method to update the view.

Now, lets work on building the required data and formatting it before updating the view. You can see below how I work through each of the four required sections and use the above updateDisplayObject method to update the view:

// Get and show the current time
var clockTime = System.getClockTime();
var timeString = Lang.format("$1$:$2$", [clockTime.hour, clockTime.min.format("%02d")]);
updateDisplayObject("TimeLabel", timeString);
//Get and show the current battery percentage
var pwr = System.getSystemStats().battery;
var batStr = Lang.format( "$1$%", [ pwr.format( "%2d" ) ] );
updateDisplayObject("BatteryPercentageLabel", batStr);
//Get and show the steps data
var stepCountString = ActivityMonitor.getInfo().steps.toString();
updateDisplayObject("StepsLabel", stepCountString);
//Get and show the date
var now = Time.now();
var info = Calendar.info(now, Time.FORMAT_LONG);
var dateString = Lang.format("$1$ $2$ $3$", [info.day_of_week, info.day, info.month]);
updateDisplayObject("DateLabel", dateString);

Now, when we hit F5 to run the watch face, you can see how the display has changed and we have built the watch layout:

The built simple watch layout

Hope this helps someone out as the documentation is a little lacking. Enjoy! 🎉 🎉

Additional Notes – You can also confirm the languages your watch face will support by using the Monkey C: Edit Languages option.

Office Click-To-Run and XML Files

So, it used to be that we would install Office using a batch script that would invoke a setup.exe, assign a specific /configure flag and manually assign a specific XML file that contained the product that we wanted to install. This was bulky. It got too bulky when we needed to install 32-bit and 64-bit versions.

TIME FOR A CHANGE!

This is when I started thinking: “wow I really hate batch. I’m really glad I’m not the one that had to write this old script. Lets PowerShell this shit!”

First I needed a template XML file to modify, So this is what that looks like:

<Configuration>
  <Add OfficeClientEdition="64" Channel="Current">
    <Product ID="O365BusinessRetail">
      <Language ID="MatchOS" />
    </Product>
  </Add>
</Configuration>

This is the file that we will edit to say which product we want installing also if we want 64-bit or 32-bit.

Next, I needed to create a PowerShell script that would take a user’s input, edit the XML file accordingly and start the setup.exe with this flag. I also needed the bit-version that they wanted.

I started by defining the variables I would need for the script:

#Variables used for the installation
$bitVersion = ""
$officeProduct = ""
$pathToOffice = "\\path\to\office\folder"
$xmlFile = "OfficeXML.xml"
$pathToXMLFile = Join-Path -Path $pathToOffice -ChildPath $xmlFile

Then I created a function I would use to update the XML file. I needed two parameters, the product that they wanted installing and the bit version they wanted:

#Updates the XML file based on the input
function Update-XMLFile([string]$product, [string]$bit){

    try{
        #Loading the XML document
        $xmlDoc = Get-Content -Path $pathToXMLFile

        #Edit the document
        $xmlDoc.Configuration.Add.OfficeClientEdition = $bit
        $xmlDoc.Configuration.Add.Product.ID = $product

        #Save the document
        $xmlDoc.Save($pathToXMLFile)
    }catch{
        $errorMessage = $_.Exception.Message
        Write-Host $errorMessage -ForegroundColor Red
        Read-Host "The script encountered the above error - will now exit"
    }
}

I then created another function to start the installation. This also required two parameters, the bit version and the XML file name

#Function to start the installation
function Start-Installation([string]$bit, [string]$xmlName){
    try{
        .\setup.exe /configure $bit\$xmlName
    }catch{
        $errorMessage = $_.Exception.Message
        Write-Host $errorMessage
        Read-Host "The script encountered the above error - will now exit"
    }
}

My final function was a verification test. Since we want to only use 64-bit for future installations, I had to make sure that whoever was using the script knew this and would be competent enough to do a little bit of math:

#Function to check the user wants 32 bit
function Get-Verification(){
    $output = $false

    Write-Host "Are you sure you want to install 32-bit?" -ForegroundColor Red
    Write-Host "All new installs should use 64-bit instead"
    Write-Host "If you want to install 32-bit, complete the test below, otherwise enter the wrong answer"

    $firstNumber = Get-Random -Minimum 1 -Maximum 11
    $secondNumber = Get-Random -Minimum 1 -Maximum 11

    $sumToCheck = $firstNumber + $secondNumber

    $verificationInput = Read-Host "$($firstNumber) + $($secondNumber) = ?"

    if ($verificationInput -eq $sumToCheck){
        Write-Host "Fine! 32-bit will be installed..."
        $output = $true
    }else{
        Write-Host "Finally! 64-bit will be installed"
        $output = $false
    }
    return $output
}

Now that all my functions were defined, I could start with the actual meat of the script. This included cleaning the screen, asking the user some questions, launching the 32-bit verification is needed, updating the XML file using a switch statement and finally kicking off the installation. Heres what that looked like:

#Clear the screen
Clear-Host

#region Checking if the user wants 64 bit or 32 bit

do{

    Write-Host "Do you want" -NoNewline
    Write-Host " 64-bit " -NoNewline -ForegroundColor Yellow
    Write-Host "or" -NoNewline
    Write-Host " 32-bit " -NoNewline -ForegroundColor Green
    Write-Host "? (64 or 32): " -NoNewline
    $bitVersionInput = (Read-Host).ToUpper()
}while((64 ,32) -notcontains $bitVersionInput)

#endregion

#Check the user definitely wants 32 bit
if ($bitVersionInput -eq "32"){
    if (Get-Verification){
        $bitVersion = $bitVersionInput
    }else{
        $bitVersionInput = "64"
    }
}

#Update the bitVersion variable
$bitVersion = $bitVersionInput

#region Asking what product to install

#Ask the user what product they want to install
Write-Host @"

Please select one product from the below list

"@

Write-Host @"
1) Business Retail
2) ProPlus Retail

"@ -ForegroundColor Cyan

Write-Host @"
3) Visio Std Volume
4) Visio Pro Volume
5) Visio Pro Retail

"@ -ForegroundColor Green

Write-Host @"
6) Project Std Volume
7) Project Pro Volume
8) Project Pro Retail

"@ -ForegroundColor Gray

Write-Host @"
C) Cancel

"@ -ForegroundColor Red

do{
    $officeProductInput = (Read-Host "Enter a number").ToUpper()
}while((1,2,3,4,5,6,7,8, "C") -notcontains $officeProductInput)

#endregion

#Update the product variable
$officeProduct = $officeProductInput

#region Switch the input to see what it is and perform the required operation

switch($officeProduct){
    
    #Business Retail
    1 { Update-XMLFile -product "O365BusinessRetail" -bit $bitVersion}
    #ProPlus
    2 { Update-XMLFile -product "O365ProPlusRetail" -bit $bitVersion}
    #Visio Std Volume
    3 { Update-XMLFile -product "VisioStd2019Volume" -bit $bitVersion}
    #Visio Pro Volume
    4 { Update-XMLFile -product "VisioPro2019Volume" -bit $bitVersion}
    #Visio Pro Retail
    5 { Update-XMLFile -product "VisioPro2019Retail" -bit $bitVersion}
    #Project Std Volume
    6 { Update-XMLFile -product "ProjectStd2019Volume" -bit $bitVersion}
    #Project Pro Volume
    7 { Update-XMLFile -product "ProjectPro2019Volume" -bit $bitVersion}
    #Project Pro Retail
    8 { Update-XMLFile -product "ProjectPro2019Retail" -bit $bitVersion}
    #Cancel
    "C" {Exit}
    default {Exit}
}

#endregion

#Start the installation
Write-Host "Installing..." -ForegroundColor Green
Start-Installation -bit $bitVersion -xmlName $xmlFile
Write-Host "This window can be closed"
Read-Host

Done!

If you’re wondering what the script looks like as a whole, wonder no longer:

#Variables used for the installation
$bitVersion = ""
$officeProduct = ""
$pathToOffice = "\\sandpdc\software\Office"
$xmlFile = "OfficeXML.xml"
$pathToXMLFile = Join-Path -Path $pathToOffice -ChildPath $xmlFile

#Updates the XML file based on the input
function Update-XMLFile([string]$product, [string]$bit){

    try{
        #Loading the XML document
        $xmlDoc = Get-Content -Path $pathToXMLFile

        #Edit the document
        $xmlDoc.Configuration.Add.OfficeClientEdition = $bit
        $xmlDoc.Configuration.Add.Product.ID = $product

        #Save the document
        $xmlDoc.Save($pathToXMLFile)
    }catch{
        $errorMessage = $_.Exception.Message
        Write-Host $errorMessage -ForegroundColor Red
        Read-Host "The script encountered the above error - will now exit"
    }
}

#Function to start the installation
function Start-Installation([string]$bit, [string]$xmlName){
    try{
        .\setup.exe /configure $bit\$xmlName
    }catch{
        $errorMessage = $_.Exception.Message
        Write-Host $errorMessage
        Read-Host "The script encountered the above error - will now exit"
    }
}

#Function to check the user wants 32 bit
function Get-Verification(){
    $output = $false

    Write-Host "Are you sure you want to install 32-bit?" -ForegroundColor Red
    Write-Host "All new installs should use 64-bit instead"
    Write-Host "If you want to install 32-bit, complete the test below, otherwise enter the wrong answer"

    $firstNumber = Get-Random -Minimum 1 -Maximum 11
    $secondNumber = Get-Random -Minimum 1 -Maximum 11

    $sumToCheck = $firstNumber + $secondNumber

    $verificationInput = Read-Host "$($firstNumber) + $($secondNumber) = ?"

    if ($verificationInput -eq $sumToCheck){
        Write-Host "Fine! 32-bit will be installed..."
        $output = $true
    }else{
        Write-Host "Finally! 64-bit will be installed"
        $output = $false
    }
    return $output
}

#Clear the screen
Clear-Host

#region Checking if the user wants 64 bit or 32 bit

do{

    Write-Host "Do you want" -NoNewline
    Write-Host " 64-bit " -NoNewline -ForegroundColor Yellow
    Write-Host "or" -NoNewline
    Write-Host " 32-bit " -NoNewline -ForegroundColor Green
    Write-Host "? (64 or 32): " -NoNewline
    $bitVersionInput = (Read-Host).ToUpper()
}while((64 ,32) -notcontains $bitVersionInput)

#endregion

#Check the user definitely wants 32 bit
if ($bitVersionInput -eq "32"){
    if (Get-Verification){
        $bitVersion = $bitVersionInput
    }else{
        $bitVersionInput = "64"
    }
}

#Update the bitVersion variable
$bitVersion = $bitVersionInput

#region Asking what product to install

#Ask the user what product they want to install
Write-Host @"

Please select one product from the below list

"@

Write-Host @"
1) Business Retail
2) ProPlus Retail

"@ -ForegroundColor Cyan

Write-Host @"
3) Visio Std Volume
4) Visio Pro Volume
5) Visio Pro Retail

"@ -ForegroundColor Green

Write-Host @"
6) Project Std Volume
7) Project Pro Volume
8) Project Pro Retail

"@ -ForegroundColor Gray

Write-Host @"
C) Cancel

"@ -ForegroundColor Red

do{
    $officeProductInput = (Read-Host "Enter a number").ToUpper()
}while((1,2,3,4,5,6,7,8, "C") -notcontains $officeProductInput)

#endregion

#Update the product variable
$officeProduct = $officeProductInput

#region Switch the input to see what it is and perform the required operation

switch($officeProduct){
    
    #Business Retail
    1 { Update-XMLFile -product "O365BusinessRetail" -bit $bitVersion}
    #ProPlus
    2 { Update-XMLFile -product "O365ProPlusRetail" -bit $bitVersion}
    #Visio Std Volume
    3 { Update-XMLFile -product "VisioStd2019Volume" -bit $bitVersion}
    #Visio Pro Volume
    4 { Update-XMLFile -product "VisioPro2019Volume" -bit $bitVersion}
    #Visio Pro Retail
    5 { Update-XMLFile -product "VisioPro2019Retail" -bit $bitVersion}
    #Project Std Volume
    6 { Update-XMLFile -product "ProjectStd2019Volume" -bit $bitVersion}
    #Project Pro Volume
    7 { Update-XMLFile -product "ProjectPro2019Volume" -bit $bitVersion}
    #Project Pro Retail
    8 { Update-XMLFile -product "ProjectPro2019Retail" -bit $bitVersion}
    #Cancel
    "C" {Exit}
    default {Exit}
}

#endregion

#Start the installation
Write-Host "Installing..." -ForegroundColor Green
Start-Installation -bit $bitVersion -xmlName $xmlFile
Write-Host "This window can be closed"
Read-Host

 

Button Masher Game in WPF and C#

I’ve been trying to learn WPF with C# as the backend instead of PowerShell. Since there’s a lot more information on using C# over PowerShell, I decided I would take the plunge and learn as much as I could to create some apps.

This is the first app that I completed. In all honesty, it only took me around 2 hours to get the functionality completed. I did this whilst feeling ill and waiting for the wife to get back from work. I was really surprised by how easy it was for me to understand and write in C# and I was quite proud of the final outcome.

The app will probably look really different on your screen but it looks really nice on my computer. This is what the app looks like once it’s loaded:

The goal of the game is for one person to mash the A button as fast as possible and for another person to mash the B button as fast as possible. The first person to mash their respective buttons 100 times is the winner. It then displays a winning screen and allows the users to play again:

Here is a little GIF of what the game looks and feels like:

And here is the download for the game’s exe file:

ButtonMasher

Now for the nitty gritty stuff. I have included the XML and C# code used for the project below and will also include them as downloads:

XML

<Window x:Class="ButtonMasher.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ButtonMasher"
        mc:Ignorable="d"
        Title="Button Masher" Width="450" MinHeight="145" MinWidth="300" KeyDown="WindowKeyDown" SizeToContent="Height" Icon="Icon.ico">
    <Window.Background>
        <LinearGradientBrush StartPoint="-1,-1" EndPoint="1.5,1.5" >
            <GradientStop Color="#FF018A8F" Offset="0.41" />
            <GradientStop Color="#FF00FFD6" Offset="1" />
        </LinearGradientBrush>
    </Window.Background>
    <Grid Name="Grid">
        <Grid.Background>
            <ImageBrush ImageSource="Winner_Background.jpg" Stretch="UniformToFill" Opacity="0" />
        </Grid.Background>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*"/>
            <ColumnDefinition Width="1*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Label Visibility="Visible" Name="PlayerOneLabel" Content="0" FontSize="46" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="White" FontFamily="Arial"  />
        <Label Visibility="Visible" Name="PlayerTwoLabel" Content="0" FontSize="46" HorizontalAlignment="Center" VerticalAlignment="Center" Grid.Column="1" Foreground="White" FontFamily="Arial"/>
        <ProgressBar Visibility="Visible" Name="ProgressBar1" Margin="10,5" Background="Transparent" BorderBrush="White" BorderThickness="2" Minimum="0" Maximum="100" Value="0" HorizontalAlignment="Stretch" Height="20"  Grid.Row="1" VerticalAlignment="Center" >
            <ProgressBar.Foreground>
                <LinearGradientBrush EndPoint="0,0.5" StartPoint="1,0.5">
                    <GradientStop Color="#83EAF1" Offset="0"/>
                    <GradientStop Color="#63A4FF" Offset="1"/>
                </LinearGradientBrush>
            </ProgressBar.Foreground>
        </ProgressBar>
        <ProgressBar Visibility="Visible" Name="ProgressBar2" Margin="10,5" Background="Transparent" BorderBrush="White" BorderThickness="2" Minimum="0" Maximum="100" Value="0" HorizontalAlignment="Stretch" Height="20" Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" RenderTransformOrigin="0.5,0.5"  >
            <ProgressBar.Foreground>
                <LinearGradientBrush EndPoint="0,0.5" StartPoint="1,0.5">
                    <GradientStop Color="#83EAF1" Offset="0"/>
                    <GradientStop Color="#63A4FF" Offset="1"/>
                </LinearGradientBrush>
            </ProgressBar.Foreground>
            <ProgressBar.RenderTransform>
                <TransformGroup>
                    <ScaleTransform/>
                    <SkewTransform/>
                    <RotateTransform Angle="180"/>
                    <TranslateTransform/>
                </TransformGroup>
            </ProgressBar.RenderTransform>
        </ProgressBar>
        <TextBlock Name="RulesTextBlock" MouseUp="ToggleRules" Visibility="Visible" Grid.Row="2" VerticalAlignment="Bottom" HorizontalAlignment="Left" FontFamily="Arial" Foreground="White" FontSize="10" Cursor="Hand" >
            See Rules
        </TextBlock>
        <TextBlock Name="WonLabel" Text="Winner" Padding="0,40,0,0" Visibility="Collapsed" Grid.ColumnSpan="2" FontSize="46" HorizontalAlignment="Center" VerticalAlignment="Bottom" FontWeight="Bold" TextDecorations="{x:Null}" Foreground="#FFFECE26" FontFamily="Arial">
            <TextBlock.Effect>
                <DropShadowEffect BlurRadius="1" Direction="320" ShadowDepth="2"/>
            </TextBlock.Effect>
        </TextBlock>
        <Border Name="PlayAgainBorder" Visibility="Collapsed" Grid.Row="1" Cursor="Hand" Grid.ColumnSpan="2" CornerRadius="10" Margin="0,0,0,10" HorizontalAlignment="Center" VerticalAlignment="Center" MouseUp="PlayAgain">
            <Border.Style>
                <Style>
                    <Setter Property="Border.Background" Value="#FF494949" />
                    <Setter Property="Border.BorderThickness" Value="3"/>
                    <Style.Triggers>
                        <Trigger Property="Border.IsMouseOver" Value="True" >
                            <Setter Property="Border.Background" Value="#2ecc71"/>
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </Border.Style>
            <Label Content="Play Again" VerticalAlignment="Center" Foreground="White" HorizontalAlignment="Center" VerticalContentAlignment="Center" FontSize="16" FontFamily="Arial"/>
        </Border>
        <TextBlock Name="RulesSection" Foreground="White" Grid.Row="3" Grid.ColumnSpan="2" TextWrapping="Wrap" TextAlignment="Center" Padding="20" Visibility="Collapsed" FontFamily="Arial" FontSize="14">
            The rules are simple, player one (left side) has to mash the 'A' button as fast as possible while player two (right side) has to mash the 'B' button as fast as possible. The first one to reach 100 button "mashes" wins the game!
        </TextBlock>
    </Grid>
</Window>

C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace ButtonMasher
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        //player one score
        int player1Score = 0;
        int player2Score = 0;

        //LOAD FORM
        public MainWindow()
        {
            InitializeComponent();
        }

        //UPDATE PLAYER ONE SCORE
        void UpdatePlayerOneScore()
        {
            player1Score++;
            PlayerOneLabel.Content = player1Score;
        }

        //UPDATE PLAYER TWO SCORE
        void UpdatePlayerTwoScore()
        {
            player2Score++;
            PlayerTwoLabel.Content = player2Score;
        }

        //SET WON SCREEN VISIBILITY
        void WonScreenVisibility(bool visible)
        {
            if (visible)
            {
                //show won screen
                PlayerOneLabel.Visibility = Visibility.Collapsed;
                PlayerTwoLabel.Visibility = Visibility.Collapsed;
                ProgressBar1.Visibility = Visibility.Collapsed;
                ProgressBar2.Visibility = Visibility.Collapsed;
                RulesTextBlock.Visibility = Visibility.Collapsed;
                RulesSection.Visibility = Visibility.Collapsed;
                WonLabel.Visibility = Visibility.Visible;
                PlayAgainBorder.Visibility = Visibility.Visible;
                Grid.Background.Opacity = 100;
                MinWidth = 350;
                MinHeight = 170;
            }
            else
            {
                //hide won screen
                PlayerOneLabel.Visibility = Visibility.Visible;
                PlayerTwoLabel.Visibility = Visibility.Visible;
                ProgressBar1.Visibility = Visibility.Visible;
                ProgressBar2.Visibility = Visibility.Visible;
                RulesTextBlock.Visibility = Visibility.Visible;
                RulesSection.Visibility = Visibility.Collapsed;
                WonLabel.Visibility = Visibility.Collapsed;
                PlayAgainBorder.Visibility = Visibility.Collapsed;
                Grid.Background.Opacity = 0;
                MinWidth = 300;
                MinHeight = 145;
            }
            //switch "show" or "hide" in rules textblock
            if (RulesSection.Visibility == Visibility.Visible)
            {
                RulesTextBlock.Text = "Hide Rules";
            }
            else
            {
                RulesTextBlock.Text = "Show Rules";
            }
        }

        //WON SCREEN
        void WonScreen(string player)
        {
            if(player == "1")
            {
                WonScreenVisibility(true);
                WonLabel.Text = "Player 1 won!";
            }else if(player == "2")
            {
                WonScreenVisibility(true);
                WonLabel.Text = "Player 2 won!";
            }
        }

        //RESET SCORES AND BARS
        void Reset()
        {
            player1Score = 0;
            player2Score = 0;
            PlayerOneLabel.Content = player1Score;
            PlayerTwoLabel.Content = player2Score;
            ProgressBar1.Value = player1Score;
            ProgressBar2.Value = player2Score;
        }

        //USER PRESSES BUTTON FORM
        private void WindowKeyDown(object sender, KeyEventArgs e)
        {
            //checking which key was pressed
            if (e.Key == Key.A)
            {
                UpdatePlayerOneScore(); 
            }else if(e.Key == Key.L)
            {
                UpdatePlayerTwoScore();
            }

            //calculating progress bar value
            ProgressBar1.Value = player1Score;
            ProgressBar2.Value = player2Score;

            //checking if anyone won
            if (ProgressBar1.Value > 99)
            {
                //player one has won
                WonScreen("1");

            }
            else if (ProgressBar2.Value > 99)
            {
                //player two has won
                WonScreen("2");
            }            
        }        

        //PLAY AGAIN
        private void PlayAgain(object sender, RoutedEventArgs e)
        {
            WonScreenVisibility(false);
            Reset();
        }

        //TOGGLE RULES
        void ToggleRules(object sender, RoutedEventArgs e)
        {
            if (RulesSection.Visibility == Visibility.Collapsed)
            {
                //show rules
                RulesSection.Visibility = Visibility.Visible;
                RulesTextBlock.Text = "Hide Rules";
                SizeToContent = SizeToContent.Height;
                MinHeight = 265;
            }
            else
            {
                //hide rules
                RulesSection.Visibility = Visibility.Collapsed;
                RulesTextBlock.Text = "Show Rules";
                SizeToContent = SizeToContent.Height;
                MinHeight = 145;
            }
        }
    }
}

I really hope you enjoy this little game that I created. Please feel free to use it, change it and distribute your own version at will 🙂

Enjoy!

Building Cleaner, Responsive WPF Forms

In my first two posts on this subject, I was just getting started with learning responsive WPF form building. I’m here today to show you a better way to build a responsive WPF using runspaces that will do the exact same thing as my previous uploads showed. Just better.

This time, I won’t be putting the form into its own runspace. As I learnt you didn’t need to from JRV over on the TechNet forums. This has some benefits that I will very helpfully, briefly and probably incorrectly list below:

  • Don’t have to use syncHash when updating the form
  • One less runspace for the form
  • Having a form in its own runspace creates additional overhead and possible errors

So to display the form I would use something like the below:

$xml = @"
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    Title="Counter" Height="119" Width="351.5" ResizeMode="CanMinimize" WindowStartupLocation="CenterScreen">
    <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
        <Label Name="Label" Content="0" HorizontalAlignment="Left" Margin="16.666,9.333,0,0" VerticalAlignment="Top" FontSize="18"/>
        <Button Name="Button" Content="Start" HorizontalAlignment="Center" VerticalAlignment="Top" Width="75" Margin="123.25,63,123.25,0"/>
    </Grid>
</Window>
"@

$Reader=(New-Object System.Xml.XmlNodeReader $xml)
$Window=[Windows.Markup.XamlReader]::Load($Reader)

$Label = $Window.FindName("Label")
$Button = $Window.FindName("Button")

$Window.ShowDialog() | Out-Null

Which will give us the below form:

But what if I want the button press to make the label number increase? I would use something like this on the button press:

$xml = @"
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    Title="Counter" Height="119" Width="351.5" ResizeMode="CanMinimize" WindowStartupLocation="CenterScreen">
    <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
        <Label Name="Label" Content="0" HorizontalAlignment="Left" Margin="16.666,9.333,0,0" VerticalAlignment="Top" FontSize="18"/>
        <Button Name="Button" Content="Start" HorizontalAlignment="Center" VerticalAlignment="Top" Width="75" Margin="123.25,63,123.25,0"/>
    </Grid>
</Window>
"@

$Reader=(New-Object System.Xml.XmlNodeReader $xml)
$Window=[Windows.Markup.XamlReader]::Load($Reader)

$Label = $Window.FindName("Label")
$Button = $Window.FindName("Button")

$Button.Add_Click({
    $counter = 1 

    do{
        Start-Sleep -Milliseconds 5
        $label.content = $counter
        [System.Windows.Forms.Application]::DoEvents()
        $counter += 1
    }while ($counter -le 5000)

})

$Window.ShowDialog() | Out-Null

This produces a form which increases the label up to 5000 when the button is pressed. You can see this below:

But what if I want to actually run something in a runspace. For example, test the connection to google.com? Then I would use the below code:

$xml = @"
<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    Title="Counter" Height="119" Width="351.5" ResizeMode="CanMinimize" WindowStartupLocation="CenterScreen">
    <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" >
        <Label Name="Label" Content="0" HorizontalAlignment="Left" Margin="16.666,9.333,0,0" VerticalAlignment="Top" FontSize="18"/>
        <Button Name="Button" Content="Start" HorizontalAlignment="Center" VerticalAlignment="Top" Width="75" Margin="123.25,63,123.25,0"/>
    </Grid>
</Window>
"@

$Reader=(New-Object System.Xml.XmlNodeReader $xml)
$Window=[Windows.Markup.XamlReader]::Load($Reader)

$Label = $Window.FindName("Label")
$Button = $Window.FindName("Button")

$Button.Add_Click({
    $syncHash = [hashtable]::Synchronized(@{})
    $Runspace = [runspacefactory]::CreateRunspace()
    $Runspace.ApartmentState = "STA"
    $Runspace.ThreadOptions = "ReuseThread"
    $Runspace.Open()
    $Runspace.SessionStateProxy.SetVariable("syncHash",$syncHash)

    $powershell = ::Create().AddScript({
        $connection = Test-Connection -ComputerName google.com -Count 5
        $syncHash.output = [math]::Round(($connection.ResponseTime | Measure-Object -Average).Average)
    })

    $powershell.Runspace = $Runspace

    $Object = $powershell.BeginInvoke()

    do {
        Start-Sleep -Milliseconds 50
        [System.Windows.Forms.Application]::DoEvents()
    }while(!$Object.IsCompleted)

    $powershell.EndInvoke($Object)
    $powershell.Dispose()

    $label.Content = $syncHash.output
})

$Window.ShowDialog() | Out-Null

All the above examples will stay responsive whilst the action is performed. There are a couple of different methods to do this as you can see above, for anything that takes some time to complete, a runspace is needed. But when you are updating the form quickly, like the counter, then no runspace is needed. 

Enjoy!