13,251,998 members (62,489 online)
Add your own
alternative version

Stats

77.4K views
5K downloads
133 bookmarked
Posted 23 Aug 2009

Circular Progress Control - Mac OS X style

, 28 Aug 2009
 Rate this:
Please Sign up or sign in to vote.
Create a circular progress control, like the one in Mac OS X, using GDI+.

Introduction

I have always been impressed by the Mac OS X GUI. It is very neat and elegant. In this article, I will show you how to create a user control using GDI+ similar to the Asynchronous Circular Progress Indicator in Mac OS X.

Based on the comments that I received from BillWoodRuff and dequadin, I have refined my code further to create two new Circular Progress Controls

• Optimized Circular Progress Control
• Matrix Circular Progress Control
I will go through their details one by one...

The Circular Progress Control

In the attached project, I have created a User Control, called `CircularProgressControl`, which encapsulates the rendering of the progress control. It also provides certain extra properties like the color of the control, the speed of the progress, and the starting angle of the control. Here is the class diagram of the `CircularProgressControl`:

The `Start` and `Stop` APIs let the user start the animation and stop it, respectively.

Using the Control

In order to use this control, you can add a reference to this project and drag and drop the `CircularProgressControl` from the ToolBox onto your `Form`. You can set the color, speed, and starting angle. The starting angle is specified in degrees, and it increases in clockwise direction.

You can also set the direction of the rotation: `CLOCKWISE` or `ANTICLOCKWISE`. For that, you need to set the `Rotation` property to one of the values of the `Direction` enum.

```public enum Direction
{
CLOCKWISE,
ANTICLOCKWISE
}```

Circular Progress Control Demystified

In order to render the spokes of the control, first the control calculates two circles - inner circle and outer circle. These two circles are concentric. The radii of these two circles are dependent on the size of the `CircularProgressControl`. The start point (X1, Y1) and the end point (X2, Y2) are calculated based on the angle of the spoke. The angle between two adjacent spokes (`m_AngleIncrement`) is based on the number of spokes (`m_SpokesCount`), and it is calculated as:

`m_AngleIncrement = (int)(360/m_SpokesCount);`

The Alpha value of the color of the first spoke is 255. After each spoke is rendered, the alpha value of the next spoke's color is reduced by a fixed amount (`m_AlphaDecrement`).

The thickness of the spoke also varies with the size of the `CircularProgressControl`.

The calculation and rendering are done in the `PaintEventHandler` of the control.

```protected override void OnPaint(PaintEventArgs e)
{
// All the paintin will be handled by us.
//base.OnPaint(e);

e.Graphics.InterpolationMode =
System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
e.Graphics.SmoothingMode =
System.Drawing.Drawing2D.SmoothingMode.HighQuality;

// Since the Rendering of the spokes is dependent upon the current size of the
// control, the following calculation needs to be done within the Paint eventhandler.
int alpha = m_AlphaStartValue;
int angle = m_StartAngle;
// Calculate the location around which the spokes will be drawn
int width = (this.Width < this.Height) ? this.Width : this.Height;
m_CentrePt = new PointF(this.Width / 2, this.Height / 2);
// Calculate the width of the pen which will be used to draw the spokes
m_Pen.Width = (int)(width / 15);
if (m_Pen.Width < MINIMUM_PEN_WIDTH)
m_Pen.Width = MINIMUM_PEN_WIDTH;
// Calculate the inner and outer radii of the control.
// The radii should not be less than the
// Minimum values
m_InnerRadius = (int)(width * (140 / (float)800));
if (m_InnerRadius < MINIMUM_INNER_RADIUS)
m_InnerRadius = MINIMUM_INNER_RADIUS;
m_OuterRadius = (int)(width * (250 / (float)800));
if (m_OuterRadius < MINIMUM_OUTER_RADIUS)
m_OuterRadius = MINIMUM_OUTER_RADIUS;

// Render the spokes
for (int i = 0; i < m_SpokesCount; i++)
{
PointF pt1 = new PointF(m_InnerRadius *
(float)Math.Cos(ConvertDegreesToRadians(angle)),
m_InnerRadius * (float)Math.Sin(ConvertDegreesToRadians(angle)));
PointF pt2 = new PointF(m_OuterRadius *
(float)Math.Cos(ConvertDegreesToRadians(angle)),
m_OuterRadius * (float)Math.Sin(ConvertDegreesToRadians(angle)));

pt1.X += m_CentrePt.X;
pt1.Y += m_CentrePt.Y;
pt2.X += m_CentrePt.X;
pt2.Y += m_CentrePt.Y;
m_Pen.Color = Color.FromArgb(alpha, this.TickColor);
e.Graphics.DrawLine(m_Pen, pt1, pt2);

if (Rotation == Direction.CLOCKWISE)
{
angle -= m_AngleIncrement;
}
else if (Rotation == Direction.ANTICLOCKWISE)
{
angle += m_AngleIncrement;
}

alpha -= m_AlphaDecrement;
}
}```

