WPF Circular Progress Indicator

By Michael Detras

This article shows how to make a WPF circular progress indicator that resembles Silverlight’s loading animation.

Introduction

I wanted to use a ProgressBar control in a WPF application I am working on and make this ProgressBar look like Silverlight’s loading animation. I was not able to achieve it by changing only the style of the ProgressBar so I ended up creating a custom control.

Getting Started

First, create a WPF Custom Control Library project. By default, this creates a custom control class which inherits from the Control class. Let’s rename it to ProgressIndicator and have it inherit from the RangeBase control, so that it will have properties like Value, Minimum, and Maximum. We’ll do a bit of copying from the ProgressBar implementation. The following code snippet shows the initial implementation of the ProgressIndicator class.

public class ProgressIndicator : RangeBase
{
public static readonly DependencyProperty IsIndeterminateProperty = DependencyProperty.Register(
"IsIndeterminate", typeof(bool), typeof(ProgressIndicator));

public bool IsIndeterminate
{
get { return (bool)GetValue(IsIndeterminateProperty); }
set { SetValue(IsIndeterminateProperty, value); }
}

static ProgressIndicator()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ProgressIndicator), new FrameworkPropertyMetadata(typeof(ProgressIndicator)));
RangeBase.MaximumProperty.OverrideMetadata(typeof(ProgressIndicator), new FrameworkPropertyMetadata(100.0));
}
}

Listing 1. Initial ProgressIndicator Class

The IsIndeterminate property will tell us whether to show the Loading… text or the percentage completed. Next thing to do is define the control’s generic style. The control template should be a canvas containing ellipses or circles. The style, contained in Generic.xaml, is shown below.


<Style x:Key="EllipseStyle" TargetType="Ellipse" >
<Style.Setters>
<Setter Property="Width" Value="25"/>
<Setter Property="Height" Value="25"/>
<Setter Property="Fill" >
<Setter.Value>
<RadialGradientBrush>
<GradientStop Color="#CA2C8DDE" Offset="0.634"/>
<GradientStop Color="#39FFFFFF" Offset="1"/>
<GradientStop Color="#CA2C64DE" Offset="0.33"/>
<GradientStop Color="#B56A8FDE" Offset="0.062"/>
</RadialGradientBrush>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>

<Style TargetType="local:ProgressIndicator">
<Style.Setters>
<Setter Property="Height" Value="75"/>
<Setter Property="Width" Value="75"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="local:ProgressIndicator">
<Grid>
<Canvas
x:Name="PART_Canvas"
SnapsToDevicePixels="True"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}">
<Ellipse Style="{StaticResource EllipseStyle}"/>
<Ellipse Style="{StaticResource EllipseStyle}"/>
<Ellipse Style="{StaticResource EllipseStyle}"/>
<Ellipse Style="{StaticResource EllipseStyle}"/>
<Ellipse Style="{StaticResource EllipseStyle}"/>
<Ellipse Style="{StaticResource EllipseStyle}"/>
<Ellipse Style="{StaticResource EllipseStyle}"/>
<Ellipse Style="{StaticResource EllipseStyle}"/>
</Canvas>
<TextBlock
Name="LoadingTextBlock"
Text="Loading..."
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<TextBlock
Name="ProgressTextBlock"
Text="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=Value}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsIndeterminate" Value="True">
<Setter Property="Visibility" TargetName="ProgressTextBlock" Value="Hidden"/>
</Trigger>
<Trigger Property="IsIndeterminate" Value="False">
<Setter Property="Visibility" TargetName="LoadingTextBlock" Value="Hidden"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style.Setters>
</Style>

Listing 2. Default Control Style

The ellipses are not yet arranged in a circle. We could define the locations of these ellipses using XAML but it is hard to identify the points on the canvas manually. This might be easier though if Expression Blend is used. So our option is to use code to arrange the ellipses. The style of the ellipse sets the Fill property to a radial gradient brush. I got this brush from Walt Ritscher’s blog
. There are also useful concepts here that were used in creating the ProgressIndicator control. Now let’s arrange the ellipses by overriding the OnApplyTemplate method.

public override void OnApplyTemplate()
{
base.OnApplyTemplate();

canvas = GetTemplateChild(ElementCanvas) as Canvas;
if (canvas != null)
{
// Get the center of the canvas. This will be the base of the rotation.
double centerX = canvas.Width / 2;
double centerY = canvas.Height / 2;

// Get the no. of degrees between each circles.
double interval = 360.0 / canvas.Children.Count;
double angle = -135;

foreach (UIElement element in canvas.Children)
{
RotateTransform rotateTransform = new RotateTransform(angle, centerX, centerY);
element.RenderTransform = rotateTransform;
angle += interval;
}
}
}

Listing 3. Arranging the Elements

The logic here is to get the children of the canvas and arrange them in a circle by applying a rotate transform. The center of rotation will be the center of the canvas. The angle starts at -135 degrees and is incremented by a certain interval, which depends on the number of elements to arrange. The starting angle places the first element at the bottom of the circle. The following figure shows the control where the elements are arranged.



Figure 1. Control Template

So next thing to do is to create an animation wherein the ellipse will grow and shrink back. This can be done by using a storyboard. Let’s create another dependency property that will hold the storyboard instance. The following storyboard will be applied to all child elements of the canvas.


<Storyboard>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleX)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.5" Value="1"/>
<SplineDoubleKeyFrame KeyTime="00:00:01" Value="1"/>
<SplineDoubleKeyFrame KeyTime="00:00:01.5" Value="0"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="(UIElement.RenderTransform).(ScaleTransform.ScaleY)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="0"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.5" Value="1"/>
<SplineDoubleKeyFrame KeyTime="00:00:01" Value="1"/>
<SplineDoubleKeyFrame KeyTime="00:00:01.5" Value="0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>

Listing 4. Grow and Shrink Storyboard

This storyboard dictates that the target element must have its RenderTransform property set to an instance of a ScaleTransform. We can easily put this in XAML. The problem is that on the OnApplyTemplate method, we set the RenderTransform property to a RotateTransform instance. This will replace any transform that we put up in our XAML file. As a workaround, we can wrap each element using a ContentControl, then apply the rotate transform on the ContentControl.


canvasElements = Array.CreateInstance(typeof(UIElement), canvas.Children.Count);
canvas.Children.CopyTo(canvasElements, 0);
canvas.Children.Clear();

foreach (UIElement element in canvasElements)
{
ContentControl contentControl = new ContentControl();
contentControl.Content = element;

RotateTransform rotateTransform = new RotateTransform(angle, centerX, centerY);
contentControl.RenderTransform = rotateTransform;
angle += interval;

canvas.Children.Add(contentControl);
}

Listing 5. Workaround on Setting Transform in XAML


To start the animation, we have to create a DispatcherTimer object and begin the animation for the current element every time the Tick event is raised. The interval between these ticks can be specified. The following code snippet shows the event handler for the Tick event.


private void DispatcherTimer_Tick(object sender, EventArgs e)
{
if (canvasElements != null && ElementStoryboard != null)
{
int trueIndex = clockwise ? index : canvasElements.Length - index - 1;

FrameworkElement element = canvasElements.GetValue(trueIndex) as FrameworkElement;
StartStoryboard(element);

clockwise = index == canvasElements.Length - 1 ? !clockwise : clockwise;
index = (index + 1) % canvasElements.Length;
}
}

Listing 6. Tick Event Handler

What the event handler does is begin the storyboard and determine the element wherein the animations will be applied. The storyboard for each element is started one at a time in a clockwise or counterclockwise direction. When the animation for the last element in the array is executed, the direction is reversed. The storyboard is started using the StartStoryboard method, which is shown below.


