Better WPF Circular Progress Bar

A while back I posted a blog post about a simple Circular Progress Bar that I did for WPF. The original post is right here : http://sachabarber.net/?p=429

It turns out that was not the best thing to do, as the old approach used a never ending animation, that was even running when the controls Visibility changed. I did notice this pretty quickly, when we profiled our app, and noticed this hot spot exactly where the progress bar was. So what we did to fix that is just remove the control when it should stop showing progress. Anyway that was the old way.

I am pleased to announce that I have a new improved Circular Progress Bar that no longer uses a never ending animation, in fact it is a lot simpler and just uses a DispatcherTimer and some elementary trigonometry, and it actually looks more like the style of progress bar we are all used to seeing on the web. Without further ado here is the code:

The xaml for the CircularProgressBar.xaml

<UserControl x:Class="ThreadingComponent.CircularProgressBar"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Height="Auto" Width="Auto" Background="Transparent" 
             IsVisibleChanged="HandleVisibleChanged">
    <Grid x:Name="LayoutRoot" Background="Transparent" 
          ToolTip="Searching...."
          HorizontalAlignment="Center" 
          VerticalAlignment="Center">
        <Canvas RenderTransformOrigin="0.5,0.5" 
                HorizontalAlignment="Center" 
             VerticalAlignment="Center" Width="120" 
             Height="120" Loaded="HandleLoaded" 
                Unloaded="HandleUnloaded"  >
            <Ellipse x:Name="C0" Width="20" Height="20" 
                     Canvas.Left="0" 
                     Canvas.Top="0" Stretch="Fill" 
                     Fill="Black" Opacity="1.0"/>
            <Ellipse x:Name="C1" Width="20" Height="20" 
                     Canvas.Left="0"
                     Canvas.Top="0" Stretch="Fill" 
                     Fill="Black" Opacity="0.9"/>
            <Ellipse x:Name="C2" Width="20" Height="20" 
                     Canvas.Left="0" 
                     Canvas.Top="0" Stretch="Fill" 
                     Fill="Black" Opacity="0.8"/>
            <Ellipse x:Name="C3" Width="20" Height="20" 
                     Canvas.Left="0" 
                     Canvas.Top="0" Stretch="Fill" 
                     Fill="Black" Opacity="0.7"/>
            <Ellipse x:Name="C4" Width="20" Height="20" 
                     Canvas.Left="0" 
                     Canvas.Top="0" Stretch="Fill" 
                     Fill="Black" Opacity="0.6"/>
            <Ellipse x:Name="C5" Width="20" Height="20" 
                     Canvas.Left="0" 
                     Canvas.Top="0" Stretch="Fill" 
                     Fill="Black" Opacity="0.5"/>
            <Ellipse x:Name="C6" Width="20" Height="20" 
                     Canvas.Left="0" 
                     Canvas.Top="0" Stretch="Fill" 
                     Fill="Black" Opacity="0.4"/>
            <Ellipse x:Name="C7" Width="20" Height="20" 
                     Canvas.Left="0" 
                     Canvas.Top="0" Stretch="Fill" 
                     Fill="Black" Opacity="0.3"/>
            <Ellipse x:Name="C8" Width="20" Height="20" 
                     Canvas.Left="0" 
                     Canvas.Top="0" Stretch="Fill" 
                     Fill="Black" Opacity="0.2"/>
            <Canvas.RenderTransform>
                <RotateTransform x:Name="SpinnerRotate" 
                     Angle="0" />
            </Canvas.RenderTransform>
        </Canvas>
    </Grid>
</UserControl>

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

And here is the CircularProgressBar.xaml.cs

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using System.Windows.Input;
using System.Windows.Shapes;

namespace ThreadingComponent
{
    /// <summary>
    /// A circular type progress bar, that is simliar to popular web based
    /// progress bars
    /// </summary>
    public partial class CircularProgressBar
    {
        #region Data
        private readonly DispatcherTimer animationTimer;
        #endregion

        #region Constructor
        public CircularProgressBar()
        {
            InitializeComponent();

            animationTimer = new DispatcherTimer(
                DispatcherPriority.ContextIdle, Dispatcher);
            animationTimer.Interval = new TimeSpan(0, 0, 0, 0, 75);
        }
        #endregion

        #region Private Methods
        private void Start()
        {
            Mouse.OverrideCursor = Cursors.Wait;
            animationTimer.Tick += HandleAnimationTick;
            animationTimer.Start();
        }

        private void Stop()
        {
            animationTimer.Stop();
            Mouse.OverrideCursor = Cursors.Arrow;
            animationTimer.Tick -= HandleAnimationTick;
        }

        private void HandleAnimationTick(object sender, EventArgs e)
        {
            SpinnerRotate.Angle = (SpinnerRotate.Angle + 36) % 360;
        }

        private void HandleLoaded(object sender, RoutedEventArgs e)
        {
            const double offset = Math.PI;
            const double step = Math.PI * 2 / 10.0;

            SetPosition(C0, offset, 0.0, step);
            SetPosition(C1, offset, 1.0, step);
            SetPosition(C2, offset, 2.0, step);
            SetPosition(C3, offset, 3.0, step);
            SetPosition(C4, offset, 4.0, step);
            SetPosition(C5, offset, 5.0, step);
            SetPosition(C6, offset, 6.0, step);
            SetPosition(C7, offset, 7.0, step);
            SetPosition(C8, offset, 8.0, step);
        }


        private void SetPosition(Ellipse ellipse, double offset, 
            double posOffSet, double step)
        {
            ellipse.SetValue(Canvas.LeftProperty, 50.0 
                + Math.Sin(offset + posOffSet * step) * 50.0);

            ellipse.SetValue(Canvas.TopProperty, 50 
                + Math.Cos(offset + posOffSet * step) * 50.0);
        }


        private void HandleUnloaded(object sender, RoutedEventArgs e)
        {
            Stop();
        }

        private void HandleVisibleChanged(object sender, 
            DependencyPropertyChangedEventArgs e)
        {
            bool isVisible = (bool)e.NewValue;

            if (isVisible)
                Start();
            else
                Stop();
        }
        #endregion
    }
}

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

And to use it you can simply make it any size you like by putting it into a ViewBox like so:

<UserControl x:Class="ThreadingComponent.BusyUserControl"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:ThreadingComponent"
    Height="Auto" Width="Auto" 
    HorizontalAlignment="Stretch" 
    VerticalAlignment="Stretch">

        
        <Viewbox Width="200" Height="200"
                HorizontalAlignment="Center" 
                VerticalAlignment="Center">
            <local:CircularProgressBar />
        </Viewbox>


    </Grid>

</UserControl>

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

And here is what it looks like when its running

All the code is here is a cut and pastable format, so no ZIP file this time, just cut and paste this code, if you don’t know how to do that, step away from the XAML.

