Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Custom Gauge Controls for Windows Phone 7: Part I

0.00/5 (No votes)
25 Feb 2013 2  
The first article in this series presents the implementation considerations and the way to use some custom gauges in WP7.

Introduction

This article is the first in a series that will try to present my implementation of some custom gauge controls for Windows Phone 7. This first article will present the various design considerations, the way to use the custom controls, and some advances configuration examples. The articles that follow in this series will go deeper into the problem and will try to explain the implementation of these controls (scales and indicators).

The articles in this series

Below, you can find a list of all the articles in this series:

Article content

A sneak preview

The following images present a sneak preview of what you can achieve by using the custom gauge controls:

Design considerations

In my opinion, a gauge should present a scale and permit multiple indicators to point to particular values within that scale. When I say “scale”, I mean everything a gauge should contain except for the indicators. This means that, in my opinion, a scale should contain ticks (minor and major), labels, and ranges. I use ranges to assign different colors to different intervals in the scale. These ranges can be used to indicate below optimal, optimal, or above optimal values.

In general terms, a scale should present the user with the following properties:

  • Minimum value – the minimum value in the scale. Indicators cannot point below this value.
  • Maximum value – the maximum value in the scale. Indicators cannot point above this value.
  • Minor tick step – small ticks will be placed at values that are multiples of the step, starting from but not including the minimum value. For example, small ticks will be placed at minimum + step, minimum + 2*step, etc.
  • Major tick step – big ticks will be placed at values that are multiples of the step, starting from the minimum value inclusively. For example, big ticks will be placed at minimum, minimum + step, etc. When the current value is a candidate for both small and big ticks, the big ticks will take precedence.
  • Ranges – these will indicate optimal ranges on the scale. For example, in a scale from 0 to 100, the user might specify that values below 50 are optimal values.
  • RangeThickness – specifies how thick the ranges should be.
  • UseDefaultRange – specifies whether or not to use a default range. This will have the effect of drawing a line below the ticks.
  • DefaultRangeColor - the color used for the default range if it is enabled.
  • DefaultTickColor - the color used to paint the default ticks and labels
  • UseRangeColorsForTicks – specifies whether or not to use the range colors to paint the ticks that are in a particular range. This means that if we have 3 ranges with 3 different colors and this property is set to true the ticks that are insinde each of these ranges will be painted in the color of the particular range. This property will apply only if the ticks and labels have the default templates.
  • Indicators – this collection will hold all the indicators for the particular scale.
  • Minor tick template, major tick template and label template – these properties specify the templates that will be used when displaying the ticks and the labels.

There are two kinds of scales: linear scales and radial scales. Each one of these will have additional properties that will be added to the ones mentioned above. This article will present implementations for both kinds of scales.

Linear scales

For linear scales, we will have the following additional properties:

  • Orientation – specifies whether the scale is horizontal or vertical.
  • Tick placement – specifies the tick placement on the scale. Here, the ticks can be above the indicators or below them for horizontal scales, or they can be to the left or to the right of the indicators for vertical scales.

Radial scales

For radial scales, we will have the following additional properties:

  • Minimum angle – specifies the minimum angle from which the rotation starts.
  • Maximum angle – specifies the maximum angle where the rotation stops.
  • Sweep direction – specifies the rotation direction.
  • EnableLabelRotation - specifies whether or not to rotate the labels to follow the contour of the control.
  • Tick placement – specifies the placement of the ticks. The ticks for a radial scale can be positioned inward or outward.
  • Radial type – concerning radial scales, there is an additional problem. We can have a full circle scale, a semicircle scale, or a quadrant scale. This option can be mainly used to save space on the screen. For example, if the user chooses a 90 degree range, maybe he doesn’t want to waste the other three quadrants of the circle. To save space, he can specify that he wants a quadrant. This property will be used in conjunction with the min, max angle and the sweep direction properties. This property will have an effect on the position of the center of rotation inside the area of the scale and on the range. If the angles and sweep direction don’t match, the user can choose to change them to get what he wants. This is easier than implementing complicated logic to determine the quadrant in code and coercing the angle values.