private void StartStoryboard(FrameworkElement element)
{
NameScope.SetNameScope(this, new NameScope());
element.Name = "Element";

NameScope.SetNameScope(element, NameScope.GetNameScope(this));
NameScope.GetNameScope(this).RegisterName(element.Name, element);

Storyboard storyboard = new Storyboard();
NameScope.SetNameScope(storyboard, NameScope.GetNameScope(this));

foreach (Timeline timeline in ElementStoryboard.Children)
{
Timeline timelineClone = timeline.Clone();
storyboard.Children.Add(timelineClone);
Storyboard.SetTargetName(timelineClone, element.Name);
}

storyboard.Begin(element);
}

Listing 7. StartStoryboard Method

The Begin method of the Storyboard object can’t be invoked right away. That is because the target object of the storyboard is not yet set. But first, we have to ensure that the name scope of both the element and storyboard are the same. Otherwise, the storyboard won’t be able to find the element and will throw an exception. Also, we might not be able to change the target name of the ElementStoryBoard animations because the ElementStoryBoard is read-only. That is why another Storyboard object is created. Next, I want to create another dependency property called IsRunning so that the animation will start when the value is set to true, and will stop when the value is set to false.


public static readonly DependencyProperty IsRunningProperty = DependencyProperty.Register(
"IsRunning", typeof(bool), typeof(ProgressIndicator), new FrameworkPropertyMetadata(IsRunningPropertyChanged));

public bool IsRunning
{
get { return (bool)GetValue(IsRunningProperty); }
set { SetValue(IsRunningProperty, value); }
}

private static void IsRunningPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
ProgressIndicator progressIndicator = (ProgressIndicator)d;

if ((bool)e.NewValue)
{
progressIndicator.Start();
}
else
{
progressIndicator.Stop();
}
}

private void Start()
{
dispatcherTimer.Tick += DispatcherTimer_Tick;
dispatcherTimer.Start();
}

private void Stop()
{
dispatcherTimer.Stop();
dispatcherTimer.Tick -= DispatcherTimer_Tick;
}

Listing 8. Starting and Stopping the Storyboard

Lastly, the control is not hidden when it is not running. We can bind the Visibility property of the control template’s main grid to the IsRunning property. Creating a value converter that converts a Boolean value to a Visibility enumeration value will do the trick.


[ValueConversion(typeof(bool), typeof(Visibility))]
public class BoolToVisibilityConverter : IValueConverter
{
#region IValueConverter Members

public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return ((bool)value) ? Visibility.Visible : Visibility.Hidden;
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}

#endregion
}

Listing 9. Boolean to Visibility Value Converter

The following code shows how to use the control in a window. In this example, the IsRunning and IsIndeterminate properties are set to true.

<Window
x:Class="MainApp.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:CustomControls;assembly=CustomControls"
Title="MainWindow" Height="300" Width="300">
<Grid>
<controls:ProgressIndicator
IsRunning="True"
IsIndeterminate="True"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Grid>
</Window>

Listing 10. Using the ProgressIndicator Control

The following figure shows the ProgressIndicator control when the application is run.



Figure 2. Indeterminate Progress Indicator

Let’s try when the IsIndeterminate property is set to false. The following code snippet shows how to start the progress indicator, set the percentage in a long-running operation, and stop the control when the operation is finished.


public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();

Loaded += new RoutedEventHandler(MainWindow_Loaded);
}

private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
progressIndicator.IsRunning = true;
ThreadPool.QueueUserWorkItem(new WaitCallback(
(object o) =>
{
for (int i = 0; i <= 100; i++)
{
progressIndicator.Dispatcher.Invoke(new Action(
() =>
{
progressIndicator.Value = i;
}
), null);

Thread.Sleep(100);
}

progressIndicator.Dispatcher.Invoke(new Action(
() =>
{
progressIndicator.IsRunning = false;
}
), null);
}
));
}
}

Listing 11. Determinate Progress Indicator

When the window loads, the progress indicator is shown by setting the IsRunning property to true. The Value property is updated every 100 milliseconds until it reaches 100. Afterwards, the IsRunning property is set to false and the control is hidden. You will notice that I used the control’s dispatcher to update the properties since we are working on another thread.



Figure 3. Progress Indicator with Percentage

