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
:
<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.
[TemplatePart(Name = "PART_body", Type = typeof(Grid))]
public class WindowsProgressRing : Control
{
private Grid partBody;
#region -- Properties --
public Grid Body { get { return partBody; } }
#region Speed
public static readonly DependencyProperty SpeedProperty = DependencyProperty.Register(
"Speed ", typeof(Duration), typeof(WindowsProgressRing),
new FrameworkPropertyMetadata(new Duration(TimeSpan.FromSeconds(2.5)),
FrameworkPropertyMetadataOptions.AffectsRender, SpeedChanged, SpeedValueCallback));
public Duration Speed
{
get { return (Duration)GetValue(SpeedProperty); }
set { SetValue(SpeedProperty, value); }
}
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);
}
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
public static readonly DependencyProperty ItemsProperty =
DependencyProperty.Register("Items", typeof(int),
typeof(WindowsProgressRing), new FrameworkPropertyMetadata(6,
FrameworkPropertyMetadataOptions.AffectsRender, ItemsChanged, ItemsValueCallback));
public int Items
{
get { return (int)GetValue(ItemsProperty); }
set { SetValue(ItemsProperty, value); }
}
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);
var binding = new Binding(ForegroundProperty.Name)
{
RelativeSource = new RelativeSource
(RelativeSourceMode.FindAncestor, typeof (WindowsProgressRing), 1)
};
BindingOperations.SetBinding(ellipse, Shape.FillProperty, binding);
Grid.SetColumn(ellipse, 2);
Grid.SetRow(ellipse, 0);
}
wpr.SetStoryBoard(wpr.Speed);
}
private static object ItemsValueCallback(DependencyObject d, object basevalue)
{
if ((int)basevalue > 20)
return 20;
if ((int)basevalue < 1)
return 1;
return basevalue;
}
#endregion
#endregion
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();
}
}
public WindowsProgressRing()
{
var res = (ResourceDictionary)Application.LoadComponent(new Uri
("/WindowsProgressRing;component/Themes/WindowsProgressRingStyle.xaml", UriKind.Relative));
Style = (Style)res["WindowsProgressRingStyle"];
}
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:
<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:
<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
:
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