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

风速计 - 自定义 WPF 控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (37投票s)

2013年3月17日

CPOL

2分钟阅读

viewsIcon

43313

downloadIcon

1472

本文档描述了如何创建一个自定义 WPF 控件,用于指示风速和风向。

引言

在一个深夜,我正在准备一门关于如何创建自定义 WPF 控件的课程时,同时也在观看天气预报,我发现他们使用了一种比较特殊的指示器来显示风速和风向,并认为这将是一个极好的课程示例。

背景

我的目标是演示一个包含随时间动态变化的动画的示例,例如绑定动画的属性。即使这看起来很简单,但在互联网上只有很少的正确示例来演示这种行为,因为大多数动画都是“固定”的,例如具有静态值的简单过渡。

想法和图形

这个控件的主要想法是提供一个易于阅读的视觉指示,显示风向和风速,并且该指示可以用于白天和夜晚。

控件的不同布局和行为如下所示

WindMeter layouts

风速计有几个动画。

The animation of WindMeter

  • 风扇将旋转以指示风速。
  • 指针将在“风中”摆动,以指示精度。
  • 指针将旋转以指示风向。
  • 布局之间的切换将淡入淡出。

Using the Code

要使用风速计,只需像在演示应用程序中一样引用该控件,并将该控件添加到您的标记中,如下所示

<Window x:Class="DemoApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:demoApp="clr-namespace:DemoApp"
xmlns:controls="clr-namespace:NMT.Wpf.Controls;assembly=NmtUiLib"
Title="MainWindow" Height="350" Width="525">
	<Grid>
		<controls:WindMeter Margin="124,119,205,36" Wind="10" Direction="45"/>
	</Grid>
</Window>

风速计具有以下属性

WindMeter class

  • 方向
    (int)(degrees) - 旋转是顺时针方向,默认方向为 0,指向西北。
  • DirectionOffset(int)(degrees) - 偏移方向,默认值为 0。
  • Display(enum) - 风扇、白天、夜晚,默认值为 DisplayType.Fan
  • Shadow (Color) - 阴影的颜色,可用于在夜间模式下使指针在深色背景上可见,默认值为黑色。
  • Wiggle(bool) - 让风速计在风中摆动,默认值为 true
  • WiggleDegrees(int)(degrees) - 摆动角度,默认值为 10。
  • Wind(int)(m/s) - 风速,单位为米/秒,默认值为 0。

模板

这是风速计的模板,设计元素已省略。

 <!-- WindMeterStyle -->
