A couple of years ago, while I was writing a java application called MyFTP which was part of a test for the university, I liked to place on the form (or should I say JFrame) I was building a progress bar. It has been a matter of 5 seconds to discover that calling setStringPainted(true) on a java progress bar gives you the ability to write the percentage text over the control.
When I made my first try to do the same on dotnet ProgressBar, I sadly discovered that you can't paint the percentage value over the control, as easily as you can do it in java. I made many tries to do it, but with no luck, until I got some knowledge about custom controls.
This article hence is about enhancing a ProgressBar in order to be able to paint the percentage text over the control and also render the control using a custom color or a custom gradient made by two colors.
To achieve the desired result we need to:
- Inherit from System.Windows.Forms.ProgressBar
- Override the OnPaint method and setup the control to use it
- Override some properties to apply our changes
- Shade some properties that we don't want to appear in design time
The explanation will be straightforward and you don't need to be confident with concepts like Control Designers or so, since we're still talking quite easy here.
Ok, first of all, fire up your VS05 environment, create a C# class library project and a windows application project that will test our custom control. It's better if you place them both into just one solution, because doing so, when you build your custom controls, they appear on the toolbox along with the standard controls and components, making their usage very easy.
Ok, let's see the code a bunch of lines at a time. Here it is the declaration:
using System;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using System.Windows.Forms;
using System.ComponentModel;
using System.Drawing.Drawing2D;
namespace YourCompany.Controls
public class ZProgressBar : System.Windows.Forms.ProgressBar {
/// <summary>
/// Constructor
/// </summary>
public ZProgressBar() {
// setting the style in order to get
// double buffering, userpaint and redraw on resizinig
this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw, true);
}
...
As you can see we inherit from ProgressBar. The constructor does just one thing, it sets some flags to true. We want double buffering (AllPaintingInWmPaint & OptimizedDoubleBuffer), we want the control to be drawn using the OnPaint method (UserPaint) and finally we also provide the ability for the control to call an invalidation when resized (ResizeRedraw).
Let's check the properties now, and then the methods.
// gradient top color if in gradient mode, top color otherwise
private Color gradientTop=Color.White;
public Color GradientTop {
get { return gradientTop; }
set {
gradientTop = value;
this.gradientColor= ZPBGradientColors.Custom;
this.Invalidate();
}
}
// gradient bottom color if in gradient mode
private Color gradientBottom=Color.Black;
public Color GradientBottom {
get { return gradientBottom; }
set {
gradientBottom = value;
this.gradientColor= ZPBGradientColors.Custom;
this.Invalidate();
}
}
private ZPBGradientColors gradientColor= ZPBGradientColors.Custom;
[Description("Sets a predefinite gradient color. If set to custom then GradientTop and GradientBottom can be set by the user")]
public ZPBGradientColors GradientColor {
get { return gradientColor; }
set {
gradientColor = value;
SetGradientColors(value);
this.Invalidate();
}
}
This section is about gradients. The ZProgressBar has basically 2 colors. If the GradientColor property (which is an enum) is set to Simple the color that will be used to paint the foreground is gradientTop. If it is set to Gradient, the gradientTop and gradientBottom colors are used to paint the foreground, with a gradient made by theirselves. Finally, if the GradientColor property is set to Blocks, our ZProgressBar will be rendered like any other standard progressbar, with those green blocks you surely have already seen somewhere.
Note the [Description] attributes that provide some text for the desing-time environment. Also note that, when you set one of the gradient colors, the GradientColor property is set to Custom and, on the opposite, when you set the GradientColor to something different from Custom, the 2 gradient colors are set accordingly.
Here we go with the enumerations I chose, so you can understand how to handle those properties:
// gradient colors enumeration
public enum ZPBGradientColors {
Custom, // default
Red,
Blue,
Green,
Yellow,
Silver
}
// ZProgressBar styles
public enum ZProgressBarStyles {
Simple, // default, one color
Gradient, // 2 colors
Blocks // green default blocks
}
// enum for the text if you decide to use it
public enum ZPBTextAlignment {
Left,
Center, // default
Right
}
Let's see the other properties:
// style of the progressbar
private ZProgressBarStyles style=ZProgressBarStyles.Simple;
[Description("The ZProgressBar style. Continuous means single color, gradient two vertically combined colors and blocks means the usual green block default ProgressBar's rendering")]
public new ZProgressBarStyles Style {
get { return this.style; }
set {
this.style=value;
if (value == ZProgressBarStyles.Blocks || value == ZProgressBarStyles.Simple) {
this.gradientColor= ZPBGradientColors.Custom;
gradientBottom=Color.Empty;
}
this.Invalidate();
}
}
// whether or not the progressbar will display the value
private bool showText=false;
[Description("If true the progressbar will draw its value over itself")]
public bool ShowText {
get { return this.showText; }
set {
this.showText=value;
this.Invalidate();
}
}
[Browsable(true)]
[Description("The font used to render the value")]
public new Font Font {
get { return base.Font; }
set { base.Font = value; }
}
private Color fontColor=Color.Black;
[Description("The color used to render the value text")]
public Color FontColor {
get { return this.fontColor; }
set {
this.fontColor=value;
this.Invalidate();
}
}
// new Value to force an invalidation
public new int Value {
get { return base.Value; }
set {
base.Value=value;
this.Invalidate();
}
}
private ZPBTextAlignment textAlignment=ZPBTextAlignment.Center;
[Description("Sets where to draw the value is ShowText is true")]
public ZPBTextAlignment TextAlignment {
get { return textAlignment; }
set {
textAlignment = value;
this.Invalidate();
}
}
When you set the Style property in desing-time, the code sets the gradient color accordingly. You don't need any of them if you're using blocks, and you'll need only the top one if you are going to use the single mode style. The ShowText property perfectly describes itself, as the Font one, but I want you to notice that [Browsable(true)] before the Font declaration. This enables the design-time environment to provide access to the font property via the property window, which is very handy and quick. Notice also that in most cases I call the Invalidate method to force a repaint of the control, since I want it to be consistent with the settings I make via the property window.
I also wanted to "shade" a couple of properties that I'm never going to use:
// shaded values
[Browsable(false)]
public override RightToLeft RightToLeft {
get { return base.RightToLeft; }
set { base.RightToLeft = value; }
}
[Browsable(false)]
public new int MarqueeAnimationSpeed {
get { return base.MarqueeAnimationSpeed; }
set { base.MarqueeAnimationSpeed=value; }
}
A simple shading is made applying the [Browsable(false)] attribute before the overriding of the property or its declaration.
Let's check the methods (there are only 2 methods apart from the contructor):
/// <summary>
/// Helper method
/// </summary>
/// <param name="gradient">The gradient to be used</param>
private void SetGradientColors(ZPBGradientColors gradient) {
switch (gradient) {
case ZPBGradientColors.Blue:
this.gradientTop=Color.AliceBlue;
this.gradientBottom=Color.DarkBlue;
break;
case ZPBGradientColors.Green:
this.gradientTop=Color.LightGreen;
this.gradientBottom=Color.DarkGreen;
break;
case ZPBGradientColors.Red:
this.gradientTop=Color.MistyRose;
this.gradientBottom=Color.DarkRed;
break;
case ZPBGradientColors.Silver:
this.gradientTop=Color.WhiteSmoke;
this.gradientBottom=Color.Black;
break;
case ZPBGradientColors.Yellow:
this.gradientTop=Color.LightYellow;
this.gradientBottom=Color.DarkOrange;
break;
}
}
This is a helper method. When you set the gradient to one of the 5 different gradients I provided (other than the Custom one), the 2 gradient colors are automatically set by this method. A quick way to set up a gradient.
And now, the core of the class' code; the OnPaint method:
protected override void OnPaint(PaintEventArgs e) {
// call to the base class OnPaint method
base.OnPaint(e);
// custom painting here
Graphics g=e.Graphics;
g.SmoothingMode= SmoothingMode.HighSpeed;
int valueLen=this.Value-this.Minimum;
valueLen*=(this.Size.Width-2*margin);
valueLen/=(this.Maximum-this.Minimum);
// foreground rectangle
Rectangle rect=new Rectangle(this.ClientRectangle.X+margin, this.ClientRectangle.Y+margin, valueLen, this.ClientRectangle.Height-2*margin);
// background
// if visualstyles are applied then use the ProgressBarRenderer
// otherwise leave the background set by the BackColor property
if (Application.RenderWithVisualStyles) {
ProgressBarRenderer.DrawHorizontalBar(g, this.ClientRectangle);
}
// foreground
switch (this.Style) {
case ZProgressBarStyles.Simple:
using (SolidBrush foreBrush=new SolidBrush(this.gradientTop)) {
g.FillRectangle(foreBrush, rect);
}
break;
case ZProgressBarStyles.Gradient:
using (LinearGradientBrush foreBrush=new LinearGradientBrush(this.ClientRectangle, gradientTop, gradientBottom, 90f)) {
foreBrush.SetBlendTriangularShape(this.triangleShape);
g.FillRectangle(foreBrush, rect);
}
break;
case ZProgressBarStyles.Blocks:
if (Application.RenderWithVisualStyles) {
ProgressBarRenderer.DrawHorizontalChunks(g, rect);
} else {
using (SolidBrush foreBrush=new SolidBrush(this.gradientTop)) {
g.FillRectangle(foreBrush, rect);
}
}
break;
}
if (this.showText) {
using (Brush fontBrush=new SolidBrush(this.fontColor)) {
using (StringFormat sf=new StringFormat()) {
switch (textAlignment) {
case ZPBTextAlignment.Center:
sf.Alignment=StringAlignment.Center;
break;
case ZPBTextAlignment.Left:
sf.Alignment=StringAlignment.Near;
break;
case ZPBTextAlignment.Right:
sf.Alignment=StringAlignment.Far;
break;
}
sf.LineAlignment=StringAlignment.Center;
g.DrawString(string.Format("{0}%", this.Value), this.Font, fontBrush, this.ClientRectangle, sf);
}
}
}
}
First we call the base.OnPaint method, letting the control to do some eventual stuff we don't want to handle. After this we have to paint the background, the foreground, and eventually some text. At first I set the Graphics.SmoothingMode to HighSpeed, since I need the control to repaint fast. It's not a Picasso painting so I don't care about AntiAliasing stuff.
To draw the backgroung I just used the ProgressBarRenderer.DrawHorizontalBar method, which draws a rounded edges rectangle with white filling. If we cannot render the ZProgressBar using visual styles (like with WinXP) the BackColor will be the background of the control. (And similarly for the foreground, the gradientTop color will be used to render a single color)
Then we have to paint the foreground, and we do it accordingly to our Style setting. A switch clause is more than enough here. Notice that I used using(...) {...} in order to release some resources as soon as possible.
Also notice that I used the LinearGradientBrush.SetBlendTriangularShape method to adjust the 2 colors relative position.
The last thing about the foreground is that, if the Style property is set to Blocks, we call the ProgressBarRenderer.DrawHorizontalChunks method that perfectly does the job as on a regular ProgressBar.
The valueLen int property is the percentage value, calculated like this:
valueLen = [(val-min)/(max-min)]*(control's length)
If the ShowText property is set to true we also write a percentage text.
We set the brush color to the fontColor property I created and then we set the StringFormat.Alignment accordingly to the textAlignment property of our control: left, center or right, which translates in Near, Center and Far of the StringAlignment enumeration.
That's it. Compile the code and drag a ZProgressBar onto your brand new form. Set the properties as you like and fire it up. It's ready to go. It should work good in almost every situation, but bear in mind that I wrote this class just to be able to write this article and therefore it hasn't been tested at 100%.
I have made a small form with some ZProgressBar controls onto it. It is bundled in this zip file, along with the source code files.
Enjoy it and live in peace.
sfabriz
|