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 III

0.00/5 (No votes)
25 Feb 2013 6  
This article is the third in the series that describes a set of gauge controls for WP7. This article focuses on the implementation of the indicators used with the gauges.

Introduction

This article is the third in a series that talks about designing and implementing a set of custom gauge controls for the Windows Phone 7 platform. The first article talked about the design considerations and about using the code, and the second article dug deeper into the implementation of the scales. In this article, I'm going to talk about how I implemented the indicators. Before reading this article, I strongly recommend that you read the first and second articles in the series. This way you can better understand what this article describes and how the indicators feel in the big picture.

The articles in this series

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

The Indicator base class

This is the base class from which all other indicators should derive. The Indicator class derives from the Control base class and it is abstract since it shouldn’t be used directly. This class exposes the properties that are common to all indicators. These properties are the Value at which the indicator points on the scale and the Owner of the indicator (the scale to which the indicator belongs).

These two properties can be seen in the image below:

The Owner property is a regular CLR property that is used to set the scale to which the indicator belongs. The definition of this property can be seen in the code below:

public Scale Owner
{
    get
    {
        return this.owner;
    }
    internal set
    {
        if (this.owner != value)
        {
            this.owner = value;
            UpdateIndicator(owner);
        }
    }
}

As you can see from the code above, every time an indicator is assigned to a new scale, the indicator is updated. This is done by calling the UpdateIndicator private function. Inside this function, the Value property of the indicator is coerced to be inside the owner scale range. The definition for this method can be seen below:

private void UpdateIndicator(Scale owner)
{
    if (owner != null)
    {
        if (Value < owner.Minimum)
            Value = owner.Minimum;
        if (Value > owner.Maximum)
            Value = owner.Maximum;
    }
    UpdateIndicatorOverride(owner);
}

As you can see, after coercing the value, the method also calls the UpdateIndicatorOverride method. This is a virtual method that can be overridden in the derived classes to add additional logic when the owner changes.

The Value property of the Indicator base class is a dependency property. This property has a change handler that can be seen below:

private static void ValuePropertyChanged(DependencyObject o, 
        DependencyPropertyChangedEventArgs e)
{
    Indicator ind = o as Indicator;
    if (ind != null)
    {
        ind.OnValueChanged((double)e.NewValue, (double)e.OldValue);
    }
}
protected virtual void OnValueChanged(double newVal, double oldVal) { }

As you can see, the change handler calls the onValueChanged virtual method. This will be overridden in the derived classes to properly update the indicators.

The last important code to talk about is the MeasureOverride method. I overloaded this method so that I can set the owner of the indicator automatically. After the indicator control is instantiated and the layout process starts, the Owner property of the indicator will be set. The definition of this method can be seen in the code below:

protected override Size MeasureOverride(Size availableSize)
{
    //the main purpose of this override is to set the owner for the 
    //indicator. The actual measuring calculation will be done in 
    //the derived classes
    DependencyObject parent = base.Parent;
    while (parent != null)
    {
        Scale scale = parent as Scale;
        if (scale != null)
        {
            this.Owner = scale;
            break;
        }
        FrameworkElement el = parent as FrameworkElement;
        if (el != null)
        {
            parent = el.Parent;
        }
    }
    return base.MeasureOverride(availableSize);
}

As you can see from the code above, the method tries to set the indicator’s owner recursively. The Owner property is set only if the parent is a Scale type.

The BarIndicator class

Another base class that is used to build some of the indicators is the BarIndicator class. This class adds the properties that are specific to bar indicators. Bar indicators are represented by a solid path. In the case of a linear scale, the indicator will be represented by a rectangle. In the case of a radial scale, the indicator will be represented by a circle segment. The specific properties are the bar thickness and the bar brush. These two properties can be seen in the image below.

As you can see from the diagram, this class is also abstract. This is because a bar indicator’s shape depends on the type of the scale in which the indicator is used. The two properties are dependency properties with change handlers attached. The change handlers are shown in the code below:

