Click here to Skip to main content
15,868,119 members
Articles / Desktop Programming / WPF

Digital Meter Control

Rate me:
Please Sign up or sign in to vote.
4.62/5 (11 votes)
6 Feb 2009CPOL5 min read 45K   1.8K   49   5
Digital meter control is a custom control in WPF which can be used as a real time monitor for showing a decimal value in a formatted way with a nice animation.

DigitalMeter.png

Introduction

The digital meter control is a custom control in WPF which can be used as a real time monitor for showing a decimal value in a formatted way with a nice animation. What I mean by formatted way is that you can set the precision, scaling factor, and the measurement unit of your decimal value, and this control takes care of how to show it. This is a look-less control which means the logic of this control is separated from its design.

In this article, I am not going to show you how to create a custom control in WPF, so if you don’t know how to create a custom control, please visit Creating a look-less custom control in WPF.

You might find this control useful in scenarios your application reads data from other devices like reading the environment temperature etc.

Using the Code

Using this control is simple and straightforward. First, you should add a reference to Asaasoft.DigitalMeter.dll in your project. Then, define an xmlns directive for it, like shown below:

HowToUseIt.png

After that, you can create an instance of the digital meter control like this:

XML
<lib:digitalmeter precision="5" scalingfactor="2" 
   measurementunit="m" foreground="Black" removed="Gold"/>

This control has a ValueChanged routed event which can be used to informed you that a value has changed. In addition, you can set the Foreground and Background properties to create your desired look for it. In the code below, you can see how easy it is to create different looks for the digital meter:

XML
<Window x:Class="Asaasoft.DigitalMeter.Demo.DemoWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:lib="clr-namespace:Asaasoft.DigitalMeter;assembly=Asaasoft.DigitalMeter"
    Title="DemoWindow" Width="839" Height="509" >
    ...
    <StackPanel Orientation="Vertical">
        <Grid HorizontalAlignment="Center">
            <Grid.RowDefinitions>
                <RowDefinition/>
                <RowDefinition/>
                <RowDefinition/>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition/>
                <ColumnDefinition/>
            </Grid.ColumnDefinitions>

            <lib:DigitalMeter x:Name="digitalMeter1" Grid.Row="0" 
               Grid.Column="0" Margin="10"/>
            <lib:DigitalMeter x:Name="digitalMeter2" Grid.Row="0" 
               Grid.Column="1" ScalingFactor="2" MeasurementUnit="m" 
               Foreground="Black" Background="Gold" 
               Width="280" Margin="10"/>
            <lib:DigitalMeter x:Name="digitalMeter3" Grid.Row="1" 
               Grid.Column="0" MeasurementUnit="bps" 
               Foreground="DarkGray" Background="Gray" Margin="10"/>
            <lib:DigitalMeter x:Name="digitalMeter4" Grid.Row="1" 
               Grid.Column="1" Precision="7" ScalingFactor="1" 
               MeasurementUnit="ml" Foreground="Black" 
               Background="Lime" Margin="10" />
            <lib:DigitalMeter x:Name="digitalMeter5" Grid.Row="2" 
               Grid.Column="0" ScalingFactor="4" MeasurementUnit="N" 
               Foreground="CornflowerBlue" Background="Navy" Margin="10" />
            <lib:DigitalMeter x:Name="digitalMeter6" Grid.Row="2" 
               Grid.Column="1" Precision="7"  MeasurementUnit="Pa" 
               Foreground="White" Background="OrangeRed" Margin="10" />
        </Grid>
        ...
    </StackPanel>
</Window>

Here is the result:

DigitalMeters.png

How it Works (Logic)

DigitalMeterClassDiagram.png

As you can see, the DigitalMeter has these properties:

  • Value: is the current value of the DigitalMeter.
  • Precision: is the length of the integral value + the fractional value.
  • ScalingFactor: is the length of the fractional value.
  • MeasurementUnit: is a measurement unit of Value which is shown in the right side of the DigitalMeter.
  • ValueText: is the result of formatting the Value based on the Precision and ScalingFactor. For example, if you set Value=80.2, Precision=5, and ScalingFactor=2, then ValueText is equal to 080.20.

Just remember that ValueText should not be set in your code (it has a public accessor because it must be accessible in the template).

C#
public class DigitalMeter : Control
{
    ...
    
    #region Dependency Properties

    public int Precision
    {
        get
        {
            return (int)GetValue(PrecisionProperty);
        }
        set
        {
            SetValue(PrecisionProperty, value);
        }
    }

    public static readonly DependencyProperty PrecisionProperty =
        DependencyProperty.Register("Precision", typeof(int), 
        typeof(DigitalMeter), 
        new PropertyMetadata( 5, new PropertyChangedCallback(SetValueText)));

    public int ScalingFactor
    {
        get
        {
            return (int)GetValue(ScalingFactorProperty);
        }
        set
        {
            SetValue(ScalingFactorProperty, value);
        }
    }

    public static readonly DependencyProperty ScalingFactorProperty =
        DependencyProperty.Register("ScalingFactor", typeof(int), 
        typeof(DigitalMeter), 
        new PropertyMetadata(0, new PropertyChangedCallback(SetValueText)) );

    public decimal Value
    {
        get
        {
            return (decimal)GetValue(ValueProperty);
        }
        set
        {
            decimal oldValue = Value;
            SetValue(ValueProperty, value);

            if ( oldValue != value )
                OnValueChanged( oldValue, value );
        }
    }

    public static readonly DependencyProperty ValueProperty =
        DependencyProperty.Register("Value", typeof(decimal), 
        typeof(DigitalMeter), 
        new PropertyMetadata( 0M, new PropertyChangedCallback( SetValueText ) ) );

    ...

    #endregion
    
    ...
}

In order to set the proper value for ValueText, we need to be informed when the value of one of ValueProperty, PrecisionProperty, or ScalingFactorProperty is changed. We can do this via the PropertyChangedCallback delegate which is one of the parameters in the PropertyMetadata constructor. As it is shown above, the SetValueText method is called when one of the values of ValueProperty,PrecisionProperty or ScalingFactorProperty is changed.

Here is the SetValueText method:

C#
private static void SetValueText( DependencyObject d, 
               DependencyPropertyChangedEventArgs e )
{
    DigitalMeter dm = (DigitalMeter)d;
    dm.ValueText = HelperClass.FormatDecimalValue(dm.Value, 
                               dm.Precision, dm.ScalingFactor );
}

FormatDecimalValue in HelperClass is a static method which is responsible for creating the proper ValueText. If the value is too big for showing, it will create a ValueText using #. For example, for Value=20080.2, Precision=5, and ScalingFactor=2, the result is ###.##. You can see this method below:

C#
internal static string FormatDecimalValue( decimal value, int precision, int scalingFactor )
{
    string valueText = "";

    if ( scalingFactor == 0 )
    {
        valueText = Math.Round(value, 0).ToString().PadLeft(precision, '0');
    }
    else
    {
        decimal integralValue = Decimal.Truncate(value);

        decimal fractionalValue = Math.Round(value - integralValue, scalingFactor);
        string fractionalValueText = fractionalValue.ToString();
        if ( fractionalValueText.IndexOf( '.' ) > 0 )
            fractionalValueText = fractionalValueText.Remove(0, 2);
        valueText = integralValue.ToString().PadLeft(precision - scalingFactor, '0');
        valueText = string.Format("{0}.{1}", valueText, 
                    fractionalValueText.PadRight(scalingFactor, '0'));
    }

    if ( ( scalingFactor == 0 && valueText.Length > precision ) || 
         ( scalingFactor > 0 && valueText.Length > precision + 1 ) )
        valueText = string.Empty.PadLeft(precision - scalingFactor, '#') + "." + 
                    string.Empty.PadLeft(scalingFactor, '#');

    return valueText;

}

Design

The default template of any WPF control is located in Generic.xaml in the Themes folder. So, you can find the default template for this control in Generic.xaml in the Themes folder. Also, we must assign the default template of this control in the DigitalMeter constructor like below:

C#
#region Constructor
public DigitalMeter()
{
    DefaultStyleKey = typeof(DigitalMeter);
}
#endregion

Before I start describing the actual design of this control, let me show you the simple design of this control.

SolutionExplorer.png

As it is illustrated above, there are two files in the Themes folder. In order to use a simple template, you should rename SimpleGeneric.xaml to Generic.xaml. Here is the content of SimpleGeneric.xaml:

XML
<ResourceDictionary
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:core="clr-namespace:System;assembly=mscorlib"
    xmlns:local="clr-namespace:Asaasoft.DigitalMeter">
    <Style TargetType="local:DigitalMeter">
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="local:DigitalMeter">
                    <StackPanel>
                        <Border BorderBrush="Black" CornerRadius="5" 
                                   Padding="10"  BorderThickness="1">
                            <TextBlock Text="{TemplateBinding ValueText}" 
                              HorizontalAlignment="Center" VerticalAlignment="Center" />
                        </Border>
                    </StackPanel>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>
</ResourceDictionary>

As it is shown above, the Text property of TextBlock is bound to ValueText via this XAML syntax:

XML
Text="{TemplateBinding ValueText}"

Here is the result of this template:

SimpleDesignResult.png

Now, I am going to describe the actual design for this control. I wanted to use an animation which starts counting up/down to the new value and becomes blurry when counting up/down (the blurry effect was my friend Khaled Atashbahar's idea, which made the animation cooler). The core of this template is shown below:

XML
<Border Background="{TemplateBinding Background}" 
        BorderBrush="Black" BorderThickness="1.5" 
        CornerRadius="15" Padding="20,20,0,20">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition></ColumnDefinition>
            <ColumnDefinition Width="100"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <TextBlock Grid.Column="0" x:Name="ValueTextBlock" 
              Foreground="{TemplateBinding Foreground}" 
              Text="{Binding Mode=OneWay}" HorizontalAlignment="Center" 
              VerticalAlignment="Center" />
        <TextBlock Grid.Column="0" x:Name="BlurValueTextBlock" 
               Foreground="{TemplateBinding Foreground}" 
               Text="{Binding}" Opacity="0.0" 
               HorizontalAlignment="Center" 
               VerticalAlignment="Center"  >
            <TextBlock.BitmapEffect>
                <BlurBitmapEffect Radius="3" />
            </TextBlock.BitmapEffect>
        </TextBlock>
        <TextBlock Grid.Column="1" Text="{TemplateBinding MeasurementUnit}" 
           HorizontalAlignment="Center" VerticalAlignment="Center" />
    </Grid>
</Border>
<Border BorderBrush="Black" BorderThickness="1.5" 
           CornerRadius="15" Padding="6">
    <Border.Background>
        <LinearGradientBrush EndPoint="0.5,0.5" StartPoint="0.5,0">
            <GradientStop Color="#AAFFFFFF" Offset="0"/>
            <GradientStop Color="#00FFFFFF" Offset="1"/>
        </LinearGradientBrush>
    </Border.Background>
</Border>

There are two Borders. The first one is used for showing data, and the second one is used for creating a glassy effect.

  • First Border (Showing the Data)
  • For showing data, I used three TextBlocks.

    The first and the second TextBlocks are responsible for showing the ValueText and they are in the same column. The second one has the blurry effect and the Opacity is set to 0 to make it invisible. When the animation starts, it makes the Opacity of the second TextBlock to 1, so it becomes visible, and that is why you can see the blurry effect.

    The third TextBlock is responsible for showing the MeasurementUnit property.

  • Second Border (Creating the Glassy Effect)
  • The LinearGradientBrush is applied for the top half of DigitalMeter, and it starts from a transparent color and moves to white. So this control looks glassy.

A collapsed TextBox is used in order to fire the animation. The animation starts when the Text of TextBox is changed. Notice that TextBox.Text is bound to ValueText. In addition, changing the opacity of the TextBlocks (those which are responsible for showing the data) happens here.

XML
<TextBlock x:Name="collapsedTextBlock" Text="{Binding Mode=OneWay}"  Visibility="Collapsed"/>
<TextBox Name="collapsedTextBox" Text="{Binding Mode=OneWay}" Visibility="Collapsed">
    <TextBox.Triggers>
        <EventTrigger RoutedEvent="TextBox.TextChanged">
            <BeginStoryboard>
                <Storyboard>
                    <local:CounterAnimation
                    Storyboard.TargetName="BlurValueTextBlock"
                    Storyboard.TargetProperty="Text"
                    From="{Binding Mode=OneWay}" 
                    To ="{Binding ElementName=collapsedTextBlock, Path=Text}"
                    Duration="0:0:0.4" />
                    <DoubleAnimationUsingKeyFrames
                        Storyboard.TargetName="ValueTextBlock"
                        Storyboard.TargetProperty="(UIElement.Opacity)"
                        Duration="0:0:0.4">
                        <LinearDoubleKeyFrame Value="0.0" KeyTime="0:0:0.0" />
                        <LinearDoubleKeyFrame Value="0.0" KeyTime="0:0:0.399999" />
                        <LinearDoubleKeyFrame Value="1.0" KeyTime="0:0:0.4" />
                    </DoubleAnimationUsingKeyFrames>
                    <DoubleAnimationUsingKeyFrames
                        Storyboard.TargetName="BlurValueTextBlock"
                        Storyboard.TargetProperty="(UIElement.Opacity)"
                        Duration="0:0:0.4">
                        <LinearDoubleKeyFrame Value="1.0" KeyTime="0:0:0.0" />
                        <LinearDoubleKeyFrame Value="1.0" KeyTime="0:0:0.399999" />
                        <LinearDoubleKeyFrame Value="0.0" KeyTime="0:0:0.4" />
                    </DoubleAnimationUsingKeyFrames>
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </TextBox.Triggers>
</TextBox>

Because the ValueText that is bound to the TextBox is a string, we need a custom animation class to create the animation based on the ValueText. As you might have noticed, the responsible class for it is CounterAnimation. CounterAnimation is inherited from StringAnimationBase, and the GetCurrentValueCore method tells us the current value based on the elapsed time and the From and To properties. You can see this method below:

C#
protected override string GetCurrentValueCore( string defaultOriginValue, 
          string defaultDestinationValue, AnimationClock animationClock )
{
    if ( To.Contains("#") )
        return To;

    TimeSpan? current = animationClock.CurrentTime;
    int precision = To.Length;
    int scalingFactor = 0;
    if ( To.IndexOf('.') > 0 )
    {
        precision--;
        scalingFactor = precision - To.IndexOf('.');
    }

    decimal from = 0;
    if ( !string.IsNullOrEmpty(From) )
    {
        if ( !From.ToString().Contains("#") )
            from = Convert.ToDecimal(From);
        else
        {
            string max = "".PadLeft(precision, '9');
            if ( scalingFactor > 0 )
                max = max.Insert(precision - scalingFactor, ".");
            from = Convert.ToDecimal(max);
        }
    }

    decimal to = Convert.ToDecimal(To);
    decimal increase = 0;
    if ( Duration.HasTimeSpan && current.Value.Ticks > 0 )
    {
        decimal factor = (decimal)current.Value.Ticks / 
                         (decimal)Duration.TimeSpan.Ticks;
        increase = ( to - from ) * factor;
    }

    from += increase;

    return HelperClass.FormatDecimalValue(from, precision, scalingFactor);
}

Points of Interest

You might have noticed that for the CounterAnimation in the animation section of the current template, I created a collapsed TextBlock whose Text is boud to the ValueText and the To property of the CounterAnimation is set to To="{Binding ElementName=collapsedTextBlock, Path=Text}" instead of To ="{TemplateBinding ValueText}". But, the second way doesn't work. I know this template is not as good as it should be, so I would really like to know if you have a better idea for it ;).

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Iran (Islamic Republic of) Iran (Islamic Republic of)
"We are all sleeping when we die we will be awake", Imam Ali(Peace be upon him)

Comments and Discussions

 
Suggestionmohandes Pin
abb_saleh27-Feb-13 22:28
abb_saleh27-Feb-13 22:28 
GeneralMy vote of 5 Pin
Reza Ahmadi5-Mar-12 10:17
Reza Ahmadi5-Mar-12 10:17 
Question3q i am just learning customcontrol ,good article! Pin
batsword22-Sep-11 22:21
batsword22-Sep-11 22:21 
GeneralNice Article Pin
Jonathan [Darka]6-Feb-09 12:23
professionalJonathan [Darka]6-Feb-09 12:23 
GeneralRe: Nice Article Pin
Matin Habibi5-Mar-09 8:07
Matin Habibi5-Mar-09 8:07 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.