WPF: Building a speedometer

Intro

I absolutely love WPF. I have been building apps in it for almost seven years now and always learn something new.

This article discusses how I learned to build a speedometer.

 

Adding the Expression Namespace

When building a speedometer, I’ve learned that I can leverage the Expression library’s Arc class.

To do this,  we first need to import the namespace:

xmlns:ed="http://schemas.microsoft.com/expression/2010/drawing"

I have the following:

<Window x:Class="MotoLens.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:ed="http://schemas.microsoft.com/expression/2010/drawing"         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:MotoLens" mc:Ignorable="d" Background="Black" Width="500" Title="MainWindow" >

Creating the Speedometer

To build a speedometer, I learned that I can leverage two arcs. The first arc will serve as the base. It will convey the range of the speedometer. However, it will do nothing more. The second arc will convey the real-time data and will thus overlay the base arc.

Consider the following XAML:

        <Grid Grid.Row="0" Margin="0,0,0,-120">
            <ed:Arc Style="{StaticResource ArcStyle}" EndAngle="120" Stroke="#FF484D5F"/>

            <ed:Arc Style="{StaticResource ArcStyle}" EndAngle="{Binding ElementName=Slider, Path=Value, Converter={StaticResource SpeedConverter}, ConverterParameter=-120}"                     Stroke="{Binding ElementName=Slider, Path=Value, Converter={StaticResource ValueToBrushConverter}, ConverterParameter=0~120}"/>
        </Grid>

Our base arc is the top-most arc within our XAML. We then provide another arc that does two things:

  1. Set the EndAngle property
  2. Set the Stroke property

Setting the EndAngle of an Arc

In the arc that reflects the real-time data, we need to adjust the amount of bars that are displayed to reflect speed.

Consider the following:

EndAngle="{Binding ElementName=Slider, Path=Value, Converter={StaticResource SpeedConverter}, ConverterParameter=-120}"

Slider is the name of a slider control that hasn’t been introduced yet.
It is defined as follows:

<Slider Grid.Row="0" x:Name="Slider" Minimum="0" Maximum="120" Background="{Binding RelativeSource={RelativeSource Self}, Path=Value, Converter={StaticResource ValueToBrushConverter}, ConverterParameter=0~100}"                  TickFrequency="1" IsSnapToTickEnabled="True" TickPlacement="TopLeft" Margin="0,50,0,0" Height="15" VerticalAlignment="Bottom" HorizontalAlignment="Stretch" />

Setting the Stroke of an Arc

The stroke property is really the color of our arc. In this implementation, we will update the color of our arc (aka: speedometer) based on the value of our slider control.

Stroke="{Binding ElementName=Slider, Path=Value, Converter={StaticResource ValueToBrushConverter}, ConverterParameter=0~120}"/>

Note how there’s yet another value converter used named ValueToBrushConverter.
The implementation of ValueToBrushConverter is as follows:

using System;
using System.Globalization;
using System.Windows.Data;
using System.Windows.Media;

namespace MotoLens
{
    class ValueToBrushConverter : IValueConverter
    {
        static readonly Color[] _colorTable =
            {
            Color.FromRgb(  0, 255, 255),
            Color.FromRgb(  0, 255,   0),
            Color.FromRgb(255, 255,   0),
            Color.FromRgb(255,   0,   0),
            };

        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var args = parameter as string;
            var minimumInput = int.Parse(args.Split('~')[0]);
            var maximumInput = int.Parse(args.Split('~')[1]);

            var currentValue = ((double)value - minimumInput) / (maximumInput - minimumInput);
            var col1 = (int)(currentValue * (_colorTable.Length - 1));
            var col2 = Math.Min(col1 + 1, (_colorTable.Length - 1));

            var t = 1.0 / (_colorTable.Length - 1);
            return new SolidColorBrush(Lerp(_colorTable[col1], _colorTable[col2], (currentValue - t * col1) / t));
        }

        public static Color Lerp(Color col1, Color col2, double t)
        {
            var r = col1.R * (1 - t) + col2.R * t;
            var g = col1.G * (1 - t) + col2.G * t;
            var b = col1.B * (1 - t) + col2.B * t;
            return Color.FromRgb((byte)r, (byte)g, (byte)b);
        }
        ...
    }
}

Honestly, the above code is too complex for me to articulate. Just experiment with it.

Conveying Speed

Now in this example, we bind the EndAngle property to the slider’s Value property and use a value-converter to dictate the number of bars to display.

Consider the following SpeedConverter:

    class SpeedConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var startAngle = double.Parse(parameter as string);
            var endAngle = startAngle + (((double)value) * 2);
            return endAngle;
        }
        ...
    }

In the above code, we take the starting angle of our base arc and based on the current value that’s derived from our slider, determine the EndAngle of our arc. Thus, it’s this value that reflects the real-time data within our UI.

That’s essentially it. The rest of the code reflects more of the business requirements than it does how a speedometer is built.

Full XAML:

<Window x:Class="MotoLens.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:ed="http://schemas.microsoft.com/expression/2010/drawing"         xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"         xmlns:local="clr-namespace:MotoLens"         mc:Ignorable="d"         Background="Black"         Width="500"         Title="MainWindow" >

    <Window.Resources>
        <local:ValueToBrushConverter x:Key="ValueToBrushConverter" />
        <local:SpeedConverter x:Key="SpeedConverter" />
        <Style x:Key="ArcStyle" TargetType="ed:Arc">
            <Setter Property="StartAngle" Value="-120" />
            <Setter Property="Stretch" Value="None" />
            <Setter Property="Opacity" Value="{Binding GaugeOpacity}" />
            <Setter Property="Height" Value="300" />
            <Setter Property="Width" Value="300" />
            <Setter Property="StrokeThickness" Value="20" />
            <Setter Property="StrokeDashArray" Value=".25" />
        </Style>

    </Window.Resources>

    <Window.DataContext>
        <local:ViewModel />
    </Window.DataContext>

    <Grid Background="#FF2E2F45">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>

        <Grid Grid.Row="0" Margin="0,0,0,-120">
            <ed:Arc Style="{StaticResource ArcStyle}" EndAngle="120" Stroke="#FF484D5F"/>

            <ed:Arc Style="{StaticResource ArcStyle}" EndAngle="{Binding ElementName=Slider, Path=Value, Converter={StaticResource SpeedConverter}, ConverterParameter=-120}"                     Stroke="{Binding ElementName=Slider, Path=Value, Converter={StaticResource ValueToBrushConverter}, ConverterParameter=0~120}"/>
        </Grid>

        <StackPanel Grid.Row="0" HorizontalAlignment="Center" VerticalAlignment="Center" Margin="0,0,0,-300">
            <TextBlock Text="mph" Foreground="#FF878A9F" Opacity="{Binding MphOpacity}" HorizontalAlignment="Center" FontSize="14"/>
            <TextBlock x:Name="SpeedLabel" Text="{Binding ElementName=Slider, Path=Value}" FontWeight="Bold" Foreground="{Binding ElementName=Slider, Path=Background}" Opacity="{Binding MphOpacity}" HorizontalAlignment="Center" FontSize="36"  Margin="0,-5,0,0"/>
        </StackPanel>

        <Grid Grid.Row="1" HorizontalAlignment="Stretch">
            <Grid.RowDefinitions>
                <RowDefinition />
                <RowDefinition />
            </Grid.RowDefinitions>

            <Slider Grid.Row="0" x:Name="Slider" Minimum="0" Maximum="120" Background="{Binding RelativeSource={RelativeSource Self}, Path=Value, Converter={StaticResource ValueToBrushConverter}, ConverterParameter=0~100}"                  TickFrequency="1" IsSnapToTickEnabled="True" TickPlacement="TopLeft" Margin="0,50,0,0" Height="15" VerticalAlignment="Bottom" HorizontalAlignment="Stretch" />

            <Button Grid.Row="1" Content="Shake" Command="{Binding Clarity}" Width="80" Height="40" />
        </Grid>
    </Grid>
</Window>

ViewModel

namespace MotoLens
{
    public partial class ViewModel : ViewModelBase
    {
        public ViewModel()
        {
            Clarity = new DelegateCommand(_ => ToggleVision());
        }

        float _mphOpacity = 1;
        public float MphOpacity
        {
            get { return _mphOpacity; }
            set
            {
                if (_mphOpacity != value)
                {
                    _mphOpacity = value;
                    OnPropertyChanged();
                }

            }
        }

        float _gaugeOpacity = 1f;
        public float GaugeOpacity
        {
            get { return _gaugeOpacity; }
            set
            {
                if (_gaugeOpacity != value)
                {
                    _gaugeOpacity = value;
                    OnPropertyChanged();
                }

            }
        }
    }
}

ViewModel.internal

namespace MotoLens
{
    public partial class ViewModel : ViewModelBase
    {
        void ToggleVision()
        {
            _shakeToggle = !_shakeToggle;
 
            if (_shakeToggle)
                MphOpacity = GaugeOpacity = FULL_CLARITY;
 
            else
                MphOpacity = GaugeOpacity = LOW_CLARITY;
        }
    }
}

ViewModel.commands

using Bizmonger.Patterns;
 
namespace MotoLens
{
    public partial class ViewModel
    {
        public DelegateCommand Clarity { get; }
    }
}

ViewModel.members

namespace MotoLens
{
    public partial class ViewModel
    {
        const float FULL_CLARITY = 1;
        const float LOW_CLARITY = .05f;
        bool _shakeToggle;
    }
}

Conclusion

In conclusion, I have described the code required for implementing a speedometer. Most of the code was XAML related for the actual speedometer. The rest of the code targeted business requirements.

You can see the solution on GitHub.

Advertisements
2 comments
  1. Adrian Hum said:

    Could you please add the reference to your patterns library to the GitHub project?

    Its coming up as a missing reference.

    Reagrds Adrian Hum

    • Hmm… I just recommitted the solution to GitHub.
      If the DelegateCommand is still undefined then just install the Bizmonger.Patterns dll from Nuget.

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

%d bloggers like this: