Click here to Skip to main content
15,881,600 members
Articles / Programming Languages / C#
Article

Seti@Home 3D progress bar

Rate me:
Please Sign up or sign in to vote.
4.07/5 (16 votes)
2 Apr 20035 min read 122.8K   2K   37   20
A C# implementation of the Seti@Home 3D progress bar using GDI+

Sample Image - setiscreen.jpg

Introduction

For those who have downloaded and run the Seti@Home desktop app, you may have noticed the cool progress bar control. Basically I wanted to replicate this control and make my own 3D bar progress control.

What you will learn

With a bit of luck you will learn:

  • How to create a custom painted control.
  • How to use point geometry to create a 3D shape
  • Use GraphicsPaths and GDI+ to render them to make a 3D shape.
  • How to be cool.....no only kidding, you programmers, you're already cool.

Creating a custom control

The basic class we will develop will have to be completely custom made, as the System.Windows.Forms.ProgressBar cannot be inherited.

So, first of all create a new 'Class Library' project.

So we will start off by creating a custom control. We first need to reference a few assemblies.

C#
System.Drawing.dll
System.Windows.Forms.dll

Add a new class item to your project if you selected a blank project, or if not, open the pre-generated class and delete the contents if you do not feel comfortable modifying them.

In your blank class file, add these lines to the very top.

C#
using System;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Drawing2D;

This will include the basic classes for developing your custom control.

Now add the following class declaration below.

C#
namespace SetiBar 
{
    public class Bar : UserControl 
    {
        public Bar()
            {

        }
    } 
}

This declares a new class that inherits the UserControl class. The UserControl class contains all the basic properties and methods of a control that is easy to customize to suit our needs.

Setting up the internal members of the progress control

The basic functionality of the progress bar will be to show the current location within a set range. So we will need three basic members to store the locations. Add the following to the class above the constructor.

C#
private int maxvalue;
private int currentvalue;
private int tickvalue;
  • The maxvalue stores the total size of the range.
  • the currentvalue stores the position within the range.
  • The tickvalue is the size of each 'step' the progress bar makes.

We can then make properties to allow IDE's to set values easily.

C#
public int MaxLength 
{
    set{this.maxlength = value;} 
    get{return this.maxlength;} 
} 
public int CurrentValue 
{
    set{this.currentvalue = value;}
    get{return this.currentvalue;}
} 
public int TickSize 
{ 
    set{this.ticksize = value;}
    get{return this.ticksize;} 
 }

Methods

The two custom methods we shall add are Step() and Restart().

Step() is called by the parent container and increments the currentvalue by one tickvalue.

C#
public void Step()
{
    if(this.currentvalue < this.maxlength) 
    {
        this.currentvalue = this.currentvalue + ticksize;
    }
    else 
    {
        this.currentvalue = this.maxlength; 
    }
    this.Refresh();
}

Restart() is called also by the parent container and resets the currentvalue to 0.

C#
public void Restart()
{
    this.currentvalue = 0;
    this.Refresh(); 

}

Point geometry

To draw the bar in an as simple as possible way, I decided to use points.

Points are a fantastic way of drawing anything on the screen when used in conjunction with a GraphicsPath.

To draw the bar, I decided to use these following points depicted here:

Sample screenshot

and to draw the background grid I used:

Sample screenshot

Now, to put these into code.

The OnPaint override

In order to get the control to draw anything correctly, we first need to set some control styles. These are done in the constructor as shown:

C#
public Bar()
{

    this.SetStyle(ControlStyles.ResizeRedraw,true);
    this.SetStyle(ControlStyles.DoubleBuffer,true);

    //thanks to JTJ for the following style settings help :-)

    this.SetStyle(ControlStyles.AllPaintingInWmPaint,true); 
    this.SetStyle(ControlStyles.UserPaint,true);
    ........
}

These settings allow the control to paint itself and without flicker.

Now for the OnPaint override.

Insert the following code to the class:

C#
protected override void OnPaint(System.Windows.Forms.PaintEventArgs e)
{
base.OnPaint(e);


}

This basic method overrides the OnPaint method and then calls the base method.

First, we set the Graphics container g to the Graphics of the control.

C#
Graphics g = e.Graphics;

Now, as the base area to paint on, I shall make a new rectangle using the bounds of the controls ClientRectangle.

Don't ask me why I've named it blackrect, and I don't even know why I used it, but the whole thing falls apart without it. I was drinking a lot of coffee at the time.

C#
Rectangle blackrect = this.ClientRectangle;
blackrect.Height = blackrect.Height-1; 
float height = ((float)blackrect.Bottom /10) * 7;

The Height variable is the height of the front face of the bar. I've made it 70% of the height of the control, but you can modify it to be however big you want.