<Style x:Key="WindMeterStyle" TargetType="{x:Type local:WindMeter}">
  <Setter Property="Background" Value="Transparent" />
  <Setter Property="ClipToBounds" Value="False" />
  <Setter Property="Template">
  <Setter.Value>
    <ControlTemplate TargetType="{x:Type local:WindMeter}">
    <Grid>
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="10*" />
        <ColumnDefinition Width="5*" />
        <ColumnDefinition Width="70*" />
        <ColumnDefinition Width="5*" />
        <ColumnDefinition Width="10*" />
      </Grid.ColumnDefinitions>
      <Grid.RowDefinitions>
        <RowDefinition Height="10*" />
        <RowDefinition Height="5*" />
        <RowDefinition Height="70*" />
        <RowDefinition Height="5*" />
        <RowDefinition Height="10*" />
      </Grid.RowDefinitions>
      <!-- PART_pointer_border -->
      <Border x:Name="PART_pointer_border" Grid.Column="0" Grid.Row="0" 
          Grid.ColumnSpan="5" Grid.RowSpan="5" RenderTransformOrigin="0.5,0.5">
        <Border.Effect>
          <DropShadowEffect Opacity=".3" Direction="320" ShadowDepth="3" 
              Color="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Shadow}" />
        </Border.Effect>
        <!-- PART_pointer -->
        <Rectangle x:Name="PART_pointer" Grid.Column="0" Grid.Row="0" Grid.ColumnSpan="5" Grid.RowSpan="5" 
            Fill="{StaticResource Pointer}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" 
            RenderTransformOrigin="0.5,0.5">
          <Rectangle.Triggers>
            <EventTrigger RoutedEvent="Rectangle.Loaded">
              <BeginStoryboard>
                <Storyboard x:Name="PART_pointer_storyboard">
                  <DoubleAnimation x:Name="PART_pointer_animation" 
                    Storyboard.TargetName="PART_pointer"
                    Storyboard.TargetProperty="(Rectangle.RenderTransform).(RotateTransform.Angle)" 
                    To="0" From="0" Duration="0:0:.5">
                    <DoubleAnimation.EasingFunction>
                      <CubicEase EasingMode="EaseInOut"></CubicEase>
                    </DoubleAnimation.EasingFunction>
                  </DoubleAnimation>
                </Storyboard>
              </BeginStoryboard>
            </EventTrigger>
          </Rectangle.Triggers>
          <Rectangle.RenderTransform>
            <RotateTransform Angle="-135" />
          </Rectangle.RenderTransform>
        </Rectangle>
        <Border.Triggers>
          <EventTrigger RoutedEvent="Border.Loaded">
            <BeginStoryboard>
              <Storyboard x:Name="PART_wiggle_storyboard">
                <DoubleAnimation x:Name="PART_wiggle_animation" 
                    Storyboard.TargetName="PART_pointer_border"
                    Storyboard.TargetProperty="(Border.RenderTransform).(RotateTransform.Angle)" 
                    To="0" From="0" Duration="0:0:0" RepeatBehavior="Forever" AutoReverse="True">
                  <DoubleAnimation.EasingFunction>
                    <ElasticEase Oscillations="2" Springiness=".5" />
                  </DoubleAnimation.EasingFunction>
                </DoubleAnimation>
              </Storyboard>
            </BeginStoryboard>
          </EventTrigger>
        </Border.Triggers>
        <Border.RenderTransform>
          <RotateTransform />
        </Border.RenderTransform>
      </Border>
      <Border Grid.Column="1" Grid.Row="1" Grid.ColumnSpan="3" Grid.RowSpan="3">
        <Border.Effect>
          <DropShadowEffect Opacity=".3" Direction="320" ShadowDepth="3" Color="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Shadow}" />
        </Border.Effect>
        <!-- PART_fan -->
        <Rectangle x:Name="PART_fan" Fill="{StaticResource Fan}" 
            HorizontalAlignment="Stretch" VerticalAlignment="Stretch" RenderTransformOrigin="0.5,0.5">
        <Rectangle.Triggers>
          <EventTrigger RoutedEvent="Rectangle.Loaded">
            <BeginStoryboard>
              <Storyboard x:Name="PART_fan_storyboard">
                <DoubleAnimation x:Name="PART_fan_animation" 
                    Storyboard.TargetName="PART_fan"
                    Storyboard.TargetProperty="(Rectangle.RenderTransform).(RotateTransform.Angle)" 
                    To="0" From="360" Duration="0:0:0" RepeatBehavior="Forever">
                </DoubleAnimation>
              </Storyboard>
            </BeginStoryboard>
          </EventTrigger>
        </Rectangle.Triggers>
        <Rectangle.RenderTransform>
          <RotateTransform />
        </Rectangle.RenderTransform>
      </Rectangle>
    </Border>
    <Viewbox Stretch="Fill" Grid.Column="2" Grid.Row="2">
      <Label x:Name="PART_numeric" FontSize="48" Content="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Wind}" Opacity="0"/>
    </Viewbox>
    </Grid>
    <ControlTemplate.Triggers>
      <Trigger Property="Display" Value="Day">
        <Trigger.EnterActions>
          <BeginStoryboard>
            <Storyboard>
              <DoubleAnimation To="0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="PART_fan" Duration="0:0:1"/>
              <DoubleAnimation To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="PART_numeric" Duration="0:0:1"/>
            </Storyboard>
          </BeginStoryboard>
        </Trigger.EnterActions>
      <Trigger.ExitActions>
        <BeginStoryboard>
          <Storyboard>
            <DoubleAnimation To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="PART_fan" Duration="0:0:1"/>
            <DoubleAnimation To="0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="PART_numeric" Duration="0:0:1"/>
          </Storyboard>
        </BeginStoryboard>
      </Trigger.ExitActions>
      <Setter Property="Fill" TargetName="PART_pointer" Value="{StaticResource PointerDay}" />
      <Setter Property="Foreground" TargetName="PART_numeric" Value="White"></Setter>
    </Trigger>
    <Trigger Property="Display" Value="Night">
      <Trigger.EnterActions>
        <BeginStoryboard>
          <Storyboard>
            <DoubleAnimation To="0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="PART_fan" Duration="0:0:1"/>
            <DoubleAnimation To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="PART_numeric" Duration="0:0:1"/>
          </Storyboard>
        </BeginStoryboard>
      </Trigger.EnterActions>
      <Trigger.ExitActions>
        <BeginStoryboard>
          <Storyboard>
            <DoubleAnimation To="1" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="PART_fan" Duration="0:0:1"/>
            <DoubleAnimation To="0" Storyboard.TargetProperty="Opacity" Storyboard.TargetName="PART_numeric" Duration="0:0:1"/>
          </Storyboard>
        </BeginStoryboard>
      </Trigger.ExitActions>
        <Setter Property="Fill" TargetName="PART_pointer" Value="{StaticResource PointerNight}" />
        <Setter Property="Foreground" TargetName="PART_numeric" Value="Yellow"></Setter>
       </Trigger>
      </ControlTemplate.Triggers>
     </ControlTemplate>
   </Setter.Value>
 </Setter>