private static void BarThicknessPropertyChanged(DependencyObject o, 
                    DependencyPropertyChangedEventArgs e)
{
    BarIndicator ind = o as BarIndicator;
    if (ind != null)
    {
        ind.OnBarThicknesChanged((int)e.NewValue, (int)e.OldValue);
    }
}
private static void BarBrushPropertyChanged(DependencyObject o, 
                    DependencyPropertyChangedEventArgs e)
{
}
protected virtual void OnBarThicknesChanged(int newVal, int oldVal) { }

As you can see, when the thickness changes, the code calls the virtual OnBarThicknessChanged method. This will be used in the derived classes to properly update the indicator. Since the Brush is freezable and gets updated automatically every time the property changes, the bar brush change handler does nothing.

The LinearBarIndicator class

This class is the first concrete class that I’m going to talk about. It is the bar indicator that can be used for linear scales. The class derives from the BarIndicator base class and defines no additional properties. Since this is a concrete Control class, I also added a default template for this indicator in the generic.xaml file. The default indicator template for the LinearBarIndicator can be seen in the listing below:

<Style TargetType="loc:LinearBarIndicator" >
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="loc:LinearBarIndicator">
                <Rectangle Fill="{TemplateBinding BarBrush}"></Rectangle>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

As you can see, the template is trivial. It doesn’t even have a part name that can be used in the code-behind. I did this because I wanted the indicator to be very simple. If you want a linear indicator that is more complex, you could add a path with a part name instead of the rectangle and handle the path in the code-behind. One such example would be if you wanted you linear indicator to look like a sine function.

The code-behind for this indicator has only a few methods. The first one I’m going to talk about is the OnValueChanged override. The definition for this method can be seen in the listing below:

protected override void OnValueChanged(double newVal, double oldVal)
{
    //every time the value changes set the width and the hight of
    //the indicator.
    //setting these properties will trigger the measure and arrange passes
    LinearScale scale = Owner as LinearScale;
    if (scale != null)
    {
        Size sz = GetIndicatorSize(scale);
        Width = sz.Width;
        Height = sz.Height;
    }
}

The first thing the method does is to check if the owner is a LinearScale. If it is, it calculates the indicator’s desired size using the GetIndicatorSize helper method and sets the Width and Height properties of the control. Setting these properties will trigger the measure and arrange passes again. This is necessary so that we can redraw the indicator to the correct size.

The code for the GetindicatorSize method can be seen in the listing below:

private Size GetIndicatorSize(LinearScale owner)
{
    //gets the size of the indicator based on the current value and the
    //owner dimensions
    double width = 0, height = 0;
    if (owner.Orientation == Orientation.Horizontal)
    {
        height = BarThickness;
        width = GetExtent(owner.ActualWidth, Value, 
                          owner.Maximum, owner.Minimum);
    }
    else
    {
        width = BarThickness;
        height = GetExtent(owner.ActualHeight, Value, 
                           owner.Maximum, owner.Minimum);
    }
    return new Size(width, height);
}

//gets the length the indicator should have
private double GetExtent(double length, double value, double max, double min)
{
    return length * (value - min) / (max - min);
}

This method gets the desired size of the indicator by making use of another helper method that is also presented. By specifying the entire available length, the current value of the indicator, and the scale range, the GetExtent helper method determines the length the indicator should have. This method doesn’t specify whether this length should be the width or the height of the control. This is determined in the GetIndicatorSize method based on the Orientation property of the scale that owns the indicator.

The last two methods are the MeasureOverride and ArrangeOverride methods. The definition for the MeasureOverride method can be seen in the listing below:

protected override Size MeasureOverride(Size availableSize)
{
    //call the base version to set the owner
    base.MeasureOverride(availableSize);
    Size size = new Size();
    //get the desired size of the indicator based on the owner size
    LinearScale owner = Owner as LinearScale;
    if (owner != null)
    {
        size = GetIndicatorSize(owner);
    }
    return size;
}