Now, to make it 3D, it needs to have some top perspective. I've copied the style of the Seti@Home bar, so I will need to have a perspective angle, depicted below:

Sample screenshot

So I will need to go back to the top of the class and declare two new members.

C#
private int angle = 45;
private int otherangle;

I've chosen 45 degrees because it seems the correct angle. Other angle is the third angle (the second being a right angle).

Now, the third angle needs to be calculated and both angles need to be converted to radians.

C#
this.otherangle = 90-this.angle;
double inrads = this.angle * (180/Math.PI);
double otherinrads = this.otherangle * (180/Math.PI);

We now need to find the horizontal shift of the top face to maintain perspective. This is done by a little Pythagoras.

C#
int x = Convert.ToInt32((height * Math.Sin(otherinrads))/Math.Sin(inrads));

Here is blackrect again. This time we are modifying the width to fit the end piece in. Again, I never bothered looking into why this is. It simply WORKS.

C#
blackrect.Width = Convert.ToInt32(blackrect.Width-1-x);

Now, the width of the bar face needs to be calculated based on the maximum length and the current value.

C#
float width = ((float)blackrect.Width / (float)this.maxlength) 
                                      * (float)this.currentvalue; 

Now come the points. These are the first set of points, the ones defining the bar itself.

C#
PointF pt1 = new PointF(blackrect.X,blackrect.Bottom-height);
PointF pt2 = new PointF(pt1.X+x,blackrect.Y); 
PointF pt3 = new PointF(pt2.X+width,pt2.Y); 
PointF pt4 = new PointF(pt1.X+width,pt1.Y); 
PointF pt5 = new PointF(pt3.X,pt3.Y+height); 
PointF pt6 = new PointF(pt4.X,pt4.Y+height); 
PointF pt7 = new PointF(pt1.X,pt1.Y+height);

Now the points for the background grid.

C#
PointF pt8 = new PointF(pt2.X,pt2.Y+height);
PointF pt9 = new PointF(pt2.X+blackrect.Width,pt2.Y); 
PointF pt10 = new PointF(pt2.X+blackrect.Width,pt2.Y+height); 
PointF pt11 = new PointF(pt7.X+blackrect.Width,pt7.Y);

The borders of the edges will be black, so I shall define a black pen to reuse.

C#
Pen bp = new Pen(new SolidBrush(Color.Black),1);

Now, to draw the shapes based on the points, I shall employ GraphicsPaths, defined in the System.Drawing.Drawing2D namespace.

C#
GraphicsPath background = new GraphicsPath();
background.AddPolygon(new PointF[]{pt7,pt1,pt2,pt8});
background.AddPolygon(new PointF[]{pt8,pt2,pt9,pt10});
background.AddPolygon(new PointF[]{pt7,pt8,pt10,pt11});
GraphicsPath mainrect = new GraphicsPath();
mainrect.AddPolygon(new PointF[]{pt7,pt1,pt4,pt6});
GraphicsPath top = new GraphicsPath(); 
top.AddPolygon(new PointF[]{pt1,pt2,pt3,pt4});
GraphicsPath edge = new GraphicsPath();
edge.AddPolygon(new PointF[]{pt6,pt4,pt3,pt5});

Firstly, we need to draw the background. This is done with one line of code, just the way we like it.

C#
g.DrawPath(new Pen(new SolidBrush(Color.White),1),background);

Now, each of the bar's panels are drawn independently, because they need to be of different colors.

There arose a problem for when the current value is 0. The bar may not be drawn with any length, but borders and edges were still drawn. I got past this by having a simple if statement testing if the currentvalue was 0.

C#
if(this.currentvalue!=0) 
{
    g.FillPath(new SolidBrush(Color.Red),mainrect);
    g.DrawPath(bp,mainrect);
    g.FillPath(new SolidBrush(Color.Maroon),top);
    g.DrawPath(bp,top);
    g.FillPath(new SolidBrush(Color.Crimson),edge);
    g.DrawPath(bp,edge); 
}

That's the class! See the source zip for the full listing.

Usage

Using the control is simple.

Add the custom control to the references of your Windows project.

Add the following to your form class file:

C#
using SetiBar;

On your form, either drag the control in from a toolbox.

Or you can add it manually. Add the following class member:

C#
private Bar bar1;

And add the following to your InitializeComponent().

C#
this.bar1 = new Bar();
this.bar1.CurrentValue = 50;
this.bar1.Location = new System.Drawing.Point(8, 8);
this.bar1.MaxLength = 100;
this.bar1.Name = "bar1";
this.bar1.Size = new System.Drawing.Size(296, 32);
this.bar1.TabIndex = 4;
this.bar1.TickSize = 1;

Of course you can customize this to be whatever size and location you want. Also you can customize the TickSize, MaxLength and CurrentValue to be whatever you want.