As you can see from the above properties, I have chosen to implement the tick placement in the derived classes. This is in order to further clarify the meaning of the properties. A single property in the base class with more options in the enumeration would have confused the user.

The image below presents the class hierarchy for the scale controls:

In the image above, you can see that the base class is abstract and that it is a Panel. The user can only instantiate derived instances.

Indicators

There can be quite a few kinds of indicators for both types of scales. We can have bar indicators (for both linear and radial scales), arrow indicators (for linear scales), marker indicators (for both linear and radial scales), or needle indicators (for radial scales). Indicators have 2 custom properties: the Value property and the Owner property. This last property identifies the scale that owns the indicator.

Bar indicators have two additional properties: BarThickness and BarBrush. Marker indicators have one additional property: MarkerTemplate.

The class hierarchy for the indicators can be seen in the image below:

Using the code

The scales and indicators are very easy to use. The first step is to add a reference to the assembly that contains the scales and reference that assembly in the page file to which you want to add the controls. Here is the assembly reference string:

xmlns:scada="clr-namespace:WPScadaControlsV2;assembly=WPScadaControlsV2"

After this, you can instantiate a scale in your page. The XAML below presents a two-row grid. Each row contains a scale. The top row contains a radial scale while the bottom row contains a linear scale.

<Grid x:Name="ContentGrid" Grid.Row="1" Margin="5" >
    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <scada:RadialScale></scada:RadialScale>
    <scada:LinearScale Grid.Row="1"/>
</Grid>

The effect of this code has can be seen in the image below:

As you can see from the image above, with very little code, you can get some very nice gauges. This is the default behavior for the two types of scales. In the following sections, I will talk about the customization options.

Basic tick and label customization

There are eight properties that you can use to customize the ticks and labels. These are: MinorTickStep, MajorTickStep, MinorTickTemplate, MajorTickTemplate, LabelTemplate, EnableLabelRotation, DefaultTickColor, and TickPlacement. The last property (TickPlacement) is implemented in the derived scale classes. We can have a linear or a radial tick placement.

The first tick customization example shows how to modify the number of ticks and labels that are shown in a scale. This number can be modified by using the MinorTickStep and MajorTickStep properties.

<scada:RadialScale MinorTickStep="10" MajorTickStep="20"></scada:RadialScale>
<scada:LinearScale Grid.Row="1" MinorTickStep="5" MajorTickStep="25"/>

The effect this code has can be seen below:

The next example will show how to modify the tick and label templates. The XAML can be seen below:

<scada:RadialScale Margin="5" UseDefaultRange="False" DefaultTickColor="Gold">
    <scada:RadialScale.MinorTickTemplate>
        <DataTemplate>
            <Ellipse Width="3" Height="3" Fill="White"/>
        </DataTemplate>
    </scada:RadialScale.MinorTickTemplate>
    <scada:RadialScale.MajorTickTemplate>
        <DataTemplate>
            <Ellipse Width="6" Height="6" Fill="Red"/>
        </DataTemplate>
    </scada:RadialScale.MajorTickTemplate>
</scada:RadialScale>
<scada:LinearScale Grid.Row="1" Margin="10" MajorTickStep="20" 
                   DefaultTickColor="Goldenrod">
    <scada:LinearScale.MinorTickTemplate>
        <DataTemplate>
            <Rectangle Width="2" Height="6" Fill="White"/>
        </DataTemplate>
    </scada:LinearScale.MinorTickTemplate>
    <scada:LinearScale.MajorTickTemplate>
        <DataTemplate>
            <Path Data="M0,0 L8,0 L4,12 Z" Fill="Red"/>
        </DataTemplate>
    </scada:LinearScale.MajorTickTemplate>
</scada:LinearScale>

The XAML above also makes use of the DefaultTickColor property. As the name suggests, this property is used to specify the tick default color. The effects of this code can be seen in the image below:

A lot of very nice effects can be obtained by modifying the tick templates. The XAML below presents an additional example:

<scada:RadialScale.MinorTickTemplate>
    <DataTemplate>
        <Rectangle Fill="Green" Width="5" Height="10"/>
    </DataTemplate>