As you know, this method is used to determine the desired size of the control on which it is overridden. In this implementation, I first call the base implementation so that the owner of the control can be set. Next, I check if the owner is a LinearScale, and if it is, I call the GetIndicatorSize helper method to get the desired size. At the end, I return either a size of 0 or the result from the GetIndicatorSize method.

The ArrangeOverride method will be used to position the indicator in the available size. This positioning will depend on the orientation and on the tick placement in the scale that owns the indicator. The definition can be seen below:

protected override Size ArrangeOverride(Size arrangeBounds)
{
    //with every arrange pass the size of the indicator should 
    //be set again. this is important if the orientation is
    //vertical as the start position changes every time the value changes
    //so the indicator should be rearranged
    LinearScale scale = Owner as LinearScale;
    Size sz = base.ArrangeOverride(arrangeBounds);
    if (scale != null)
    {
        //reset the indicator size after each arrange phase
        sz = GetIndicatorSize(scale);
        Width = sz.Width;
        Height = sz.Height;
        Point pos = scale.GetIndicatorOffset(this);
        TranslateTransform tt = new TranslateTransform();
        tt.X = pos.X;
        tt.Y = pos.Y;
        this.RenderTransform = tt;
    }
    return sz;
}

In this method, the indicator is arranged by using a TranslateTransform. In order to get the offset position at which to place the indicator, the method calls an internal LinearScale function. Another important thing to note here is that I reset the width and height of the indicator. This needs to be done because every time this method is called, the indicator should have a new size (even though the Value property remains unchanged). I didn’t present the GetIndicatorOffset method in the previous article because I wanted to talk about it here. This method gets the offset the indicator should be placed at depending on the scale orientation and on the tick placement. The definition of this method can be seen below:

internal Point GetIndicatorOffset(Indicator ind)
{
    //get's the offset at which the indicator is placed inside the owner
    Point pos = new Point();
    if (Orientation == Orientation.Horizontal)
    {

        if (TickPlacement == LinearTickPlacement.TopLeft)
        {
            pos.X = 0;
            pos.Y = GetLabels().Max(p => p.DesiredSize.Height) + 
              GetTicks().Max(p => p.DesiredSize.Height) + RangeThickness + 5;
        }
        else
        {
            pos.X = 0;
            pos.Y = ActualHeight - ind.DesiredSize.Height - 
              (GetLabels().Max(p => p.DesiredSize.Height) + 
               GetTicks().Max(p => p.DesiredSize.Height) + RangeThickness + 7);
        }
    }
    else
    {
        if (TickPlacement == LinearTickPlacement.TopLeft)
        {
            pos.X = GetLabels().Max(p => p.DesiredSize.Width) + 
              GetTicks().Max(p => p.DesiredSize.Width) + RangeThickness + 6;
            pos.Y = ActualHeight - ind.DesiredSize.Height;
        }
        else
        {
            pos.X = ActualWidth - ind.DesiredSize.Width - 
             (GetLabels().Max(p => p.DesiredSize.Width) + 
              GetTicks().Max(p => p.DesiredSize.Width) + RangeThickness + 6);
            pos.Y = ActualHeight - ind.DesiredSize.Height;
        }
    }
    return pos;
}

The image below presents a set of four Linear scales that use this type of indicator:

As you can see, the indicator changes its shape and position based on the scale Orientation and TickPlacement properties.

The RadialBarIndicator class

This class is the other class that derives from the BarIndicator base class. This will represent the bar indicator for radial scales. The default template for the RadialBarIndicator can be seen in the listing below:

<Style TargetType="loc:RadialBarIndicator">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="loc:RadialBarIndicator">
                <Path x:Name="PART_BAR" Fill="{TemplateBinding BarBrush}"/>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

The indicator is represented by a path that will be set in the code-behind every time the value changes. The shape of the indicator needs to be changed very often. The first place this happens is when the owner changes. To handle this, I have overridden the UpdateIndicatorsOverride method. The code can be seen in the listing below:

