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

Windows 进度环

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (27投票s)

2013年12月21日

CPOL

1分钟阅读

viewsIcon

62296

downloadIcon

2276

Windows 进度环作为自定义控件。

引言

我读到一篇关于使用 Windows Forms 创建进度环的文章,我认为使用 WPF 会容易得多。
我想知道这需要多少努力,并给自己提出了这个挑战。
在这篇文章中,你可以阅读到结果。

进度环

进度环主要出现在 Windows 启动和安装过程中,但也用于许多应用程序和不同的变体中。

实现

这个进度环被实现为一个自定义的 WPF 控件。
基本上,该控件是一个包含 5 行和 5 列的 Grid,以及一些围绕网格中心旋转的椭圆。
旋转是通过一个带有 QuarticEase 函数的动画 RotateTransform 完成的。

控件 Style

     <Style x:Key="WindowsProgressRingStyle" TargetType="{x:Type local:WindowsProgressRing}">
        <Setter Property="Background" Value="Transparent"/>
        <Setter Property="Foreground" Value="Black"/>
        <Setter Property="ClipToBounds" Value="False"/>
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:WindowsProgressRing}">
                    <Grid x:Name="PART_body" Background="{TemplateBinding Background}">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="1*"/>
                            <RowDefinition Height="1*"/>
                            <RowDefinition Height="1*"/>
                            <RowDefinition Height="1*"/>
                            <RowDefinition Height="1*"/>
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="1*"/>
                            <ColumnDefinition Width="1*"/>
                            <ColumnDefinition Width="1*"/>
                            <ColumnDefinition Width="1*"/>
                            <ColumnDefinition Width="1*"/>
                        </Grid.ColumnDefinitions>
                    </Grid>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style> 

请注意,椭圆并没有存在于样式中,而是在相应的类 WindowsProgressRing 中添加。

前景色设置椭圆的 Fill
Background 设置背景,正如你可能猜到的那样。

  /// <summary>
  /// Class WindowsProgressRing.
  /// </summary>
  [TemplatePart(Name = "PART_body", Type = typeof(Grid))]
  public class WindowsProgressRing : Control
  {
    /// <summary>
    /// The part body
    /// </summary>
    private Grid partBody;

    #region -- Properties --

    public Grid Body { get { return partBody; } }

    #region Speed
    /// <summary>
    /// The speed property
    /// </summary>
    public static readonly DependencyProperty SpeedProperty = DependencyProperty.Register(
      "Speed ", typeof(Duration), typeof(WindowsProgressRing), 
      new FrameworkPropertyMetadata(new Duration(TimeSpan.FromSeconds(2.5)),
        FrameworkPropertyMetadataOptions.AffectsRender, SpeedChanged, SpeedValueCallback));

    /// <summary>
    /// Gets or sets the speed.
    /// </summary>
    /// <value>The speed.</value>
    public Duration Speed
    {
      get { return (Duration)GetValue(SpeedProperty); }
      set { SetValue(SpeedProperty, value); }
    }
    /// <summary>
    /// Speed changed.
    /// </summary>
    /// <param name="dependencyObject">The dependency object.</param>
    /// <param name="dependencyPropertyChangedEventArgs"
    /// >The <see cref="DependencyPropertyChangedEventArgs" /> 
    /// instance containing the event data.</param>
    private static void SpeedChanged(DependencyObject dependencyObject, 
    DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
    {
      var wpr = (WindowsProgressRing) dependencyObject;
      if (wpr.Body == null) return;
      var speed = (Duration)dependencyPropertyChangedEventArgs.NewValue;
      wpr.SetStoryBoard(speed);
    }

    /// <summary>
    /// Speed value callback.
    /// </summary>
    /// <param name="dependencyObject">The dependency object.</param>
    /// <param name="baseValue">The base value.</param>
    /// <returns>System.Object.</returns>
    private static object SpeedValueCallback(DependencyObject dependencyObject, object baseValue)
    {
      if (((Duration)baseValue).HasTimeSpan && 
      ((Duration)baseValue).TimeSpan > TimeSpan.FromSeconds(5))
        return new Duration(TimeSpan.FromSeconds(5));
      return baseValue;
    }
    #endregion // Speed

    #region Items
    /// <summary>
    /// The items property
    /// </summary>
    public static readonly DependencyProperty ItemsProperty = 
     DependencyProperty.Register("Items", typeof(int),
      typeof(WindowsProgressRing), new FrameworkPropertyMetadata(6,
        FrameworkPropertyMetadataOptions.AffectsRender, ItemsChanged, ItemsValueCallback));

    /// <summary>
    /// Gets or sets the items.
    /// </summary>
    /// <value>The items.</value>
    public int Items
    {
      get { return (int)GetValue(ItemsProperty); }
      set { SetValue(ItemsProperty, value); }
    }

    /// <summary>
    /// Items changed.
    /// </summary>
    /// <param name="d">The d.</param>
    /// <param name="e">The <see 
    /// cref="DependencyPropertyChangedEventArgs" /> instance containing the event data.</param>
    private static void ItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
      var wpr = (WindowsProgressRing)d;
      if (wpr.Body == null) return;
      wpr.Body.Children.Clear();
      var items = (int)e.NewValue;
      for (var i = 0; i < items; i++)
      {
        var ellipse = new Ellipse
                              {
                                VerticalAlignment = VerticalAlignment.Stretch,
                                HorizontalAlignment = HorizontalAlignment.Stretch,
                                ClipToBounds = false,
                                RenderTransformOrigin = new Point(0.5, 2.5)
                              };
        wpr.Body.Children.Add(ellipse);
        // Binding 
        var binding = new Binding(ForegroundProperty.Name)
                      {
                        RelativeSource = new RelativeSource
                        (RelativeSourceMode.FindAncestor, typeof (WindowsProgressRing), 1)
                      };
        BindingOperations.SetBinding(ellipse, Shape.FillProperty, binding);
        // Placement
        Grid.SetColumn(ellipse, 2);
        Grid.SetRow(ellipse, 0);
      }
      wpr.SetStoryBoard(wpr.Speed);
    }

    /// <summary>
    /// Items callback.
    /// </summary>
    /// <param name="d">The d.</param>
    /// <param name="basevalue">The base value.</param>
    /// <returns>System.Object.</returns>
    private static object ItemsValueCallback(DependencyObject d, object basevalue)
    {
      if ((int)basevalue > 20)
        return 20;
      if ((int)basevalue < 1)
        return 1;
      return basevalue;
    }
    #endregion

    #endregion

    /// <summary>
    /// Sets the story board.
    /// </summary>
    /// <param name="speed">The speed.</param>
    private void SetStoryBoard(Duration speed)
    {
      int delay = 0;
      foreach (Ellipse ellipse in partBody.Children)
      {
        ellipse.RenderTransform = new RotateTransform(0);
        var animation = new DoubleAnimation(0, -360, speed)
        {
          RepeatBehavior = RepeatBehavior.Forever,
          EasingFunction = new QuarticEase { EasingMode = EasingMode.EaseInOut },
          BeginTime = TimeSpan.FromMilliseconds(delay += 100)
        };
        var storyboard = new Storyboard();
        storyboard.Children.Add(animation);
        Storyboard.SetTarget(animation, ellipse);
        Storyboard.SetTargetProperty(animation, 
        new PropertyPath("(Rectangle.RenderTransform).(RotateTransform.Angle)"));
        storyboard.Begin();
      }
    }

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

    /// <summary>
    /// When overridden in a derived class, is invoked whenever application code 
    /// or internal processes call <see cref="M:System.Windows.FrameworkElement.ApplyTemplate" />.
    /// </summary>
    public override void OnApplyTemplate()
    {
      base.OnApplyTemplate();
      partBody = Template.FindName("PART_body", this) as Grid;
      ItemsChanged(this, new DependencyPropertyChangedEventArgs(ItemsProperty, 0, Items));
      SpeedChanged(this, new DependencyPropertyChangedEventArgs
                           (SpeedProperty, Duration.Forever, Speed));
    }
  }
}