When the `Start` API is called, a `Timer`, whose `TickInterval` is equal to the value of the `Interval` property of the `CircularProgressControl`, is started. Upon each `Tick` of the timer, the angle of the first spoke is increased or decreased based on the `Rotation` property, and then the `Invalidate()` method is called which forces the repaint of the control. The `Stop` API stops the timer.

Optimized Circular Progress Control

In the Optimized Circular Progress Control, I have taken out the points calculation part from the `OnPaint` method in order to improve the control's performance and moved it to another method called `CalculateSpokePoints`. This method will be called in the constructor, when the Rotation property is changed and also when the size of the control is changed either at runtime or at design time (this is handled by subscribing to the `ClientSizeChanged` event). I have also defined a data structure to store the start point and end point of each spoke.

```struct Spoke
{
public PointF StartPoint;
public PointF EndPoint;

public Spoke(PointF pt1, PointF pt2)
{
StartPoint = pt1;
EndPoint = pt2;
}
}```

In the `CalculateSpokePoints` method, the points are calculated and encapsulated in the `Spoke` structure and stored in the `m_SpokePoints`.

```/// <summary>
/// Calculate the Spoke Points and store them
/// </summary>
private void CalculateSpokesPoints()
{
m_Spokes = new List<Spoke>();

// Calculate the angle between adjacent spokes
m_AngleIncrement = (360 / (float)m_SpokesCount);
// Calculate the change in alpha between adjacent spokes
m_AlphaChange = (int)((255 - m_AlphaLowerLimit) / m_SpokesCount);

// Calculate the location around which the spokes will be drawn
int width = (this.Width < this.Height) ? this.Width : this.Height;
m_CentrePt = new PointF(this.Width / 2, this.Height / 2);
// Calculate the width of the pen which will be used to draw the spokes
m_Pen.Width = (int)(width / 15);
if (m_Pen.Width < MINIMUM_PEN_WIDTH)
m_Pen.Width = MINIMUM_PEN_WIDTH;
// Calculate the inner and outer radii of the control.
//The radii should not be less than the Minimum values
m_InnerRadius = (int)(width * INNER_RADIUS_FACTOR);
if (m_InnerRadius < MINIMUM_INNER_RADIUS)
m_InnerRadius = MINIMUM_INNER_RADIUS;
m_OuterRadius = (int)(width * OUTER_RADIUS_FACTOR);
if (m_OuterRadius < MINIMUM_OUTER_RADIUS)
m_OuterRadius = MINIMUM_OUTER_RADIUS;

float angle = 0;

for (int i = 0; i < m_SpokesCount; i++)
{
PointF pt1 = new PointF(m_InnerRadius * (float)Math.Cos(
ConvertDegreesToRadians(angle)), m_InnerRadius * (float)Math.Sin(
ConvertDegreesToRadians(angle)));
PointF pt2 = new PointF(m_OuterRadius * (float)Math.Cos(
ConvertDegreesToRadians(angle)), m_OuterRadius * (float)Math.Sin(
ConvertDegreesToRadians(angle)));

pt1.X += m_CentrePt.X;
pt1.Y += m_CentrePt.Y;
pt2.X += m_CentrePt.X;
pt2.Y += m_CentrePt.Y;

// Create a spoke based on the points generated
Spoke spoke = new Spoke(pt1, pt2);
// Add the spoke to the List
m_Spokes.Add(spoke);

if (Rotation == Direction.CLOCKWISE)
{
angle -= m_AngleIncrement;
}
else if (Rotation == Direction.ANTICLOCKWISE)
{
angle += m_AngleIncrement;
}
}
}```