.csharpcode, .csharpcode pre
{
font-size: small;
color: black;
font-family: consolas, “Courier New”, courier, monospace;
background-color: #ffffff;
/*white-space: pre;*/
}
.csharpcode pre { margin: 0em; }
.csharpcode .rem { color: #008000; }
.csharpcode .kwrd { color: #0000ff; }
.csharpcode .str { color: #006080; }
.csharpcode .op { color: #0000c0; }
.csharpcode .preproc { color: #cc6633; }
.csharpcode .asp { background-color: #ffff00; }
.csharpcode .html { color: #800000; }
.csharpcode .attr { color: #ff0000; }
.csharpcode .alt
{
background-color: #f4f4f4;
width: 100%;
margin: 0em;
}
.csharpcode .lnum { color: #606060; }

About these ads

21 thoughts on “Better WPF Circular Progress Bar

  1. Very cool…I’ve changed a couple of things though. might not be the right thing to do but:
    In the circularprogressbar xaml I’ve added the viewbox directly so you don’t need to remember it when you add an instance..i.e.

    <Grid x:Name="LayoutRoot" Background="Transparent"
    ToolTip="Searching…." …

    I’ve also added to the following to HandleVisibleChanged to stop it running in design mode…
    private void HandleVisibleChanged(object sender,
    DependencyPropertyChangedEventArgs e)
    {

    if (System.ComponentModel.DesignerProperties.GetIsInDesignMode(this)) return;

    bool isVisible = (bool)e.NewValue;
    if (isVisible)
    Start();
    else
    Stop();
    }

  2. Mark Koopmans says:

    Sacha,
    I would like to know if you can put me in contact with someone of your caliber to assist in the development of a system based strongly on WPF. I am the CTO of a company working on a product the relies on WPF,WCF,SQL and IIS with an interface built in XAML and wired up using C#. Our problem has been identifying individuals that have these skill sets. Our product has the look and feel of a KIOSK with no access to the underlying OS by the user. It is essentially a full screen WPF application with custom graphics and user controls on a touch screen PC to access applications in a content window within the app. I can explain more in detail if you are are interested in the project or if you require more info to direct me to someone who may be able to assist us.
    My apologies for utilizing your blog to contact you, could not find another way. You name seems to pop up in all things WPF, I am confident you can get me pointed in the right direction.

    Cheers!!

    Mark

  3. mekansm says:

    Awesome control!!! :)

    One issue i had was after I got done using this in my application, when I moused over gridsplitters my mouse would not update. If you use Mouse.SetCursor() instead, it seems to be a quick fix for the problem.

    What I eventually did though, was completely remove the setting of the cursor. I prefer to hold a State property on my ViewModel and then set up a DataTrigger to show the progress bar when my object is loading/processing. If that makes sense….? If your confused I can dig up a simple example for you.

  4. Alden Alexander says:

    Hey, great job on this control. I noticed that the control is overriding the mouse cursor and setting it to an arrow when it is done. This should be changed so that it sets the OverrideCursor to null instead so other cursors (IBeams, Resizing, etc…) still show up as expected in the rest of the application.

    • sacha says:

      Add extra DependencyProperty to this control something like IsRunnable, then in changed callback for this DP, if IsRunnable is true call Start() if its false call Stop() method

  5. Jacob Easo says:

    Thanks Sacha!
    This was indeed useful. However I modified the code slightly and decided to just change the opacity of the ellipses rather than use the <RotateTransform x:Name="SpinnerRotate"
    We get the same effect of a spinning circle. Thanks again

    private void HandleAnimationTick(object sender, EventArgs e)
    {
    //SpinnerRotate.Angle = (SpinnerRotate.Angle + 36) % 360;

    double i = 0;
    i = C0.Opacity;
    C0.Opacity = C1.Opacity;
    C1.Opacity = C2.Opacity;
    C2.Opacity = C3.Opacity;
    C3.Opacity = C4.Opacity;
    C4.Opacity = C5.Opacity;
    C5.Opacity = C6.Opacity;
    C6.Opacity = C7.Opacity;
    C7.Opacity = C8.Opacity;
    C8.Opacity = C9.Opacity;
    C9.Opacity = i;
    }

  6. Dariusmol says:

    Sacha,

    I’ve been following your work all along and I admire what your creativity and passion for WPF.
    However you definitely dropped the ball on this one…
    Be honest to yourself and everybody else and admit it…
    you CANNOT refresh the circular bar to spin from the same thread and it will throw an exception if you try to do it on the different thread…
    it is not possible (at least in the scenario you describing here where the control is hosted on the same page).

    • sacha says:

      Dariousmol

      That is quite agressive. Now lets tackle this shall we. When an author writes a blog you try and condense it down to a very small thing this one simply shows how to do a circular progress bar, that is it.

      You know rightly or wrongly I assume that the reader will do some more work themselves. Like thinking of Dispatcher threading issues. I am completely aware that you need to do multithreading for this progress bar, which is why I wrote another whole article around a component which would do this for the user : http://www.codeproject.com/KB/WPF/ThreadingComponent.aspx

      That article is some 2 years old so it has been around a lot longer than this latest comment of yours. And I have improved it over the years since I wrote that article and my Cinch MVVM framework now includes a really good version of it.

      Perhaps you should not throw stones, until you have dug a little deeper.

  7. Dariusmol says:

    Sasha,

    If you feel offended, I apologize for what I’ve said…I honestly did not intend to vent out my frustration but it rather meant to be a constructive critisism of sorts…but I really had no right to do this…so as I admitted – my bad, mate.
    Having said that I still stand by my believe that it cannot be done in the fashion you described in your previous comments to another reader (with BeginInvoke() etc.) and yes, I have been following your work and articles for quite some time and learned a great deal from your valuable knoweledge that you are kind enough to share with the community.

  8. sacha says:

    Dariusmol

    Sorry I was in bad mood about something else, sorry. yeah you are 100% correct, you need threads to dot it. That article I posted link to and the work in my Cinch framework show how I do it at work. Works a treat.

    Sorry man

  9. jaja says:

    In the stop method is better to set Mouse.OverrideCursor = null; instead of Cursors.Arrow, to restore the expected behaviour of the mouse ;)

  10. Great component! I created a dependency property for the color of the circle so I could make it white on black.

    public static DependencyProperty ForegroundProperty =
    DependencyProperty.Register(“Foreground”,
    typeof(Brush),
    typeof(CircularProgressBar));

    public Brush Foreground
    {
    get { return (Brush)GetValue(ForegroundProperty); }
    set { SetValue(ForegroundProperty, value); }
    }

    and set the name of the UserControl to control and set all the Ellipse to have

    Fill=”{Binding ElementName=control, Path=Foreground}”

    • sachabarber says:

      To be honest I tend to do it like this now days

      <Style TargetType="{x:Type local:CircularProgressBar}">
              <Setter Property="Template">
                  <Setter.Value>
                      <ControlTemplate TargetType="{x:Type local:CircularProgressBar}">
      
                          <Grid Background="Transparent" Width="120" Height="120" ClipToBounds="True" HorizontalAlignment="Center" VerticalAlignment="Center">
      
                              <Canvas 
                                  RenderTransformOrigin="0.5,0.5"
                                  HorizontalAlignment="Center"
                                  VerticalAlignment="Center"
                                  Width="120"
                                  Height="120">
      
                                  <FrameworkElement.Resources>
                                      <Style TargetType="Ellipse">
                                          <Setter Property="Width" Value="20" />
                                          <Setter Property="Height" Value="20" />
                                          <Setter Property="Fill" Value="{DynamicResource toolBarBackGround}" />
                                      </Style>
                                  </FrameworkElement.Resources>
      
                                  <FrameworkElement.Style>
                                      <Style TargetType="{x:Type Canvas}">
      
                                          <Setter Property="RenderTransform">
                                              <Setter.Value>
                                                  <RotateTransform />
                                              </Setter.Value>
                                          </Setter>
      
                                          <Style.Triggers>
                                              <Trigger Property="IsVisible" Value="True">
                                                  <Trigger.EnterActions>
                                                      <BeginStoryboard Name="EllipsesStoryboard">
                                                          <Storyboard
                                                              Storyboard.TargetProperty="RenderTransform.Angle">
                                                              <DoubleAnimationUsingKeyFrames 
                                                                  RepeatBehavior="Forever"
                                                                  Duration="0:0:1.8">                                                           
                                                                  <animation:StepDoubleKeyFrame 
                                                                      Steps="10"
                                                                      Value="360" />
                                                              </DoubleAnimationUsingKeyFrames>
                                                          </Storyboard>
                                                      </BeginStoryboard>
                                                  </Trigger.EnterActions>
                                                  <Trigger.ExitActions>
                                                      <StopStoryboard BeginStoryboardName="EllipsesStoryboard" />
                                                  </Trigger.ExitActions>
                                              </Trigger>
                                          </Style.Triggers>
                                      </Style>
                                  </FrameworkElement.Style>
      
                                  <Ellipse
                                      Canvas.Left="20.6107373853763"
                                      Canvas.Top="90.4508497187473"
                                      Opacity="1.0"/>
                                  <Ellipse
                                      Canvas.Left="50"
                                      Canvas.Top="100"
                                      Opacity="0.9"/>
                                  <Ellipse
                                      Canvas.Left="79.3892626146236"
                                      Canvas.Top="90.4508497187473"
                                      Opacity="0.8"/>
                                  <Ellipse
                                      Canvas.Left="97.5528258147576"
                                      Canvas.Top="65.4508497187473"
                                      Opacity="0.7"/>
                                  <Ellipse
                                      Canvas.Left="97.5528258147576"
                                      Canvas.Top="34.5491502812526"
                                      Opacity="0.6"/>
                                  <Ellipse
                                      Canvas.Left="79.3892626146236"                    
                                      Canvas.Top="9.54915028125263" 
                                      Opacity="0.5"/>
                                  <Ellipse
                                      Canvas.Left="50"
                                      Canvas.Top="0"
                                      Opacity="0.4"/>
                                  <Ellipse
                                      Canvas.Left="20.6107373853763" 
                                      Canvas.Top="9.54915028125262"
                                      Opacity="0.3"/>
                                  <Ellipse
                                      Canvas.Left="2.44717418524232"
                                      Canvas.Top="34.5491502812526"
                                      Opacity="0.2"/>
                              </Canvas>
                          </Grid>
                      </ControlTemplate>
                  </Setter.Value>
              </Setter>
          </Style>
      

      using System;
      using System.Collections.Generic;
      using System.Linq;
      using System.Text;
      using System.Windows.Media.Animation;
      using System.Windows;

      namespace Moneycorp.Omni.UI.UserControls.Animation
      {
      public class StepDoubleKeyFrame : DoubleKeyFrame
      {
      public int Steps
      {
      get { return (int)GetValue(StepsProperty); }
      set { SetValue(StepsProperty, value); }
      }

      public static readonly DependencyProperty StepsProperty =
      DependencyProperty.Register(“Steps”, typeof(int), typeof(StepDoubleKeyFrame), new UIPropertyMetadata(10));

      protected override double InterpolateValueCore(double baseValue, double keyFrameProgress)
      {
      double progress = keyFrameProgress * Steps;
      double oneStep = (this.Value – baseValue) / Steps;
      double result = progress * oneStep;
      double steps = Math.Floor(result / oneStep);
      return steps * oneStep;
      }

      protected override Freezable CreateInstanceCore()
      {
      return new StepDoubleKeyFrame() { Steps = Steps };
      }
      }
      }

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s