使用进度环

示例应用程序以不同的设置显示进度环,以便你了解控件的灵活性。

图片:看起来有点奇怪,但只是运行应用程序的快照。

这是 MainWindow.xaml

<Window x:Class="WindowsProgressRingSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:controls="clr-namespace:NMT.Wpf.Controls;assembly=WindowsProgressRing"
        Title="WindowsProgressRing Sample" Height="284" Width="290" >
    <Grid>
        <controls:WindowsProgressRing Foreground="Black" 
        Speed="0:0:2.5" Margin="10,10,167,141" Items="6" />
        <controls:WindowsProgressRing Foreground="White" 
        Background="DodgerBlue" Speed="0:0:2.5" 
        Margin="176,10,10,149" Items="6" />
        <controls:WindowsProgressRing Foreground="Red" 
        Speed="0:0:2.5" Margin="10,117,222,86" Items="1" />
        <controls:WindowsProgressRing Foreground="Blue" 
        Speed="0:0:2.5" Margin="65,117,167,86" Items="3" />
        <controls:WindowsProgressRing Foreground="Green" 
        Speed="0:0:2.5" Margin="167,117,65,86" Items="10" />
        <controls:WindowsProgressRing Foreground="Purple" 
        Speed="0:0:2.5" Margin="222,117,10,86" Items="20" />
        <controls:WindowsProgressRing Foreground="DeepPink" 
        Speed="0:0:5" Margin="10,193,222,10" Items="20"/>
        <controls:WindowsProgressRing Foreground="Orange" 
        Speed="0:0:7" Margin="65,193,167,10" Items="5"/>
        <controls:WindowsProgressRing Foreground="DeepSkyBlue" 
        Speed="0:0:1.25" Margin="167,193,65,10" Items="5"/>
        <controls:WindowsProgressRing Foreground="DarkSlateGray" 
        Speed="0:0:.8" Margin="222,193,10,10" Items="3"/>
    </Grid>
</Window>  

关注点

我将椭圆的 Fill 绑定到父级的 Foreground 画笔。
我最初是在 XAML 中使用 TemplatedParent 作为 RelativeSource 来完成的,就像这样

<Ellipse x:Name="PART_ellipse1" Grid.Column="2" Grid.Row="0" 
 Fill="{TemplateBinding Foreground}" ClipToBounds="False" HorizontalAlignment="Stretch" 
 VerticalAlignment="Stretch" RenderTransformOrigin="0.5,2.5"/> 

我真的尝试用代码实现相同的解决方案,但我只能使用 FindAncestor 才能使其工作。

// Binding 
var binding = new Binding(ForegroundProperty.Name)
{
  RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof (WindowsProgressRing), 1)
};
BindingOperations.SetBinding(ellipse, Shape.FillProperty, binding); 

结论

就个人而言,我发现 WPF 解决方案要简单得多,而且速度也快得多。

历史

  • 初始版本
© . All rights reserved.