</Style>

风速计类

这是相应的风速计类的代码。

 /// <summary>
/// WindMeter Custom Control
/// </summary>
[TemplatePart(Name = "PART_pointer", Type = typeof(Rectangle))]
[TemplatePart(Name = "PART_fan", Type = typeof(Rectangle))]
[TemplatePart(Name = "PART_pointer_border", Type = typeof(Border))]
[TemplatePart(Name = "PART_fan_storyboard", Type = typeof(Storyboard))]
[TemplatePart(Name = "PART_fan_animation", Type = typeof(DoubleAnimation))]
[TemplatePart(Name = "PART_pointer_storyboard", Type = typeof(Storyboard))]
[TemplatePart(Name = "PART_pointer_animation", Type = typeof(DoubleAnimation))]
[TemplatePart(Name = "PART_wiggle_storyboard", Type = typeof(Storyboard))]
[TemplatePart(Name = "PART_wiggle_animation", Type = typeof(DoubleAnimation))]
public class WindMeter : Control
{
  #region -- Declares --

  private Rectangle partFan, partPointer;
  private Storyboard fanStoryBoard, pointerStoryBoard, wiggleStoryBoard;
  private DoubleAnimation fanAnimation, pointerAnimation, wiggleAnimation;

  /// <summary>
  /// Display type, Fan, Day, Night
  /// </summary>
  public enum DisplayType
{
  Fan,
  Day,
  Night
}

  #endregion

  #region -- Properties --

  public static readonly DependencyProperty DirectionOffsetProperty =
    DependencyProperty.Register("DirectionOffset", typeof (int), typeof (WindMeter), new PropertyMetadata(default(int),
    (o, e) => ((WindMeter)o).ChangeDirection(((WindMeter)o).Direction, ((WindMeter)o).Direction)));

  public int DirectionOffset
  {
    get { return (int) GetValue(DirectionOffsetProperty); }
    set { SetValue(DirectionOffsetProperty, value); }
  } 

  public static readonly DependencyProperty WiggleDegreesProperty =
    DependencyProperty.Register("WiggleDegrees", typeof(int), typeof(WindMeter), new PropertyMetadata(10, 
    (o, e) => ((WindMeter)o).ChangeWiggle()));

  public int WiggleDegrees
  {
    get { return (int)GetValue(WiggleDegreesProperty); }
    set { SetValue(WiggleDegreesProperty, value); }
  }

  public static readonly DependencyProperty WiggleProperty =
    DependencyProperty.Register("Wiggle", typeof(bool), typeof(WindMeter), new PropertyMetadata(true, 
    (o, e) => ((WindMeter)o).ChangeWiggle()));

  public bool Wiggle
  {
    get { return (bool)GetValue(WiggleProperty); }
    set { SetValue(WiggleProperty, value); }
  }

  public static readonly DependencyProperty ShadowProperty =
    DependencyProperty.Register("Shadow", typeof(Color), typeof(WindMeter), new PropertyMetadata(Colors.Black));

  public Color Shadow
  {
    get { return (Color)GetValue(ShadowProperty); }
    set { SetValue(ShadowProperty, value); }
  }

  public static readonly DependencyProperty DisplayProperty =
    DependencyProperty.Register("Display", typeof(DisplayType), typeof(WindMeter), new PropertyMetadata(DisplayType.Fan,
    (o, e) => ((WindMeter)o).ChangeDisplay((DisplayType)e.NewValue)));

  public DisplayType Display
  {
    get { return (DisplayType)GetValue(DisplayProperty); }
    set { SetValue(DisplayProperty, value); }
  }

  public static readonly DependencyProperty WindProperty =
    DependencyProperty.Register("Wind", typeof(int), typeof(WindMeter), new PropertyMetadata(default(int),
    (o, e) => ((WindMeter)o).ChangeWind((int)e.OldValue, (int)e.NewValue)));