protected override void UpdateIndicatorOverride(Scale owner)
{
    base.UpdateIndicatorOverride(owner);
    RadialScale scale = Owner as RadialScale;
    if (scale != null)
    {
        SetIndicatorGeometry(scale, Value);
    }
}

The same needs to happen when the value changes or when the bar thickness changes. The definitions for these methods can be seen in the listing below:

protected override void OnValueChanged(double newVal, double oldVal)
{
    RadialScale scale = Owner as RadialScale;
    if (scale != null)
    {
        SetIndicatorGeometry(scale, Value);
    }
}

protected override void OnBarThicknesChanged(int newVal, int oldVal)
{
    base.OnBarThicknesChanged(newVal, oldVal);
    RadialScale scale = Owner as RadialScale;
    if (scale != null)
    {
        SetIndicatorGeometry(scale, Value);
    }
}

All these methods use a private helper that creates the necessary geometry. The definition for this helper method can be seen in the listing below:

private void SetIndicatorGeometry(RadialScale scale, double value)
{
    if (thePath != null)
    {
        double min = scale.MinAngle;
        double max = scale.GetAngleFromValue(Value);
        if (scale.SweepDirection == SweepDirection.Counterclockwise)
        {
            min = -min;
            max = -max;
        }
        double rad = scale.GetIndicatorRadius();
        if (rad > BarThickness)
        {
            Geometry geom = RadialScaleHelper.CreateArcGeometry(
                              min, max, rad, BarThickness, scale.SweepDirection);
            //stop the recursive loop. only set a new geometry
            //if it is different from the current one
            if (thePath.Data == null || thePath.Data.Bounds != geom.Bounds)
                thePath.Data = geom;
        }
    }
}

The first thing the method does is to determine the minimum and maximum angles of the indicator. This is done by calling an internal scale method called GetAngleFromValue. This method can be seen below:

internal double GetAngleFromValue(double value)
{
    //ANGLE=((maxa-mina)*VAL+mina*maxv-maxa*minv)/(maxv-minv)
    double angle = ((MaxAngle - MinAngle) * value + MinAngle * 
           Maximum - MaxAngle * Minimum) / (Maximum - Minimum);
    return angle;
}

The next thing that needs to be done is to calculate the radius of the indicator. This is done by calling the GetindicatorRadius internal scale method. The definition for this method can be seen below:

internal double GetIndicatorRadius()
{
    double maxRad = RadialScaleHelper.GetRadius(RadialType, 
           new Size(ActualWidth, ActualHeight), MinAngle, MaxAngle, SweepDirection);
    return maxRad - GetLabels().Max(p => p.DesiredSize.Height) - 
           GetTicks().Max(p => p.DesiredSize.Height) - RangeThickness - 3;
}

As you can see, the method delegates to the RadialScaleHelper in order to get the maximum allowed radius. It then subtracts from the maximum the label, tick, and range heights.

The shape of the indicator is then created by using the CreateArcGeometry method that was described in the previous article.

The last remaining methods are the MeasureOverride and the ArrangeOverride methods. These will be used to calculate the desired size of the indicator and to arrange it. The definition for the MeasureOverride method can be seen in the listing below:

protected override Size MeasureOverride(Size availableSize)
{
    //call the base version to set the parent
    base.MeasureOverride(availableSize);
    //return all the available size
    double width = 0, height = 0;
    if (!double.IsInfinity(availableSize.Width))
        width = availableSize.Width;
    if (!double.IsInfinity(availableSize.Height))
        height = availableSize.Height;
    RadialScale scale = Owner as RadialScale;
    if (scale != null)
    {
        //every time a resize happens the indicator needs to be redrawn
        SetIndicatorGeometry(scale, Value);
    }
    return new Size(width, height);
}

The method first calls the base version in order to set the owner. After this, the method calculates and sets the indicator geometry by using the helper method described above. The definition of the ArrangeOverride method can be seen below:

