65.9K
CodeProject is changing. Read more.
Home

Seti@Home 3D progress bar

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.07/5 (14 votes)

Apr 3, 2003

5 min read

viewsIcon

123973

downloadIcon

1968

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.

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.

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.

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.

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.

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.

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.

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:

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:

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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.

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:

using SetiBar;

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

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

private Bar bar1;

And add the following to your InitializeComponent().

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.