Seti@Home 3D progress bar






4.07/5 (14 votes)
Apr 3, 2003
5 min read

123973

1968
A C# implementation of the Seti@Home 3D progress bar using GDI+
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
GraphicsPath
s 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:
and to draw the background grid I used:
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:
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 GraphicsPath
s, 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.