Timo Korinth

2 Meter, 2 Mark

Creating a circular progress bar in WPF or Silverlight

| 39 Comments

Final result

CircularProgressBarWPFSL

Features

The goal of this article is to create a circular progress bar (see the examples in the screenshot above) as simple as possible, that is working in Silverlight and WPF and only uses the path arc segment. Because of that, it’s fully customizable.

  • Draws an arc segment based on the angle / percentage
  • Specifies radius of the circle
  • Only uses path ArcSegment
  • Specifies Stroke Thickness
  • Fully customizable
  • Runs in Silverlight AND WPF
  • Implementation as simple as possible

Creating the circular progress bar

First of all we are creating a user control with the following content (in a real project you will create a custom control instead):

<Path x:Name="pathRoot" Stroke="{Binding SegmentColor, ElementName=userControl}" 
    StrokeThickness="{Binding StrokeThickness, ElementName=userControl}" 
    HorizontalAlignment="Left" VerticalAlignment="Top">
    <Path.Data>
        <PathGeometry>
            <PathGeometry.Figures>
                <PathFigureCollection>
                    <PathFigure x:Name="pathFigure">
                        <PathFigure.Segments>
                            <PathSegmentCollection>
                                <ArcSegment x:Name="arcSegment" SweepDirection="Clockwise" />
                            </PathSegmentCollection>
                       </PathFigure.Segments>
                    </PathFigure>
                </PathFigureCollection>
            </PathGeometry.Figures>
        </PathGeometry>
    </Path.Data>
</Path>

The properties “Stroke” and “StrokeThickness” of the path element are bound to dependency properties of the user control.
We’ll get to the code behind class in a minute, but let’s get the XAML code right. The path’s data property is set to a path geometry figure, which consists of a path segment collection.
The first and only segment is an arc segment, which is used to draw the circular progress bar. Notice, that the path figure and the arc segment got a name attached.
These references are used in the code behind to update the arc segment (in a custom control template you will provide these references as a template part). That’s all we have to do in the XAML part of the user control.
Now let’s get to the interesting part.

Code behind

The code behind class is the heart of the circular progress bar, but it’s not that complicated. Let’s start with the most difficult part of the control: the calculation of the arc segment.
To understand the meaning of the arc segment, it is necessary to dig into the world of higher mathematics.
Or you just read along and get the solution to the problem of drawing a perfect circle with the arc segment.

public void RenderArc()
{
       Point startPoint =  new Point(Radius, 0);
       Point endPoint = ComputeCartesianCoordinate(Angle, Radius);
       endPoint.X += Radius;
       endPoint.Y += Radius;

       pathRoot.Width = Radius * 2 + StrokeThickness;
       pathRoot.Height = Radius * 2 + StrokeThickness;
       pathRoot.Margin = new Thickness(StrokeThickness, StrokeThickness, 0, 0);

       bool largeArc = Angle > 180.0;

       Size outerArcSize = new Size(Radius, Radius);

       pathFigure.StartPoint = startPoint;

       if (startPoint.X == Math.Round(endPoint.X) && startPoint.Y == Math.Round(endPoint.Y))
           endPoint.X -= 0.01;

       arcSegment.Point = endPoint;
       arcSegment.Size = outerArcSize;
       arcSegment.IsLargeArc = largeArc;
}

The arc segment doesn’t have a starting point property. It starts from the last drawn point of the path figure.
In this example the start point of the path figure is set to the top center of the control (Radius, 0).
The end point of the arc segment is calculated depending on the radius and the angle. Now we’ll take a look at the calculation:

private Point ComputeCartesianCoordinate(double angle, double radius)
{
       double angleRad = (Math.PI / 180.0) * (angle - 90);

       double x = radius * Math.Cos(angleRad);
       double y = radius * Math.Sin(angleRad);

       return new Point(x, y);
}

It translates the angle to a cartesian coordinate using the radius of the circle. It’s necessary to get the end point as an absolute coordinate, because the arc segment needs to get this information.
Last but not least, here is the calculation of the angle, depending on the percentage (0% to 100%):

Angle = (Percentage * 360) / 100;

To try this sample, feel free to download the source code or ask any questions in the comments.

Source Code:
CircularProgressBar.zip

