Introduction
This is my first article at CodeProject and my first (English-language) article at all, so forgive my syntax mistakes and poor language phrases (English is not my native language).
While coding an application, I faced a situation where I wanted an indicator about my application working progress so I dropped the .NET ProgressBar
and completed my work. After test running my application, I noted that this default control did not fit in my case. I think it doesn't fit in many other cases and is not sufficient when dealing with a serious well designed UI application. So I started searching and looking for some more powerful Progress controls that could help me. I found a couple dozen of them hanging around all over the net but no one matched my percentage indicator goal, so I decided to implement my own control and that was the beginning of this article.
Using the code
Let's start this mid-sized article step by step (I think it's better for beginners to download the source code to be in touch with any explanation that might be confusing).
Preparing the scene stage
First of all, when you code some intensive drawing functionality, a small reflection may appear that prevents the normalized look of the drawing operation, or at least reflects a badly optimized drawing algorithm. Well.. that�s a situation where a little low-level optimization may fit, but this low-level optimization requires more experience and more hard work to implement. Fortunately, .NET provides the perfect solution with the price of one more line of code, by calling the SetStyle
(System.Windows.Froms.Control.SetStyle
) function with the appropriate parameters. All the (behind the scene) work is done for you here.
The SetStyle
function expects a value of ControlStyles
enumerator (System.Windows.Forms.ControlStyles
) that specifies what to do with the control. This enumeration has the FlagsAttribute
which indicates that the members of the enumeration can be combined using a bitwise OR operator (|
). The members of ControlStyles
that concern us here are those members that reduce the flicker in drawing our control:
ControlStyles.UserPaint
: causes the control to draw itself rather than the OS.
ControlStyles.AllPaintingInWmPaint
: causes the control to ignore the WM_ERASEBKGND
message.
ControlStyles.DoubleBuffer
: causes the control to use a buffer with the drawing operation.
At this point, you just need to understand that combining all these parameters will reduce the flicker and satisfy our goal. For a better understanding of these members, you can surly refer the MSDN.
Reduce the flicker:
this.SetStyle(ControlStyles.UserPaint |
ControlStyles.AllPaintingInWmPaint | ControlStyles.DoubleBuffer,true);
Although our control already uses the SetStyle
function, there is another use of it which may fit in this situation (and if not, I decided to mention it for the sake of completeness). The feature I'm talking about is the background transparency of the control, i.e. the control shows its parent control background color instead of its own (of course, when its background is set to Transparent
!).
To enable the transparency of a control you could use the SetStyle
function and turn on the ControlStyles.SupportsTransparentBackColor
flag. This option allows your control to accept a color with an alpha component of less than 255 for its BackColor
property (a color with 255 alpha component is a completely opaque color and a color with 0 alpha component is a fully transparent color).
Allow the control transparent background color (optional and may be skipped):
this.SetStyle(ControlStyles.SupportsTransparentBackColor, true);
Coding actual work
You might now be thinking "What the hell is this guy talking about? and where is the Progress related stuff? is there any real code explanation?". Well.. yes, there is such an explanation and a large amount of it, but keep in mind that the actual idea behind coding a progressbar control and specially calculating its percentage value is a matter of math guys stuff, and I'm not going to give you a lecture about mathematics, addition, multiplication ..etc, nor explain the (area of a rectangle, size of circle, and length of a vector) rules.
What we really need here is a logical width to draw the work progress to it. You might think that we could use the value reached (the current drawing edge) till now and draw according to it, keeping the width of the control in mind! That's a perfect solution, but what if the user of the control decides that the maximum value must be greater than the control width or less than it?? The perfect solution won't be perfect anymore. It will work fine in cases of equality of both the control width and the maximum value. To overcome this problem, think of this calculation:
- ( (Control Width) * (Current drawing value) / ( Maximum value - Minimum value) )
drawingWidth = (int)(this.Width * _value) / (maxValue - minValue);
This will give us the exact point at which the drawing should reach (as a float
value that would be converted into int
).
What about the percentage of the current point? Another calculation would be enough:
- ( (Current drawing value / Maximum value) * 100 )
percentageValue = (_value / maxValue) * 100;
After calculating these values we may continue drawing our control as a filled rectangle from (0,0) to (logical width, control height), and that's all it takes!
e.Graphics.FillRectangle(_Drawer, 0, 0, drawingWidth, this.Height);
To this point in the code, we have a regular but colored progress control, and all we need is to add the percentage value (we just calculated) to it, and that's a simple DrawString
function call with the correct values passed to it. These values depend on the style of percentage we want to draw. There are three styles for percentage drawing (None
, Center
, Movable
), don't be confused, handling them is too simple. Now do the following:
- For the
None
style don't draw any percentage at all (too hard to code?!!).
- For the
Center
style just draw the percentage value centered vertically and horizontally (that's all shown in the code).
- For the last
Movable
style draw the percentage value centered vertically and horizontally located at our logical width value (also coded).
What else?
I have used other techniques that I think are worth some explanation.
LinearGradientBrush
located in System.Drawing.Drawing2D
is a brush that has more than the base color all mixed together in a gradient manner.
ColorBlend
also located in System.Drawing.Drawing2D
is used for mixing more than two colors in a gradient brush.
ControlPaint
located in System.Windows.Forms
is used to extract dark and light values of a specified color.
Note that what I just said is not by any means an explanation of these classes or their functionality, not at all. All I have said is for use in this article only and doesn't represent an actual reference on using them. Every one of these classes deserve a dedicated article for its own, but you can always refer to the MSDN for further investigation if you are interested.
Final Version
Whether to write the previous details in the Main
or in a different function is not a good question to ask here, but for those who haven't caught it yet (if any), you should put it in the Paint
event handler, and the actual code should look something like this:
private void ProgressEx_Paint(object sender, System.Windows.Forms.PaintEventArgs e)
{
if(_value != 0 & _value <= maxValue)
{
drawingWidth=(int)(this.Width * _value) / (maxValue - minValue);
percentageValue = (_value / maxValue) * 100;
_Drawer.InterpolationColors = gradientBlender;
e.Graphics.FillRectangle(_Drawer, 0, 0, drawingWidth, this.Height);
if(percentageDrawingMode != PercentageDrawingMode.None)
{
string text=((int)percentageValue).ToString() + "%";
SizeF textSize=e.Graphics.MeasureString(text,writingFont);
if(percentageDrawingMode == PercentageDrawingMode.Movable)
{
e.Graphics.DrawString(text,writingFont,
writingBrush,drawingWidth,
(e.ClipRectangle.Height / 2 - textSize.Height / 2));
}
else if(percentageDrawingMode == PercentageDrawingMode.Center)
{
e.Graphics.DrawString(text, writingFont, writingBrush,
new PointF((e.ClipRectangle.Width / 2 - textSize.Width / 2),
(e.ClipRectangle.Height / 2 - textSize.Height / 2)));
}
}
}
}
Mission accomplished
As we finish our custom user control, I have a few words to say:
This control doesn't inherit from the ProgressBar
class (System.Window.Forms.ProressBar
). Instead it's inherited from the Control
class (System.Windows.Forms.Control
). That's why there is no override for Paint
or Maximum
(or any other members that originally exist in the ProgressBar
control) in it. And if you feel uncomfortable with the source code, then you have to look for an entire article about creating custom controls (inheriting from System.Windows.Forms.Control
or System.ComponentModel.Component
) or some related thing (there are some good articles in CodeProject about this subject, search and read some of them).
Points of Interest
The wise men said "don't reinvent the wheel" (I don't know who these men are, nor do I know if they said that or even if they ever existed) and I totally agree with them! What I am trying to say is this:
If you want a rectangular wheel, don't bother yourself reinventing it, just inherit the existing one and override its circular shape and that should be fine. But on the other hand, doing some (from-scratch) work with a user control is worth the bother and a good practice, so give it a try. And keep in mind that starting a control inherited from System.Windows.Forms.Control
doesn't mean changing the usability of the control (how it must be used), it's just changing the functionality (how it must act or perform). I.e. even if you start on your own risk a from-scratch user control that is to look like or act like an existing control, change the way it works but don't change the way it is used.
That�s it, thanks for reading this article, and if you find it useful, please give your feedback.
Enjoy! :)