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:
- Set the EndAngle property
- 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.
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.