</scada:RadialScale.MinorTickTemplate>
<scada:RadialScale.MajorTickTemplate>
    <DataTemplate>
        <Path Data="M0,0 L10,0 L5,20 Z" Fill="Purple"/>
    </DataTemplate>
</scada:RadialScale.MajorTickTemplate>
<scada:RadialScale.LabelTemplate>
    <DataTemplate>
        <TextBlock Text="{Binding}" FontWeight="Bold"
                   Foreground="Red"/>
    </DataTemplate>
</scada:RadialScale.LabelTemplate>

The image below presents the result of applying these templates to both a linear and a radial scale:

The next tick related property to talk about is the TickPlacement property. For the controls in the image above, the tick placement is set to the default values. These are Outward for radial scales and TopLeft for linear scales.

The image below presents the same controls with the TickPlacement properties set to Inward (for the radial scale) and BottomRight (for the linear scale).

The last property to talk about in regards to tick customization is the EnableLabelRotation property. This boolean property determines whether or not to rotate the labels to follow the scale's contour. By default, this property is set to true which means that the labels will be rotated. By setting this property to false, there will be no label rotation. In order to see the effect of this property, the image below presents two scales: one with the default value and one with the property set to false.

Working with ranges

There are five range related properties defined for the scales. These are: UseDefaultRange, DefaultRangeColor, Ranges, RangeThickness, and UseRangeColorsForTicks.

Most of the scales (linear or radial) you have seen so far have a line drawn beneath or above the ticks. This line represents the default scale range. The linear and radial scales also give the user the possibility to add additional ranges. The ranges could represent optimal ranges. The use of the default range can be selected by using the UseDefaultRange boolean property. If we set this to false for both the controls in the previous image, we get the result that can be seen in the image below:

In regards to the default range, the user can also use the DefaultRangeColor property. As the name suggests, this property determines the color of the default range if this range is in use. The image below presents a scale that sets this property. This image can be compared to the ones above that have the default value.

This above scale has been built using the code below.

<scada:RadialScale Grid.RowSpan="2" Grid.ColumnSpan="2" 
    RangeThickness="5" MinorTickStep="5" MajorTickStep="10"
    MinAngle="-90" MaxAngle="180" EnableLabelRotation="False"
    DefaultRangeBrush="{StaticResource PhoneAccentBrush}" >
</scada:RadialScale>

These two default range properties (UseDefaultRange and DefaultRangeColor) can be replicated by using custom ranges, as you will see in the following paragraphs.

Besides this default range, the user can add additional ranges by using the Ranges property. The XAML below shows a collection of three ranges. For each range, the user can set the color and the maximum value. Also, the user can set the RangeThickness property of the scale to control how thick the ranges should be.

<scada:RadialScale.Ranges>
    <scada:GaugeRange Color="Green" Offset="30"/>
    <scada:GaugeRange Color="Orange" Offset="60"/>
    <scada:GaugeRange Color="Red" Offset="100"/>
</scada:RadialScale.Ranges>

The result can be seen in the image below. In this case, the range thickness has been set to 5.

The image below presents the same settings, but this time with the TickPlacement property set to Inward.

The last range related property that remains is the UseRangeColorsForTicks property. As the name suggests, this property will enable the ticks and labels to be painted by using the color of their corresponding range.

The XAML below presents an example with two radial scales that use this property:

<scada:RadialScale Margin="5" RangeThickness="5" UseRangeColorsForTicks="True">
    <scada:RadialScale.Ranges>
        <scada:GaugeRange Offset="30" Color="{StaticResource PhoneAccentColor}"/>
        <scada:GaugeRange Offset="60" Color="Gold"/>
        <scada:GaugeRange Offset="90" Color="Red"/>
    </scada:RadialScale.Ranges>
</scada:RadialScale>
<scada:RadialScale Grid.Column="1" Margin="5" 
           RangeThickness="5" UseRangeColorsForTicks="True"
           DefaultRangeColor="Red">                
</scada:RadialScale>

The result of using this code can be seen in the image below:

As you can see from the image above, if the property is set to true and there are no ranges defined, the ticks and labels will be painted using the DefaultRangeColor property value. If there are ranges defined, the ticks and labels will use the corresponding colors. The UseRangeColorsForTicks property is meant to be used in order to add more tick customization with minimal code and effort (without resorting to templates). As such, the property only applies when the ticks use the default templates.

The same effect can be obtained by using templates, but I will talk about this in the advanced customization section.

Min angles, Max angles, and SweepDirection

Another set of properties that will help you customize the radial controls are the minimum and maximum angles and the sweep direction. These properties do exactly what their names suggest: they define the arc that will be used to draw the ticks and ranges. Some examples can be seen in the image below:

Radial type

The last property that can be used to customize the radial gauges is the RadialType property. Like I said earlier in the article, this property can be used to minimize the space used by radial gauges when the angle span is within certain intervals. For example, if the angle span is below 90 degrees, you could use a RadialType of Quarter to only occupy a quadrant of the entire circle. The same happens with semi circles. This property will check the min angle, max angle, and sweep direction properties to calculate the best layout of the control. Some examples that illustrate the use of this property can be seen below:

Advanced scale customizations

So far you have seen a lot of examples that presented all the properties that can be set on the two types of scales. This section will talk about some more advanced customization options. These refer to a more advanced use of the tick and label templates.

This section is going to show the use of some value converters in conjunction with the template properties in order to obtain some radical effects. These effects include:

  • Changing the color of the ticks based on their value.
  • Changing the shape of the ticks and labels based on their value.
  • Adding gradients to the ticks and labels based on their value.

In order to customize the scales, I have created four custom value converters. The first value converter is used to convert the tick value to a color based on a particular range. The converter is called RangeColorConverter and can be seen in the code below:

public class ColorRange
{
    public double Minimum { get; set; }
    public double Maximum { get; set; }
    public Color Color { get; set; }
}
[ContentProperty("Ranges")]
public class RangeColorConverter:IValueConverter
{
    public ObservableCollection<ColorRange> Ranges { get; set; }
    public Color DefaultColor { get; set; }
    public RangeColorConverter()
    {
        Ranges = new ObservableCollection<ColorRange>();
        DefaultColor = Colors.White;
    }
    public object Convert(object value, Type targetType, object parameter, 
                  System.Globalization.CultureInfo culture)
    {
        double val = (double)value;
        for (int i = 0; i < Ranges.Count; i++)
        {
            if (Ranges[i].Minimum <val && val <= Ranges[i].Maximum)
                return new SolidColorBrush(Ranges[i].Color);
        }
        return new SolidColorBrush(DefaultColor);
    }

