Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPFSpark: 1 of n: SprocketControl

0.00/5 (No votes)
22 Dec 2011 1  
A circular progress control in WPF

Introduction

WPFSpark is an open source project in CodePlex, which I started in December 2009, with the hope of creating a library of rich user controls which can be used by the developer community. Initially, I ported the Circular Progress Control (which I had developed using C# and GDI+) to WPF. I had planned for adding a few more user controls, but, due to unavoidable commitments, I could not devote much time to this project as a result of which this project was dormant for more than a year.

Now, I have taken up the responsibility of doing justice to this project and adding more controls to the library. As a result, I have revamped the Codeplex site (and my blog too!). In this article and the subsequent articles, I will be describing in detail about how I developed these controls and how they can be used in your projects in a simple way.

History of SprocketControl

I had created the CircularProgressControl initially in C# and GDI. I had published an article in CodeProject for the same. It was titled Circular Progress Control - Mac OS X Style. Then as I gained more knowledge of WPF, I thought of porting the CircularProgressControl to WPF as well. I created a basic version of the control with limited features, renaming the control as SprocketControl, and released it in CodePlex.

With the revamp of my CodePlex site, I refactored the SprocketControl, adding more features which allow the user to customize it.

Update: WPFSpark v1.0

With the release of WPFSpark v1.0, the following changes have been incorporated into SprocketControl:

  • Internal timer stopped when control is no longer visible. It is started only when the control is Visible. This reduces CPU load.
  • Added the LowestAlpha dependency property which indicates the lowest Opacity value that must be used while rendering the SprocketControl's spokes.
  • Added the AlphaTicksPercentage dependency property which indicates the percentage of total ticks which must be considered for step by step reduction of the alpha value. The remaining ticks remain at the LowestAlpha value.
  • SprocketControl now implements IDisposable.

Check out the latest source code available at CodePlex to get the latest SprocketControl.

SprocketControl Properties

Dependency Property Type Description Default Value
AlphaTicksPercentage Double Gets or sets the percentage of total ticks which must be considered for step by step reduction of the alpha value. The remaining ticks remain at the LowestAlpha value. Value Range: 0-100. 100
Interval Double Gets or sets the duration (in milliseconds) of moving the spoke(tick) to the next position. 60 ms
IsIndeterminate Boolean Gets or sets whether the SprocketControl is a normal Progress control or an indeterminate one. True
LowestAlpha Int32 Gets or sets the lowest Opacity value that must be used while rendering the SprocketControl's spokes. 0
Progress Double Gets or sets progress of the SprocketControl. Used when IsIndeterminate=False. Value Range: 0-100. 0
Rotation Direction (enum) Gets or sets the direction of Rotation - clockwise or anticlockwise. Direction.CLOCKWISE
StartAngle Double Gets or sets the Angle (in degrees) at which the first spoke(tick) should be drawn. 270
TickColor Color Gets or sets the color of each spoke(tick) in the Control. RGB(58,58,58)
TickCount Int32 Gets or sets the number of spokes(ticks) in the SprocketControl. 12
TickStyle PenLineCap (enum) Gets or sets the shape at the end of each spoke(tick) - Flat, Round, Square or Triangle. PenLineCap.Round
TickWidth Double Gets or sets the width of each spoke(tick). 3

The StartAngle dependency property follows the following convention for specifying the angle in degrees.

SprocketControl 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 for each spoke, based on its angle. These two points are the stored as the StartPoint and EndPoint of the structure Spoke.

struct Spoke
{
    public Point StartPoint;
    public Point EndPoint;

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

The angle between two adjacent spokes (angleIncrement) is based on the number of spokes (TickCount), and it is calculated as:

angleIncrement = (int)(360/TickCount);

The calculation of the spoke points is done in the CalculateSpokePoints. This method is called in the constructor and whenever any of the following properties change:

  • Size
  • Rotation
  • StartAngle
  • TickCount
private void CalculateSpokesPoints()
{
    spokes = new List<spoke>();

    // Calculate the angle between adjacent spokes
    angleIncrement = (360 / (double)TickCount);
    // Calculate the change in alpha between adjacent spokes
    alphaChange = (int)((255 - ALPHA_LOWER_LIMIT) / (double)TickCount);

    // Set the start angle for rendering
    renderStartAngle = StartAngle;

    // Calculate the location around which the spokes will be drawn
    double width = (this.Width < this.Height) ? this.Width : this.Height;
    centerPoint = new Point(this.Width / 2, this.Height / 2);
    // Calculate the inner and outer radii of the control. 
    // The radii should not be less than the
    // Minimum values
    innerRadius = (int)(width * INNER_RADIUS_FACTOR);
    if (innerRadius < MINIMUM_INNER_RADIUS)
        innerRadius = MINIMUM_INNER_RADIUS;
    outerRadius = (int)(width * OUTER_RADIUS_FACTOR);
    if (outerRadius < MINIMUM_OUTER_RADIUS)
        outerRadius = MINIMUM_OUTER_RADIUS;

    double angle = 0;

    for (int i = 0; i < TickCount; i++)
    {
        Point pt1 = new Point(innerRadius * 
			(float)Math.Cos(ConvertDegreesToRadians(angle)), 
          innerRadius * (float)Math.Sin(ConvertDegreesToRadians(angle)));
        Point pt2 = new Point(outerRadius * 
			(float)Math.Cos(ConvertDegreesToRadians(angle)), 
          outerRadius * (float)Math.Sin(ConvertDegreesToRadians(angle)));

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

        // If it is not it Indeterminate state, 
        // ensure that the spokes are drawn in clockwise manner
        if (!IsIndeterminate)
        {
            angle += angleIncrement;
        }
        else
        {
            if (Rotation == Direction.CLOCKWISE)
            {
                angle -= angleIncrement;
            }
            else if (Rotation == Direction.ANTICLOCKWISE)
            {
                angle += angleIncrement;
            }
        }
    }
}

The OnRender method is overridden to handle the rendering of the control. Here to the RenderTransform of the control, a TranslateTransform and a RotateTransform is applied so that the SprocketControl is rendered with respect to its centre. 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 (alphaDecrement) until it reaches the lower limit of the Alpha Value (ALPHA_LOWER_LIMIT).

protected override void OnRender(DrawingContext dc)
{
    if (spokes == null)
        return;

    TranslateTransform translate = new TranslateTransform(centerPoint.X, centerPoint.Y);
    dc.PushTransform(translate);
    RotateTransform rotate = new RotateTransform(renderStartAngle);
    dc.PushTransform(rotate);

    byte alpha = (byte)255;

    // Get the number of spokes that can be drawn with zero transparency
    int progressSpokes = (int)Math.Floor((Progress * TickCount) / 100.0);

    // Render the spokes
    for (int i = 0; i < TickCount; i++)
    {
        if (!IsIndeterminate)
        {
            if (progressSpokes > 0)
                alpha = (byte)(i <= progressSpokes ? 255 : DEFAULT_PROGRESS_ALPHA);
            else
                alpha = (byte)DEFAULT_PROGRESS_ALPHA;
        }

        Pen p = new Pen(new SolidColorBrush(Color.FromArgb(alpha,
            this.TickColor.R, this.TickColor.G, this.TickColor.B)), TickWidth);
        p.StartLineCap = p.EndLineCap = TickStyle;
        dc.DrawLine(p, spokes[i].StartPoint, spokes[i].EndPoint);

        if (IsIndeterminate)
        {
            alpha -= (byte)alphaChange;
            if (alpha < ALPHA_LOWER_LIMIT)
                alpha = (byte)ALPHA_LOWER_LIMIT;
        }
    }

    // Perform a reverse Rotation and Translation to obtain the original Transformation
    dc.Pop();
    dc.Pop();
}

Using the SprocketControl

As an Indeterminate Progress Control

By default, the SprocketControl is an indeterminate progress control. You can customize the control by setting appropriate values to the control's dependency properties - Interval, StartAngle, TickStyle, TickCount, TickWidth. Setting the TickCount to a large number will display the control like a solid ring.

As a Normal Progress Control

By setting the IsIndeterminate property to false, the SprocketControl will behave like a normal progress control. In this mode, the StartAngle is set to 270 degrees. You must set the Progress property to indicate the progress of the process. You can further customize the control by setting appropriate values to the control's dependency properties - Interval, TickStyle, TickCount, TickWidth.

EndPoint

If you look at the WPFSpark project in CodePlex, I have added the screenshot of the next control - ToggleSwitch which I am developing. I will be explaining that control in detail in my next article.

History

  • December 21, 2011: WPFSpark v1.0 released
  • May 28, 2011: WPFSpark v0.5 released

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here