The Alpha value of the spoke drawn at 0 degree angle is calculated as follows

```/// <summary>
/// Calculate the Alpha Value of the Spoke drawn at 0 degrees angle
/// </summary>
private void CalculateAlpha()
{
if (this.Rotation == Direction.CLOCKWISE)
{
if (m_StartAngle >= 0)
{
m_AlphaStartValue = 255 - (((int)((
m_StartAngle % 360) / m_AngleIncrement) + 1) *
m_AlphaChange);
}
else
{
m_AlphaStartValue = 255 - (((int)((360 +
(m_StartAngle % 360)) / m_AngleIncrement) + 1) *
m_AlphaChange);
}
}
else
{
if (m_StartAngle >= 0)
{
m_AlphaStartValue = 255 - (((int)((360 - (
m_StartAngle % 360)) / m_AngleIncrement) + 1) *
m_AlphaChange);
}
else
{
m_AlphaStartValue = 255 - (((int)(((
360 - m_StartAngle) % 360) / m_AngleIncrement) +
1) * m_AlphaChange);
}
}
}```

Now the `OnPaint` method looks like this

```/// <summary>
/// Handles the Paint Event of the control
/// </summary>
/// <param name="e">PaintEventArgs</param>
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.InterpolationMode =
System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;

int alpha = m_AlphaStartValue;

// Render the spokes
for (int i = 0; i < m_SpokesCount; i++)
{
m_Pen.Color = Color.FromArgb(alpha, this.TickColor);
e.Graphics.DrawLine(m_Pen, m_Spokes[i].StartPoint,
m_Spokes[i].EndPoint);

alpha -= m_AlphaChange;
if (alpha < m_AlphaLowerLimit)
alpha = 255 - m_AlphaChange;
}
}```

Each time the `Timer.Elapsed` occurs, I am manipulating the `m_AlphaStartValue` so that the spokes' alpha value keeps changing.

Matrix Circular Progress Control

I have further modified the `OptimizedCircularProgressControl` to create this control. In the Paint method, I am employing the Translation and Rotation transformations.

```/// <summary>
/// Handles the Paint Event of the control
/// </summary>
/// <param name="e">PaintEventArgs</param>
protected override void OnPaint(PaintEventArgs e)
{
e.Graphics.InterpolationMode =
System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
e.Graphics.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;

// Perform a Translation so that the centre of the control
// is the centre of Rotation
e.Graphics.TranslateTransform(m_CentrePt.X, m_CentrePt.Y,
System.Drawing.Drawing2D.MatrixOrder.Prepend);
// Perform a Rotation about the control's centre
e.Graphics.RotateTransform(m_StartAngle,
System.Drawing.Drawing2D.MatrixOrder.Prepend);

int alpha = 255;

// Render the spokes
for (int i = 0; i < m_SpokesCount; i++)
{
m_Pen.Color = Color.FromArgb(alpha, this.TickColor);
e.Graphics.DrawLine(m_Pen, m_Spokes[i].StartPoint,
m_Spokes[i].EndPoint);

alpha -= m_AlphaChange;
if (alpha < m_AlphaLowerLimit)
alpha = 255 - m_AlphaChange;
}

// Perform a reverse Rotation and Translation to obtain the
// original Transformation
e.Graphics.RotateTransform(-m_StartAngle,
System.Drawing.Drawing2D.MatrixOrder.Append);
e.Graphics.TranslateTransform(-m_CentrePt.X, -m_CentrePt.Y,
System.Drawing.Drawing2D.MatrixOrder.Append);
}```