That sums it up. The good thing about this control is that it does not restrict the animation to scaling. Also, the number of elements is not fixed. I guess the only thing that is fixed here is that how the animations are performed in clockwise and counterclockwise directions. If you are interested in the full code, you can download the Visual Studio 2008 solution here.

Popularity  (15896 Views)
Biography - Michael Detras
.NET developer. Interested in WPF, Silverlight, and XNA.
My blog
My FAQs
Create New Account
Article Discussion: WPF Circular Progress Indicator
Michael Detras posted at Wednesday, March 10, 2010 6:17 PM
naga replied to Michael Detras at Sunday, October 10, 2010 10:22 AM
hi Michael,

This is Naga, I saw your sample code to create circular progress indicator..but hard to findout the total files i have to create if I want to use ur sample. So will you please provide me the downloadable solution for this.

thanks
Naga
naga replied to naga at Sunday, October 10, 2010 10:22 AM
hey michael,
sorry i find the solution link in your code.
naga replied to naga at Sunday, October 10, 2010 10:22 AM
hi Michael,

I tried your sample, but rotation is like from left to right and right to left instead of finising circle each time. Where I have to fix if I want circular motion.
Michael Detras replied to naga at Sunday, October 10, 2010 10:22 AM
Hi Naga,

Yes, I wrote it that way. If you want to change the behavior you can change the StoryBoard and the DispatcherTimer_Tick event handler. If you don't have time to change it, Sacha Barber created a progress bar that looks like the one you need, which is downloadable in his http://sachabarber.net/?p=639. It's a bit different implementation since he used a UserControl.

Hope this helps,
Mike
naga replied to Michael Detras at Sunday, October 10, 2010 10:22 AM
Hi mike,

Yes I changed that event..it is working fine now. But my requirement is like I have a wizard, in the third page I provided with 5 buttons, if I click first button..it should open another form with all patient details. When this form is opening it is taking time. So I would like to create circular loading progress indicator control...but dont know how to implement that in my situation. You are calling usercontrol in windows appliation directly. But in my situation if I click button then progress indicator image should come and after that form should be loaded. Do you have any idea about this?.

Thanks
Naga
Michael Detras replied to naga at Sunday, October 10, 2010 10:22 AM
What you want is to show the progress control on the form with 5 buttons, is this correct? If so, you can add it on the form's XAML file. Next, bind the IsRunning dependency property to some property in your code. When the first button is clicked, set the property to true. This will start the progress indicator. After the other form has loaded, set the property to false. I hope I got your question correctly.
naga replied to Michael Detras at Sunday, October 10, 2010 10:22 AM

hi Mike,

 

I have this kind of button in my wizard page,

 

<

 

 

Button HorizontalAlignment="Center" Margin="0,0,0,10" Width="200" Content="Create New PACE - Short Form" Command="{x:Static infrastructure:MenuCommands.CreatePACECommand}">

 

 

 

 

 

 

 

 

<Button.CommandParameter>

 

 

 

 

 

 

 

 

<MultiBinding Converter="{StaticResource PaceActivityConverter}">

 

 

 

 

 

 

 

 

<Binding Source="{x:Static infrastructure:PACEFormType.SHORT}"/>

 

 

 

 

 

 

 

 

</MultiBinding>

 

 

 

 

 

 

 

 

 

</Button.CommandParameter>

 

 

 

 

 

 

 

 

</Button>

so if i click this button it will open a Short form. but it is taking time to load the form. What i thought like if add  circular progress indicator before loading the short form, the user will be notified as form is loading. So to make that kind of environment, I visited ur article. But I am not able to make it out. will you please help me with a sample(simple button, if you click it progressindicator should be open)

 

Michael Detras replied to naga at Sunday, October 10, 2010 10:22 AM

Hi Naga,

 

Sorry for the late response. I thought I replied already. Have you solved your problem? If not, could you upload a sample solution that I can work on? Thanks!

Kurt Denhoff replied to naga at Sunday, October 10, 2010 10:22 AM
Where?