protected override Size ArrangeOverride(Size arrangeBounds)
{
    TranslateTransform tt = new TranslateTransform();
    RadialScale scale = Owner as RadialScale;
    if (scale != null)
    {
        //calculate the geometry again. the first time this
        //was done the owner had a size of (0,0)
        //and so did the indicator. Once the owner has
        //the correct size (measureOveride has been called)
        //i should re-calculate the shape of the indicator
        SetIndicatorGeometry(scale, Value);
        Point center = scale.GetIndicatorOffset();
        tt.X = center.X;
        tt.Y = center.Y;
        RenderTransform = tt;
    }
    return base.ArrangeOverride(arrangeBounds);
}

As you can see, the center of the scale is determined and the indicator is offset by using a TranslateTransform. The GetIndicatorOffset internal scale method can be seen below:

internal Point GetIndicatorOffset()
{
    return RadialScaleHelper.GetCenterPosition(RadialType, 
           new Size(ActualWidth, ActualHeight), 
           MinAngle, MaxAngle, SweepDirection);
}

This method delegates to the GetCenterPosition helper method that I talked about in the second article of the series.

The image below presents a set of three Radial scales that use this type of indicator:

As you can see from the image above, by combining more scales and positioning them just the right way, you can get some really nice gauges. The code used for these three gauges can be seen in the listing below:

<scada:RadialScale Grid.RowSpan="2" Grid.ColumnSpan="2"
       RangeThickness="5" MinorTickStep="10" MajorTickStep="50"
       Maximum="400" MinAngle="-90" MaxAngle="90">
    <scada:RadialScale.Ranges>
        <scada:GaugeRange Color="Red" Offset="20" />
        <scada:GaugeRange Color="Orange" Offset="40" />
        <scada:GaugeRange Color="WhiteSmoke" Offset="60" />
        <scada:GaugeRange Color="{StaticResource PhoneAccentColor}" Offset="100" />
    </scada:RadialScale.Ranges>
    <scada:RadialBarIndicator 
       Value="{Binding ElementName=slider,Path=Value}" 
       BarThickness="20" BarBrush="{StaticResource PhoneAccentBrush}" />
    
</scada:RadialScale>
<scada:RadialScale RangeThickness="5" MinorTickStep="25" MajorTickStep="50"
       MinAngle="110" MaxAngle="160" RadialType="Quadrant" 
       SweepDirection="Counterclockwise" 
       Width="130" Height="130" Margin="80,155,10,44" Grid.RowSpan="2">
    <scada:RadialScale.Ranges>
        <scada:GaugeRange Color="GreenYellow" Offset="50" />
        <scada:GaugeRange Color="Gold" Offset="75" />
        <scada:GaugeRange Color="Red" Offset="100" />
    </scada:RadialScale.Ranges>
    
    <scada:RadialBarIndicator 
       Value="{Binding ElementName=slider2,Path=Value, Mode=TwoWay}" 
       BarBrush="Gold" BarThickness="60"/>
</scada:RadialScale>
<scada:RadialScale RangeThickness="5" MinorTickStep="25" MajorTickStep="50"
       MinAngle="110" MaxAngle="160" RadialType="Quadrant" 
       SweepDirection="Clockwise" Width="130" Height="130" 
       Margin="10,157,80,42" Grid.Column="1" Grid.RowSpan="2">
    <scada:RadialScale.Ranges>
        <scada:GaugeRange Color="GreenYellow" Offset="50" />
        <scada:GaugeRange Color="Gold" Offset="75" />
        <scada:GaugeRange Color="Red" Offset="100" />
    </scada:RadialScale.Ranges>

    <scada:RadialBarIndicator 
      Value="{Binding ElementName=slider2,Path=Value, Mode=TwoWay}" 
      BarBrush="Gold" BarThickness="60"/>
</scada:RadialScale>

The MarkerIndicator class

The next concrete indicator I implemented was a marker indicator. This marker indicator can be used with both linear and radial scales. This class derives from the Indicator base class and defines an additional property. This is the MarkerTemplate property. The default template for this control can be seen in the listing below:

<Style TargetType="loc:MarkerIndicator">
    <Setter Property="MarkerTemplate">
        <Setter.Value>
            <DataTemplate>
                <Path Data="M0,0 L6,0 L6,20 L0,20 Z"
                          Fill="White" />
            </DataTemplate>
        </Setter.Value>
    </Setter>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="loc:MarkerIndicator">
                <ContentPresenter x:Name="PART_Marker"
                      ContentTemplate="{TemplateBinding MarkerTemplate}" />
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

From the above XAML, you can see that the default template is a white rectangle. This is applied by binding the MarkerTemplate property to the ContentTemplate property of the presenter.

The first method of this indicator that I'm going to talk about is the Arrange method. This method is an override of the method that was initially defined in the base Indicator class. This method is used to arrange the indicator by calling the Arrange method from the base Control class. The definition for this method can be seen in the code below:

public override void Arrange(Size finalSize)
{
    base.Arrange(DesiredSize);
    //call method to arrange the marker
    SetIndicatorTransforms();
    PositionMarker();
}

As you can see, the method calls the base implementation with the DesiredSize of the indicator. After this, the method positions and rotates the indicator. This is done in two steps:

  1. Set the RenderTransform property of the indicator to a TransformGroup, and
  2. Modify the transforms in that group.

The RenderTransform property is set in the SetIndicatorTransforms method. The definition for this method can be seen below:

private void SetIndicatorTransforms()
{
    if (RenderTransform is MatrixTransform)
    {
        TransformGroup tg = new TransformGroup();
        TranslateTransform tt = new TranslateTransform();
        RotateTransform rt = new RotateTransform();

        tg.Children.Add(rt);
        tg.Children.Add(tt);

        this.RenderTransformOrigin = new Point(0.5, 0.5);
        this.RenderTransform = tg;
    }
}

The PositionMarker method is where most of the work gets done. This method first checks to see if the indicator has an Owner. If it doesn't, it returns.

if (Owner == null)
    return;

After this, the method checks the owner type. Based on the type, the indicator will be arranged in different ways. If the owner is a radial scale, the method does the following:

  • Calculates the angle the indicator should be rotated by.
  • Sets the RotateTransform in the transform group.
  • Calculates the indicator position.
  • Moves the indicator to the required position by setting the TranslateTransform in the transform group.

All the steps above can be seen in the listing below:

RadialScale rs = (RadialScale)Owner;
//get the angle based on the value
double angle = rs.GetAngleFromValue(Value);
if (rs.SweepDirection == SweepDirection.Counterclockwise)
{
    angle = -angle;
}
//rotate the marker by angle
TransformGroup tg = RenderTransform as TransformGroup;
if (tg != null)
{
    RotateTransform rt = tg.Children[0] as RotateTransform;
    if (rt != null)
    {
        rt.Angle = angle;
    }
}
//position the marker based on the radius
Point offset = rs.GetIndicatorOffset();
double rad = rs.GetIndicatorRadius();

//position the marker
double px = offset.X + (rad - DesiredSize.Height / 2) * Math.Sin(angle * Math.PI / 180);
double py = offset.Y - (rad - DesiredSize.Height / 2) * Math.Cos(angle * Math.PI / 180);
px -= DesiredSize.Width / 2;
py -= DesiredSize.Height / 2;
if (tg != null)
{
    TranslateTransform tt = tg.Children[1] as TranslateTransform;
    if (tt != null)
    {
        tt.X = px;
        tt.Y = py;
    }
}

In order to arrange the indicator inside a linear scale, the following code is used:

LinearScale ls = Owner as LinearScale;
Point offset = ls.GetIndicatorOffset(this);
//the getIndicatorOffset returns only one correct dimension
//for marker indicators the other dimension will have to be calculated again
if (ls.Orientation == Orientation.Horizontal)
{
    offset.X = ls.ActualWidth * (Value - ls.Minimum) / 
          (ls.Maximum - ls.Minimum) - DesiredSize.Width / 2;
}
else
{
    offset.Y = ls.ActualHeight - ls.ActualHeight * (Value - ls.Minimum) / 
              (ls.Maximum - ls.Minimum) - DesiredSize.Height / 2;
}
TransformGroup tg = RenderTransform as TransformGroup;
if (tg != null)
{
    TranslateTransform tt = tg.Children[1] as TranslateTransform;
    if (tt != null)
    {
        tt.X = offset.X;
        tt.Y = offset.Y;
    }
}

