65.9K
CodeProject 正在变化。 阅读更多。
Home

数字仪表控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.62/5 (11投票s)

2009 年 2 月 6 日

CPOL

5分钟阅读

viewsIcon

46318

downloadIcon

1847

数字仪表控件是一个WPF中的自定义控件,可以作为实时监视器,以一种格式化的方式,带有漂亮的动画来显示十进制值。

DigitalMeter.png

引言

数字仪表控件是一个WPF中的自定义控件,可以作为实时监视器,以一种格式化的方式,带有漂亮的动画来显示十进制值。我所说的格式化方式是指,您可以设置十进制值的精度、缩放因子和测量单位,这个控件会负责如何显示它。这是一个无外观的控件,意味着这个控件的逻辑与其设计是分离的。

在本文中,我将不展示如何创建WPF自定义控件,所以如果您不知道如何创建自定义控件,请访问《如何创建WPF无外观自定义控件》

在您的应用程序从其他设备读取数据(例如读取环境温度等)的场景中,您可能会发现这个控件很有用。

Using the Code

使用这个控件很简单直接。首先,您应该在项目中添加对Asaasoft.DigitalMeter.dll的引用。然后,为它定义一个xmlns指令,如下所示:

HowToUseIt.png

之后,您可以像这样创建一个数字仪表控件实例:

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

这个控件有一个ValueChanged路由事件,可以用来通知您值已更改。此外,您可以设置ForegroundBackground属性来创建您想要的样式。在下面的代码中,您可以看到创建不同的数字仪表样式是多么容易:

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

结果如下

DigitalMeters.png

工作原理(逻辑)

DigitalMeterClassDiagram.png

正如您所见,DigitalMeter具有以下属性:

  • Value:是DigitalMeter的当前值。
  • Precision:是整数部分的长度 + 小数部分的长度。
  • ScalingFactor:是小数部分的长度。
  • MeasurementUnit:是Value的测量单位,显示在DigitalMeter的右侧。
  • ValueText:是根据PrecisionScalingFactor格式化Value的结果。例如,如果您设置Value=80.2Precision=5,和ScalingFactor=2,那么ValueText等于080.20。

请记住,ValueText不应在您的代码中设置(它有一个公共访问器,因为它必须在模板中可访问)。

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
    
    ...
}

为了设置ValueText的正确值,我们需要在ValuePropertyPrecisionPropertyScalingFactorProperty的值更改时收到通知。我们可以通过PropertyChangedCallback委托来实现,这是PropertyMetadata构造函数中的参数之一。如上所示,当ValuePropertyPrecisionPropertyScalingFactorProperty的任一值更改时,会调用SetValueText方法。

这是SetValueText方法:

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

HelperClass中的FormatDecimalValue是一个静态方法,负责创建正确的ValueText。如果值太大无法显示,它将使用#来创建ValueText。例如,对于Value=20080.2Precision=5ScalingFactor=2,结果是###.##。您可以在下面看到这个方法:

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;

}

设计

任何WPF控件的默认模板都位于Themes文件夹的Generic.xaml文件中。所以,您可以在Themes文件夹的Generic.xaml文件中找到这个控件的默认模板。此外,我们必须在DigitalMeter构造函数中指定该控件的默认模板,如下所示:

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

在开始描述这个控件的实际设计之前,让我先展示一下这个控件的简单设计。

SolutionExplorer.png

如上所示,Themes文件夹中有两个文件。为了使用一个简单的模板,您应该将SimpleGeneric.xaml重命名为Generic.xaml。这是SimpleGeneric.xaml的内容:

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

如上所示,TextBlockText属性通过以下XAML语法绑定到ValueText

Text="{TemplateBinding ValueText}"

这是该模板的结果:

SimpleDesignResult.png

现在,我将描述这个控件的实际设计。我想要使用一个动画,该动画会开始计数到新值(向上/向下),并在计数时变得模糊(模糊效果是我朋友Khaled Atashbahar的主意,这让动画更酷)。这个模板的核心如下所示:

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

有两个Border。第一个用于显示数据,第二个用于创建玻璃效果。

  • 第一个边框(显示数据)
  • 为了显示数据,我使用了三个TextBlock

    第一个和第二个TextBlock负责显示ValueText,并且它们在同一列。第二个具有模糊效果,并且Opacity设置为0使其不可见。当动画开始时,它将第二个TextBlockOpacity设置为1,使其可见,这就是为什么您能看到模糊效果。

    第三个TextBlock负责显示MeasurementUnit属性。

  • 第二个边框(创建玻璃效果)
  • LinearGradientBrush应用于DigitalMeter的上半部分,它从透明色开始,渐变到白色。因此,这个控件看起来像玻璃一样。

使用一个折叠的TextBox来触发动画。当TextBoxText更改时,动画就会开始。请注意,TextBox.Text绑定到ValueText。此外,这里会发生TextBlock(负责显示数据的那几个)的透明度变化。

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

因为绑定到TextBoxValueTextstring类型,我们需要一个自定义动画类来根据ValueText创建动画。您可能已经注意到,负责的类是CounterAnimationCounterAnimation继承自StringAnimationBase,并且GetCurrentValueCore方法告诉我们基于经过时间和FromTo属性的当前值。您可以在下面看到这个方法:

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);
}

关注点

您可能已经注意到,对于当前模板动画部分中的CounterAnimation,我创建了一个折叠的TextBlock,其Text绑定到ValueText,并且CounterAnimationTo属性被设置为To="{Binding ElementName=collapsedTextBlock, Path=Text}"而不是To ="{TemplateBinding ValueText}"。但是,第二种方法不起作用。我知道这个模板不是尽善尽美的,所以如果你们有更好的想法,我非常想知道;)。

© . All rights reserved.