The angle of rotation is changed in the `Timer.Elapsed` event handler.

When you compile the attached source code (Optimized Progress Control) and execute it, you will see a Form in which I have place the 3 Controls side by side.

Note

While using OptimizedCircularControl, if you are setting the StartAngle by yourself, make sure that you set the Rotation property first.

License

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

About the Author

 Software Developer United States
An individual with more than a decade of experience in desktop computing and mobile app development primarily on the Microsoft platform. He loves programming in C#, WPF & XAML related technologies.
Current interests include web application development, developing rich user experiences across various platforms and exploring his creative side.

Ratish's personal blog: wpfspark.wordpress.com

Comments and Discussions

 First PrevNext
 My vote of 5 version_2.01-Jul-11 3:20 version_2.0 1-Jul-11 3:20
 Re: My vote of 5 Ratish Philip3-Jul-11 5:44 Ratish Philip 3-Jul-11 5:44
 MFC Version of it. spinoza24-Jun-11 9:49 spinoza 24-Jun-11 9:49
 Re: MFC Version of it. Ratish Philip1-Jul-11 2:28 Ratish Philip 1-Jul-11 2:28
 My vote of 4 Slacker00728-Jan-11 1:34 Slacker007 28-Jan-11 1:34
 forgot to rate jeevgoran5-Nov-10 5:56 jeevgoran 5-Nov-10 5:56
 cool work jeevgoran5-Nov-10 5:54 jeevgoran 5-Nov-10 5:54
 Re: cool work Ratish Philip23-Nov-10 23:26 Ratish Philip 23-Nov-10 23:26
 My vote of 5 Member 366671417-Aug-10 2:54 Member 3666714 17-Aug-10 2:54
 Re: My vote of 5 Ratish Philip23-Nov-10 23:25 Ratish Philip 23-Nov-10 23:25
 Pleasure to see MFC VC+++ Implementation comberti3-Mar-10 4:56 comberti 3-Mar-10 4:56
 How can I use this great control running on large waiting process Edward11114-Oct-09 15:28 Edward111 14-Oct-09 15:28
 Re: How can I use this great control running on large waiting process [modified] Ratish Philip25-Oct-09 16:50 Ratish Philip 25-Oct-09 16:50
 Re: How can I use this great control running on large waiting process kjward13-Dec-09 8:10 kjward 13-Dec-09 8:10
 Thanks GlimmerMan2-Sep-09 8:43 GlimmerMan 2-Sep-09 8:43
 Re: Thanks Ratish Philip2-Sep-09 20:03 Ratish Philip 2-Sep-09 20:03
 nice work ! BillWoodruff25-Aug-09 1:06 BillWoodruff 25-Aug-09 1:06
 Re: nice work ! Ratish Philip25-Aug-09 2:10 Ratish Philip 25-Aug-09 2:10
 Re: nice work ! BillWoodruff25-Aug-09 4:55 BillWoodruff 25-Aug-09 4:55
 Allright Yves24-Aug-09 14:16 Yves 24-Aug-09 14:16
 Re: Allright Ratish Philip24-Aug-09 16:14 Ratish Philip 24-Aug-09 16:14
 Misleading title? dequadin23-Aug-09 8:55 dequadin 23-Aug-09 8:55
 Re: Misleading title? Ratish Philip23-Aug-09 15:48 Ratish Philip 23-Aug-09 15:48
 Re: Misleading title? dequadin24-Aug-09 1:25 dequadin 24-Aug-09 1:25
 Re: Misleading title? Ratish Philip24-Aug-09 8:36 Ratish Philip 24-Aug-09 8:36
 Last Visit: 31-Dec-99 19:00     Last Update: 20-Nov-17 12:17 Refresh 12 Next »

General    News    Suggestion    Question    Bug    Answer    Joke    Praise    Rant    Admin

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.171114.1 | Last Updated 28 Aug 2009
Article Copyright 2009 by Ratish Philip
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid