Click here to Skip to main content
15,884,353 members
Articles / Desktop Programming / WPF

Windows Progress Ring

Rate me:
Please Sign up or sign in to vote.
4.99/5 (28 votes)
21 Dec 2013CPOL1 min read 60.1K   2.2K   28   11
The Windows Progress Ring as custom control

Introduction

I read an interesting article on creating a Progress Ring using Windows Forms and I thought that it would be A LOT easier using WPF.
I wondered how much effort it would take and challenged myself with the task.
In this article, you can read about the results.

The Progress Ring

The progress ring is mainly observed when Windows starts and during installation, but is also used in several applications and in different variants.

Implementation

This progress ring is implemented as a custom WPF control.
Basically, the control is a Grid with 5 rows and columns and some ellipses where the ellipses rotate around the center of the grid.
The rotation is done using an animated RotateTransform including a QuarticEase function.

The control Style:

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

Notice that the ellipses are not present in the style, but are added in the corresponding class WindowsProgressRing:

Foreground sets the Fill of the ellipses.
Background sets the background as you would presumably have guessed.

C#
  /// <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));
    }
  }
}

Using the Progress Ring

The sample application displays the progress ring with different settings so you can get a good idea about the flexibility of the control.

Image: Looks a bit weird but it's just a snapshot of the running app.

This is the MainWindow.xaml:

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

Points of Interest

I bind the ellipses Fill to the parents Foreground brush.
I initially did in XAML using TemplatedParent as RelativeSource like this:

XML
<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"/> 

I really tried to make the same solution in code but I could only make it work using FindAncestor:

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

Conclusion

Personally, I found the WPF solution to be much simpler and a lot faster.

History

  • Initial version

License

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


Written By
Architect
Denmark Denmark
Name: Niel Morgan Thomas
Born: 1970 in Denmark
Education:
Dataengineer from Odense Technical University.
More than 20 years in IT-business.
Current employment:
Cloud architect at University College Lillebaelt

Comments and Discussions

 
QuestionEllipse Size Pin
AHKhalid7-May-20 17:59
AHKhalid7-May-20 17:59 
GeneralMy vote of 5 Pin
Iftikhar Akram4-Dec-16 23:31
Iftikhar Akram4-Dec-16 23:31 
Questiondirection Pin
khoirom19-Sep-16 17:24
khoirom19-Sep-16 17:24 
AnswerRe: direction Pin
Niel M.Thomas19-Sep-16 19:57
professionalNiel M.Thomas19-Sep-16 19:57 
QuestionMy Vote 5 Pin
msbsoftware10-Dec-14 23:27
msbsoftware10-Dec-14 23:27 
GeneralMy vote of 5 Pin
Agent__0077-Oct-14 18:36
professionalAgent__0077-Oct-14 18:36 
GeneralMy vote of 5 Pin
ElektroStudios17-Aug-14 13:38
ElektroStudios17-Aug-14 13:38 
GeneralRe: My vote of 5 Pin
Philippe Mori1-Feb-16 6:37
Philippe Mori1-Feb-16 6:37 
You can host a WPF control in a WinForms application. Doing a WinForms version would not make much sense as that code is mainly based on improvement made in WPF that enable that kind of thing to be done easily.
Philippe Mori

SuggestionWindows Progress Ring Pin
Nirav Chauhan18-Jun-14 0:43
Nirav Chauhan18-Jun-14 0:43 
GeneralThank you Pin
Manikandan106-Jun-14 2:59
professionalManikandan106-Jun-14 2:59 
GeneralNicely done! Pin
Ravi Bhavnani21-Dec-13 5:53
professionalRavi Bhavnani21-Dec-13 5:53 

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.