To step the progress bar, call the Step() method. In the demo app, I've used a timer to call the step every 10ms.

Notes

I've not added any error handling to this control, nor have I put any prevention of inaccurate values stated. This I leave to you. If I get round to it, I may sort this out in later versions.

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


Written By
Web Developer
United Kingdom United Kingdom
I think therefore I'm tired.

Goto my blog....now!

Comments and Discussions

 
Generalimprovements Pin
improvements 14-Oct-05 18:12
sussimprovements 14-Oct-05 18:12 
GeneralWhy couldn't you do with 8 points instead of 11 points Pin
jcarter12121220-Jul-05 6:38
jcarter12121220-Jul-05 6:38 
GeneralI like it! Pin
clementsm3-Sep-03 8:32
clementsm3-Sep-03 8:32 
GeneralRe: I like it! Pin
Jon Newman3-Sep-03 9:20
Jon Newman3-Sep-03 9:20 
GeneralRe: I like it! Pin
Nickle13-Jun-04 12:22
Nickle13-Jun-04 12:22 
GeneralSPAMMER Pin
abhadresh3-May-04 12:50
abhadresh3-May-04 12:50 
GeneralRe: SPAMMER Pin
Jon Newman3-May-04 23:42
Jon Newman3-May-04 23:42 
GeneralPainting Suggestion Pin
Heath Stewart4-Apr-03 2:20
protectorHeath Stewart4-Apr-03 2:20 
It's probably not the best place to do all your calculations - especially those which probably won't change during runtime such as max extents and the background - in OnPaint. Instead, perform this operation in OnCreateControl or OnResize or something that is fired after properties are assigned and before the control is painted. This will enhance performance quite a bit.

In a control I created (an MSDS label control using NFPA's standards), I did something similar and noticed quite a difference. If you like, I also took into account that a user of the control could allow things about it to change at runtime so that's why I put my "static" calculations in OnResize.

In your case, you could even calculate the progress bar's extents outside of paint, like in Step or, if you implement it (didn't think to look for this) OnValueChanged or something*. Then, if the control is painted multiple times before the value of the progress bar has changed, it does't have to recalculate every time.

* One thing I noticed is that you don't have any events to let developers handle when the progress value has changed or anything like that. It's a pretty good idea and doesn't require much code. Microsoft's suggestion is basically this:

C#
public event EventNameEventHander EventNamde;
protected virtual void OnEventName(EventNameEventArgs args)
{
    if (EventName != null)
        EventName(this, args);
}

public void MyMethod()
{
    // Do stuff, and either fire your event before, during, or after.
    OnEventName(new EventNameEventArgs(...));
}


Of course, there's many ways you can do this and usually using EventHandler and EventArgs (the base classes for other event "helper" object classes) suffices, but this seems to be the easiest for a developer to write and use throughout his code, and lets child classes handle the event without attaching a delegate to the event.


Reminiscent of my younger years...
10 LOAD "SCISSORS"
20 RUN

GeneralRe: Painting Suggestion Pin
Jon Newman4-Apr-03 3:06
Jon Newman4-Apr-03 3:06 
GeneralNice article! Pin
Maximilian Hänel3-Apr-03 8:32
Maximilian Hänel3-Apr-03 8:32 
GeneralRe: Nice article! Pin
Jon Newman3-Apr-03 10:51
Jon Newman3-Apr-03 10:51 
GeneralNice typo Pin
Marc Clifton3-Apr-03 8:29
mvaMarc Clifton3-Apr-03 8:29 
GeneralRe: Nice typo Pin
Jon Newman3-Apr-03 10:41
Jon Newman3-Apr-03 10:41 
GeneralRe: Nice typo Pin
Fuat Uler4-Apr-03 3:21
Fuat Uler4-Apr-03 3:21 
GeneralRe: Nice typo Pin
Jon Newman4-Apr-03 3:24
Jon Newman4-Apr-03 3:24 
GeneralWooaahhh Pin
Rein Hillmann3-Apr-03 6:49
Rein Hillmann3-Apr-03 6:49 
GeneralRe: Wooaahhh Pin
Jon Newman3-Apr-03 10:43
Jon Newman3-Apr-03 10:43 
GeneralRe: Wooaahhh Pin
Nnamdi Onyeyiri6-Apr-03 0:01
Nnamdi Onyeyiri6-Apr-03 0:01 
GeneralRe: Wooaahhh Pin
Jon Newman12-Apr-03 4:05
Jon Newman12-Apr-03 4:05 
GeneralYou did it! Pin
peterchen3-Apr-03 6:47
peterchen3-Apr-03 6:47 
GeneralRe: You did it! Pin
Jon Newman3-Apr-03 10:46
Jon Newman3-Apr-03 10:46 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

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