The first thing this code does is to determine the offset the indicator should have. This is done by calling the GetIndicatorOffset internal method from the LinearScale class. This method was initially designed to return the offset for the LinearBarIndicator and was presented in detail earlier. This method does not return a correct offset for marker indicators. This is corrected in the next lines. This code will have to be fixed because the offset should not depend on the indicator type.

At the end, the code sets the TranslateTransform in order to move the indicator to the desired location. The image below presents some examples of using this type of indicator:

The code used to build the indicator shown above can be seen in the listing below:

<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 NeedleIndicator class

The last indicator I implemented for this library was the needle indicator for radial scales. This is another concrete indicator class. This class derives from the Indicator base class and defines no additional properties. The default template for this control can be seen in the listing below:

<Style TargetType="loc:NeedleIndicator">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="loc:NeedleIndicator">
                <Path x:Name="PART_Needle" 
                   Data="M7,0 L0,10 L5,10 L5,70 L8,70 L8,10 L13,10 Z" Fill="White" />
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

The default template is a Path that defines an arrow. This can be seen in the image below:

The first method that I’m going to talk about here is the Arrange method. This method will be used to arrange the needle indicator by using a set of three transforms:

  • A scale transform – to draw the needle all the way near the scale no matter the size of the scale
  • A rotate transform – to rotate the needle based on the Value property
  • A translate transform – to position the needle based on the radial type property

The definition for this method can be seen in the listing below:

public override void Arrange(Size finalSize)
{
    base.Arrange(DesiredSize);
    //arrange the indicator in the center
    SetIndicatorTransforms();
    RadialScale scale = Owner as RadialScale;
    if (scale != null)
    {
        Point center = scale.GetIndicatorOffset();
        TransformGroup tg = RenderTransform as TransformGroup;
        if (tg != null)
        {
            //add a scale in order to make the needle to feet exactly inside the range
            ScaleTransform st = tg.Children[0] as ScaleTransform;
            double rad = scale.GetIndicatorRadius();
            if (st != null && DesiredSize.Height != 0 && 
                !double.IsInfinity(DesiredSize.Height) && rad > 0)
            {
                //factor is the radius devided by the height
                double factor = rad / ( DesiredSize.Height);
                st.ScaleX = factor;
                st.ScaleY = factor;
            }
            TranslateTransform tt = tg.Children[2] as TranslateTransform;
            if (tt != null)
            {
                tt.X = center.X - DesiredSize.Width / 2;
                tt.Y = center.Y - DesiredSize.Height;
            }
        }
    }
}

The first thing this method does is to arrange the indicator by calling the base class version and passing the desired size of the indicator.

The next thing this method does is to set the RenderTransform property of the indicator. This is done in the SetIndicatorTransforms method. This method sets the RenderTransform property only if the property has its default value which is a matrix transform. This can be seen in the listing below:

private void SetIndicatorTransforms()
{
    if (RenderTransform is MatrixTransform)
    {
        TransformGroup tg = new TransformGroup();
        TranslateTransform tt = new TranslateTransform();
        RotateTransform rt = new RotateTransform();
        ScaleTransform st = new ScaleTransform();

        tg.Children.Add(st);
        tg.Children.Add(rt);
        tg.Children.Add(tt);

        this.RenderTransformOrigin = new Point(0.5, 1);
        this.RenderTransform = tg;
    }
}

The next step is to scale the indicator based on the current size of the owner. This is done by dividing the radius of the owner to the height of the indicator. The last step in the ArrangeOverride method is to calculate the corresponding offset and to set the translate transform.

Another important helper method is the SetIndicatorAngle method. This is used to modify the indicator’s rotate transform based on the current value of the Value property. The definition can be seen below:

private void SetIndicatorAngle(RadialScale scale, double value)
{
    double angle = scale.GetAngleFromValue(Value);
    if (scale.SweepDirection == SweepDirection.Counterclockwise)
    {
        angle = -angle;
    }
    //rotate the needle
    TransformGroup tg = RenderTransform as TransformGroup;
    if (tg != null)
    {
        RotateTransform rt = tg.Children[1] as RotateTransform;
        if (rt != null)
        {
            rt.Angle = angle;
            Debug.WriteLine("angle changed to " + angle);
        }
    }
}

This method is called in the value change handler. The definition for the OnValueChanged handler can be seen in the image below:

protected override void OnValueChanged(double newVal, double oldVal)
{
    RadialScale scale = Owner as RadialScale;
    if (scale != null)
    {
        SetIndicatorAngle(scale, Value);
    }
}

The method is also called when the indicator owner changes. The definition for UpdateindicatorsOverride can be seen below:

protected override void UpdateIndicatorOverride(Scale owner)
{
    base.UpdateIndicatorOverride(owner);
    SetIndicatorTransforms();
    RadialScale scale = owner as RadialScale;
    if(scale!=null)
    {
        SetIndicatorAngle(scale, Value);
    }
}

The image below presents two sets of radial gauges that use this type of control:

The code used to build the gauges on the left side of the image above can be seen below. For the gauges on the right, similar code was used.

<scada:RadialScale Grid.RowSpan="2" Grid.ColumnSpan="2"
       RangeThickness="5" MinorTickStep="10" MajorTickStep="50"
       MinAngle="-90" MaxAngle="180">
    <scada:RadialScale.Ranges>
        <scada:GaugeRange Color="Red" Offset="20" />
        <scada:GaugeRange Color="Orange" Offset="40" />
        <scada:GaugeRange Color="WhiteSmoke" Offset="60" />
        <scada:GaugeRange Color="{StaticResource PhoneAccentColor}" Offset="100" />
    </scada:RadialScale.Ranges>
    
    <scada:NeedleIndicator 
       Value="{Binding ElementName=slider,Path=Value}" Background="White"/>
</scada:RadialScale>
<scada:RadialScale MinAngle="100" MaxAngle="170" 
       SweepDirection="Counterclockwise" MinorTickStep="25" 
       MajorTickStep="50" RadialType="Quadrant" Width="123" 
       Height="122" Margin="80,16,16,26" Grid.Row="1"
       RangeThickness="4">
    <scada:NeedleIndicator 
       Value="{Binding ElementName=slider2,Path=Value}" Background="GreenYellow"/>
    <scada:RadialScale.Ranges>
        <scada:GaugeRange Offset="100" Color="GreenYellow"/>
    </scada:RadialScale.Ranges>
    <scada:RadialScale.LabelTemplate>
        <DataTemplate>
            <TextBlock Text="{Binding}" Foreground="GreenYellow" 
                 FontWeight="Bold" FontSize="16"/>
        </DataTemplate>
    </scada:RadialScale.LabelTemplate>
</scada:RadialScale>

You can also use multiple indicators on a single scale. This can be seen in the image below which presents two radial scale controls each with two indicators:

Final thoughts

This article talked about only a few types of indicators. There are certainly many others that can be implemented. Two such examples are a marker indicator and a needle indicator for linear scales. The needle indicator for the linear scale could show a line with an arrow and a label at the specified value. This could also be replicated with the marker indicator you just saw. The image below shows how the needle indicator for the linear scale could look like:

There are, of course, many other customization and extension possibilities.

Please feel free to post your comments and suggestions. Also, if you want to vote for this article, please vote for the first one in the series Smile | :) .

History

  • Created on March 21, 2011.
  • Updated on March 23, 2011.
  • Updated on March 28, 2011.
  • Updated source code on April 04, 2011.
  • Updated source code on April 10, 2011.
  • Updated 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