数字仪表控件






4.62/5 (11投票s)
数字仪表控件是一个WPF中的自定义控件,可以作为实时监视器,以一种格式化的方式,带有漂亮的动画来显示十进制值。
引言
数字仪表控件是一个WPF中的自定义控件,可以作为实时监视器,以一种格式化的方式,带有漂亮的动画来显示十进制值。我所说的格式化方式是指,您可以设置十进制值的精度、缩放因子和测量单位,这个控件会负责如何显示它。这是一个无外观的控件,意味着这个控件的逻辑与其设计是分离的。
在本文中,我将不展示如何创建WPF自定义控件,所以如果您不知道如何创建自定义控件,请访问《如何创建WPF无外观自定义控件》。
在您的应用程序从其他设备读取数据(例如读取环境温度等)的场景中,您可能会发现这个控件很有用。
Using the Code
使用这个控件很简单直接。首先,您应该在项目中添加对Asaasoft.DigitalMeter.dll的引用。然后,为它定义一个xmlns
指令,如下所示:
之后,您可以像这样创建一个数字仪表控件实例:
<lib:digitalmeter precision="5" scalingfactor="2"
measurementunit="m" foreground="Black" removed="Gold"/>
这个控件有一个ValueChanged
路由事件,可以用来通知您值已更改。此外,您可以设置Foreground
和Background
属性来创建您想要的样式。在下面的代码中,您可以看到创建不同的数字仪表样式是多么容易:
<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>
结果如下
工作原理(逻辑)
正如您所见,DigitalMeter
具有以下属性:
Value
:是DigitalMeter
的当前值。Precision
:是整数部分的长度 + 小数部分的长度。ScalingFactor
:是小数部分的长度。MeasurementUnit
:是Value
的测量单位,显示在DigitalMeter
的右侧。ValueText
:是根据Precision
和ScalingFactor
格式化Value
的结果。例如,如果您设置Value=80.2
,Precision=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
的正确值,我们需要在ValueProperty
、PrecisionProperty
或ScalingFactorProperty
的值更改时收到通知。我们可以通过PropertyChangedCallback
委托来实现,这是PropertyMetadata
构造函数中的参数之一。如上所示,当ValueProperty
、PrecisionProperty
或ScalingFactorProperty
的任一值更改时,会调用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.2
、Precision=5
和ScalingFactor=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
在开始描述这个控件的实际设计之前,让我先展示一下这个控件的简单设计。
如上所示,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>
如上所示,TextBlock
的Text
属性通过以下XAML语法绑定到ValueText
:
Text="{TemplateBinding ValueText}"
这是该模板的结果:
现在,我将描述这个控件的实际设计。我想要使用一个动画,该动画会开始计数到新值(向上/向下),并在计数时变得模糊(模糊效果是我朋友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使其不可见。当动画开始时,它将第二个TextBlock
的Opacity
设置为1,使其可见,这就是为什么您能看到模糊效果。
第三个TextBlock
负责显示MeasurementUnit
属性。
LinearGradientBrush
应用于DigitalMeter
的上半部分,它从透明色开始,渐变到白色。因此,这个控件看起来像玻璃一样。
使用一个折叠的TextBox
来触发动画。当TextBox
的Text
更改时,动画就会开始。请注意,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>
因为绑定到TextBox
的ValueText
是string
类型,我们需要一个自定义动画类来根据ValueText
创建动画。您可能已经注意到,负责的类是CounterAnimation
。CounterAnimation
继承自StringAnimationBase
,并且GetCurrentValueCore
方法告诉我们基于经过时间和From
和To
属性的当前值。您可以在下面看到这个方法:
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
,并且CounterAnimation
的To
属性被设置为To="{Binding ElementName=collapsedTextBlock, Path=Text}"
而不是To ="{TemplateBinding ValueText}"
。但是,第二种方法不起作用。我知道这个模板不是尽善尽美的,所以如果你们有更好的想法,我非常想知道;)。