  public int Wind
  {
    get { return (int)GetValue(WindProperty); }
    set { SetValue(WindProperty, value); }
  }

  public static readonly DependencyProperty DirectionProperty =
    DependencyProperty.Register("Direction", typeof(int), typeof(WindMeter), new PropertyMetadata(0,
    (o, e) => ((WindMeter)o).ChangeDirection((int)e.OldValue, (int)e.NewValue)));

  public int Direction
  {
    get { return (int)GetValue(DirectionProperty); }
    set { SetValue(DirectionProperty, value); }
  }

  #endregion

  #region -- Constructor --

  /// <summary>
  /// Initializes a new instance of the <see cref="WindMeter"/> class.
  /// </summary>
  public WindMeter()
  {
    var res = (ResourceDictionary)Application.LoadComponent(new Uri("/NmtUiLib;component/Themes/WindMeterStyle.xaml", UriKind.Relative));
    Style = res["WindMeterStyle"] as Style;
  }

  #endregion

  #region -- Public Methods --

  public void ChangeDisplay(DisplayType type)
  {
    if (fanStoryBoard == null) return;
    if (type != DisplayType.Fan)
     fanStoryBoard.Stop();
    else
      fanStoryBoard.Begin();
  }

  public void ChangeWind(int oldValue, int newValue)
  {
    if (partFan == null) return;
      fanStoryBoard.Stop();
    if (newValue > 0)
    {
      fanAnimation.Duration = new Duration(TimeSpan.FromMilliseconds((int)(20000.0 / newValue)));
      fanStoryBoard.Begin();
    }
    ChangeWiggle();
  }

  public void ChangeWiggle()
  {
    if (wiggleAnimation == null) return;
      wiggleStoryBoard.Stop();
    if (!Wiggle || Wind <= 0) return;
    wiggleAnimation.From = WiggleDegrees /2; 
    wiggleAnimation.To = 0;
    wiggleAnimation.Duration = new Duration(TimeSpan.FromMilliseconds(10000.0 / Wind)); // Depending on wind speed
    wiggleStoryBoard.Begin();
  }

  public void ChangeDirection(int oldValue, int newValue)
  {
    if (partPointer == null) return;
    pointerStoryBoard.Stop();
    pointerAnimation.To = newValue + DirectionOffset;
    pointerAnimation.From = oldValue + DirectionOffset;
    pointerStoryBoard.Begin();
  }

  public override void OnApplyTemplate()
  {
    base.OnApplyTemplate();
    // Reference template parts
    partFan = GetTemplateChild("PART_fan") as Rectangle;
    partPointer = GetTemplateChild("PART_pointer") as Rectangle;
    pointerAnimation = GetTemplateChild("PART_pointer_animation") as DoubleAnimation;
    pointerStoryBoard = GetTemplateChild("PART_pointer_storyboard") as Storyboard;
    fanAnimation = GetTemplateChild("PART_fan_animation") as DoubleAnimation;
    fanStoryBoard = GetTemplateChild("PART_fan_storyboard") as Storyboard;
    wiggleAnimation = GetTemplateChild("PART_wiggle_animation") as DoubleAnimation;
    wiggleStoryBoard = GetTemplateChild("PART_wiggle_storyboard") as Storyboard;
    //
    // Startup & initialize
    ChangeDirection(0, Direction);
    ChangeWind(0, Wind);
    ChangeWiggle();
    ChangeDisplay(Display);
  }

  #endregion
}

关注点

动画非常灵活,我特别喜欢缓动函数,但是绑定到动画需要一些额外的工作。例如,我是否应该使用属性回调方法来更改动画设置,仅仅因为我必须先停止故事板才能接受更改后的值。例如

 public static readonly DependencyProperty WindProperty =
  DependencyProperty.Register("Wind", typeof(int), typeof(WindMeter), new PropertyMetadata(default(int),
  (o, e) => ((WindMeter)o).ChangeWind((int)e.OldValue, (int)e.NewValue)));
...
public void ChangeWind(int oldValue, int newValue)
{
  if (partFan == null) return;
  fanStoryBoard.Stop();
  if (newValue > 0)
  {
    fanAnimation.Duration = new Duration(TimeSpan.FromMilliseconds((int)(20000.0 / newValue)));
    fanStoryBoard.Begin();
  }
  ChangeWiggle(); //Wiggle is depending on Wind speed.
}

但是,这需要您在控件的 C# 类 OnApplyTemplate 中引用模板部件,即使最初看起来像是一种不好的做法,但很快您会觉得这很正常。

历史

这是第一个版本。

© . All rights reserved.