39 Comments

  1. Thank you for your post. You saved me a lot of time.

    The only modifications I made were to (1) move “RenderArc();” from the constructor to the Loaded event and (2) in the RenderArc function added “if (this.IsLoaded == true)…”.

    We observed an issue where the the arc was not correctly rendered depending on the placement of the Percentage setting in the XAML tag. So, for example “…StrokeThickness=”25″ Percentage=”100″ />” would work, but “…Percentage=”60″ StrokeThickness=”25″ />” would not work.

    These changes solved the problem.

  2. Why if the radius is too small(for example Radius=”10″) there is some problem in the drawing of the arc?

  3. Hi matteo, make sure that the StrokeThickness is <= 2 x Radius. In your example try a Thickness of a maximum of 20.

  4. Oh hay there,

    I tried using this on windows phone 7, but the bar appears to lose its circle appearance with percentages under 65. AM i doing it wrong?

  5. Thank you so much for this wonderful progress bar! Just wondering, how could I make this thread safe? I update my percentage from another thread, but there is no invoke.

    Thanks!

  6. Great article!

  7. Awesome control but it does not work on Windows Phone. Tried it on WP7 and WP8, copied code from both WPF and Silverlight versions but all I could see was a gray unchanging square. It is a black eye for Microsoft. Why does it have to be so different between platforms? Why every time porting from WPF to WP7/8 is such a pain and waste of time.
    Anyway, whining aside how can I make it work on WP8? What is the magic sauce that is missing?

    • It works fine to me on WP, I just rebind in blend the SegmentColor to the UserControl.

  8. How to resize the control and make it bigger?

  9. Thanks helps me .. a lot

  10. Muchas gracias!

  11. Hello,
    this is the best solution for a circular progress bar in WPF so far as i know. May can i use this in one of my programs which im going to sell? Happy to hear from you! Great job u did there.

    Fabian

  12. Hi Man,
    I searched for a couple of things regarding this..
    But simply done it with the Arc segmentt. THis is what i exactly searching..
    Super.!!!!! ..
    Thank u so much for nice article…

  13. Thank you very much. Your solution works fine.

  14. Dude, thank you. You have saved me lots of time

  15. Nice Thing, thank you

  16. Many Thanks. You saved my time :) great work..

  17. I’ll use parts of your amazing control for one plugin I’m writing. Thanks a lot for this article.

  18. Pingback: Circular Progress Bar in Windows Phone 8.1 Store App | 我爱源码网

  19. Hello,

    Thanks so Much for the big effort, so simple and easy to use, it’s really life saving.
    Can you me how to make it work CounterClockWise?

  20. Hello !
    Great component but I didn’t find how to resize the control and make it bigger or smaller?
    Can you help me?

  21. Thanks for your work!
    I just want to make one remark. When we do:
    pathRoot.Width = Radius * 2 + StrokeThickness;
    pathRoot.Height = Radius * 2 + StrokeThickness;
    pathRoot.Margin = new Thickness(StrokeThickness, StrokeThickness, 0, 0);
    we get (StrokeThinkness / 2) margin aroud circular progress bar. But if we take (StrokeThikness / 2) instead of (StrokeThikness) there like:
    pathRoot.Width = Radius * 2 + StrokeThickness / 2.0;
    pathRoot.Height = Radius * 2 + StrokeThickness / 2.0;
    pathRoot.Margin = new Thickness(StrokeThickness / 2.0, StrokeThickness / 2.0, 0, 0);
    we get rid of that margin.

    • I’ve used this aproach. Width and Height stays the same, only Margin modified.

      _path.Width = Radius * 2 + StrokeThickness;
      _path.Height = Radius * 2 + StrokeThickness;
      _path.Margin = new Thickness(StrokeThickness / 2, StrokeThickness / 2, -StrokeThickness / 2, -StrokeThickness / 2);

  22. I want to define the start point from bottom instead of upper (sameple code).
    What will be the technique using this sample code, please help.

  23. Pingback: Creating a circular progress bar - wpf

  24. Hi,

    Apologies as I’m new to wpf, so this could be a simple one.

    I’m finding the circle “clips” and becomes square at a certain size, and I cant tell why.

    I’ve tried making the Grid of the control bigger but this still happens.

    thanks for any response

  25. Amazing code!

    what would need to change for it to go in a clockwise movement? Changing the SweepDirection does no good

  26. Best implementation!
    Made my day :)

  27. How can I adjust the code to ago CounterClockwise

  28. Hey Mark & sal,
    you can do the following:

    Change SweepDirection to “Counterclockwise” (in CircularProgressBar.xaml) and in the code behind change all Angle calculations (add a “-“):
    Line 26: Angle = -(Percentage * 360) / 100;
    Line 83: circle.Angle = -(circle.Percentage * 360) / 100;
    Line 103: bool largeArc = -Angle > 180.0;
    Line 110: endPoint.X += 0.01;

  29. How can we start from the bottom instead of the top as the startpoint?

  30. Thank you for your post. You saved me a lot of time :)

  31. Thank you ! Very userfull. I just used it in my UWP project.

  32. Yo Timo. I am trying to use this code on UWP app or WPF app with .net framework 4.6 the circular progressbar is not visible for some reason can’t find the cause very strange.

    • I have the exact same problem.

      • Some property bindings doesn’t workwith UWP and WPF. Try manually set some values.

        local:CircularProgressBar x:Name=”circularProgressBar” HorizontalAlignment=”Left” Margin=”114,231,0,0″ Percentage=”{Binding Value, ElementName=slider}” SegmentColor=”#FF0595E0″ Radius=”50″ BorderBrush=”#FF3686D6″ StrokeThickness=”8″ VerticalAlignment=”Top” Width=”124″ Height=”124″ />

Leave a Reply