    public object ConvertBack(object value, Type targetType, 
           object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

As you can see, the Convert method finds the range to which the value belongs and returns a SolidColorBrush based on the value. The code below presents a scale that uses this converter:

<loc:RangeColorConverter x:Key="colConv">
    <loc:ColorRange Color="Red" Minimum="20" Maximum="40"/>
    <loc:ColorRange Color="Blue" Minimum="60" Maximum="80"/>
</loc:RangeColorConverter>
//...
<scada:RadialScale Grid.Column="1" MajorTickStep="5">
    <scada:RadialScale.MajorTickTemplate>
        <DataTemplate>
            <Rectangle Width="2" Height="10" 
               Fill="{Binding Converter={StaticResource colConv}}"/>
        </DataTemplate>
    </scada:RadialScale.MajorTickTemplate>
</scada:RadialScale>

The result of running this code can be seen below:

The second converter (GradientConverter) is similar to the first one, but it is used to calculate a gradient. The implementation can be seen below:

public class GradientConverter:IValueConverter
{
    public Color StartColor { get; set; }
    public Color EndColor { get; set; }
    public Color DefaultColor { get; set; }
    public double StartValue { get; set; }
    public double EndValue { get; set; }
    public GradientConverter()
    {
        DefaultColor = Colors.White;
        StartColor = Colors.White;
        EndColor = Colors.White;
    }
    public object Convert(object value, Type targetType, 
           object parameter, System.Globalization.CultureInfo culture)
    {
        double val=(double)value;
        int n = (int)(EndValue - StartValue);
        int curr = (int)(val-StartValue);
        Color newColor = DefaultColor;
        if(val>=StartValue && val<=EndValue)
        {
            double u=(double)curr/n;
            newColor.R = (byte)(StartColor.R * (1 - u) + EndColor.R * u);
            newColor.G = (byte)(StartColor.G * (1 - u) + EndColor.G * u);
            newColor.B = (byte)(StartColor.B * (1 - u) + EndColor.B * u);
        }
        return new SolidColorBrush(newColor);
    }

    public object ConvertBack(object value, Type targetType, 
           object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

The code below presents the use of such a converter:

<loc:GradientConverter x:Key="conv1" EndColor="Blue" 
            StartColor="Red" StartValue="64" EndValue="90" />
<scada:RadialScale Grid.Row="1" MajorTickStep="5">
    <scada:RadialScale.MajorTickTemplate>
        <DataTemplate>
            <Rectangle Width="2" Height="10" 
                Fill="{Binding Converter={StaticResource conv1}}"/>
        </DataTemplate>
    </scada:RadialScale.MajorTickTemplate>
</scada:RadialScale>

The image below presents the results:

The third converter (SizeConverter) is used to return a size based on the tick value and on a range. The definition can be seen below:

public class SizeConverter:IValueConverter
{
    public double StartSize { get; set; }
    public double EndSize { get; set; }
    public double DefaultSize { get; set; }
    public double StartValue { get; set; }
    public double EndValue { get; set; }
    public SizeConverter()
    {
        StartSize = EndSize = DefaultSize = 10;
    }
    public object Convert(object value, Type targetType, 
           object parameter, System.Globalization.CultureInfo culture)
    {
        double val = (double)value;
        double size = DefaultSize;
        if (val >= StartValue && val <= EndValue && EndValue != StartValue)
        {
            size = StartSize + (EndSize - StartSize) * val / (EndValue - StartValue);
        }
        return size;
    }

    public object ConvertBack(object value, Type targetType, 
           object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

The code below presents how this converter is used:

<loc:SizeConverter x:Key="conv2" EndValue="50" StartSize="3" EndSize="15" />
<scada:RadialScale Grid.Row="1" Grid.Column="1" MajorTickStep="5">
    <scada:RadialScale.MajorTickTemplate>
        <DataTemplate>
            <Rectangle Width="2" 
              Height="{Binding Converter={StaticResource conv2}}" 
              Fill="White"/>
        </DataTemplate>
    </scada:RadialScale.MajorTickTemplate>
</scada:RadialScale>

The image below presents the results:

The last converter (RangeTemplateConverter) is the most versatile. It is used to change the tick and label templates based on the value. It resembles the template selectors in WPF. The definition of this converter can be seen below:

public class TemplateRange
{
    public double Minimum { get; set; }
    public double Maximum { get; set; }
    public DataTemplate Template { get; set; }
}
[ContentProperty("Ranges")]
public class RangeTemplateConverter:IValueConverter
{
    public ObservableCollection<TemplateRange> Ranges { get; set; }
    public DataTemplate DefaultTemplate { get; set; }
    public RangeTemplateConverter()
    {
        Ranges = new ObservableCollection<TemplateRange>();
    }
    public object Convert(object value, Type targetType, object parameter, 
                          System.Globalization.CultureInfo culture)
    {
        double val = (double)value;
        for (int i = 0; i < Ranges.Count; i++)
        {
            if (Ranges[i].Minimum < val && val <= Ranges[i].Maximum)
                return Ranges[i].Template;
        }
        return DefaultTemplate;
    }

    public object ConvertBack(object value, Type targetType, 
           object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

The XAML in the listing below presents how to use this converter.

<loc:GradientConverter x:Key="conv1" EndColor="Blue" 
                StartColor="Red" StartValue="64" EndValue="90" />
<loc:SizeConverter x:Key="conv2" EndValue="50" StartSize="3" EndSize="15" />
<DataTemplate x:Key="defTemplate">
    <Rectangle Width="2" Height="{Binding Converter={StaticResource conv2}}" Fill="White"/>
</DataTemplate>
<loc:RangeTemplateConverter x:Key="conv3" DefaultTemplate="{StaticResource defTemplate}" >
    <loc:TemplateRange Minimum="60" Maximum="90" >
        <loc:TemplateRange.Template>
            <DataTemplate>
                <Path Data="M0,0 L10,0 L5,10 Z" 
                  Fill="{Binding Converter={StaticResource conv1}}"/>
            </DataTemplate>
        </loc:TemplateRange.Template>
    </loc:TemplateRange>
    
</loc:RangeTemplateConverter>
//...
<scada:RadialScale MinorTickStep="5" MajorTickStep="5">
    <scada:RadialScale.MajorTickTemplate>
        <DataTemplate>
            <ContentPresenter Content="{Binding}" 
                   ContentTemplate="{Binding Converter={StaticResource conv3}}"/>
        </DataTemplate>
    </scada:RadialScale.MajorTickTemplate>
</scada:RadialScale>

The results can be seen in the image below:

This example presented a way to combine the various value converters to achieve the desired result. There are, of course, a lot more ways to customize these scales. These examples presented only a small portion of what can be done.

Adding indicators

As you saw at the beginning of the article, I have implemented four concrete indicator classes. The first set of indicators that I am going to present are the bar indicators. These indicators can be added to both radial and linear scales. The XAML below presents bar indicators for both scale types:

<scada:RadialBarIndicator Value="{Binding ElementName=slider, Path=Value}" 
       BarThickness="20" BarBrush="{StaticResource PhoneAccentBrush}" />

<scada:LinearBarIndicator Value="{Binding ElementName=slider, Path=Value}"
       BarThickness="10" BarBrush="{StaticResource PhoneAccentBrush}"/>

The effect of adding the indicators presented in the XAML above can be seen in the image below:

The next indicator I am going to present is the needle indicator. This is used the same as the others. The image below presents a needle indicator:

This indicator was created by using the following XAML:

<scada:NeedleIndicator Value="20" Background="GreenYellow"/>

The last implemented indicator is the marker indicator. With a marker indicator, you can show any type of shape at a certain value in a scale by means of a data template. The MarkerIndicator can be used with both linear and radial scales. The XAML below presents the definition of two such indicators. The first has a default template (the white rectangle), and the second has a custom template (the red circle).

<scada:MarkerIndicator Value="40" />
<scada:MarkerIndicator Value="20" >
    <scada:MarkerIndicator.MarkerTemplate>
        <DataTemplate>
            <Ellipse Width="15" Height="15" Fill="Red"/>
        </DataTemplate>
    </scada:MarkerIndicator.MarkerTemplate>
</scada:MarkerIndicator>

The results of using this code can be seen in the image below:

As you have seen in the above image, you can use multiple indicators in a single scale. You can also overlap scales to obtain some really cool effects. The image below presents another example:

Advanced indicator customization

The indicators presented so far make a big impact, but you can further customize them by using custom value converters in conjunction with the indicator Template properties. The following example will show you how the custom value converters that were applied to scales can also be applied to further customize the indicators.

The image above presents a single indicator in different phases. The indicator has been customized to change its color based on its value. In this case, this has been achieved by using the gradient converter. The code below shows the control:

<loc:GradientConverter x:Key="conv1" EndColor="Blue" 
         StartColor="Red" StartValue="64" EndValue="90" />
<scada:RadialScale MajorTickStep="25" >
    <scada:NeedleIndicator Value="90" 
        Background="{Binding RelativeSource={RelativeSource Mode=Self},
        Path=Value,Converter={StaticResource conv1}}"/>
</scada:RadialScale>

Final thoughts

This is all I wanted to say in this article. Please read the other articles in this series as they go deeper into the problem and present the implementations of the scales and indicators. You can find the links at the top.

If you liked the article and if the code was useful, please take a minute to comment and vote.

History

  • Created on March 07, 2011.
  • Updated source code on March 23, 2011.
  • Updated article and source code on March 28, 2011.
  • Updated article and source code on April 05, 2011.
  • Updated article and source code on April 10, 2011.
  • Updated article and source code on Febraury 24, 2013.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here