- Drawing the gauge
- Designer integration
- Using the code
- Points of Interest
This project provides a simple gauge-type UserControl that can be incorporated into your projects. It rotates through 360 degrees and allows the programmer to set a "red zone". I will also give some basic information on Forms Designer integration, which adds hugely to the usefulness of your custom controls.
You should have basic experience using .NET tools and Windows Forms; however, you shouldn't have too much, since this is a somewhat trivial control. Still, it can add a nice look to any form that needs a graphical display of values with a max value.
I am going to refer to the red pointer doohickey as the "vertical arrow", and the yellow lines pointing to the numbers around the edge of the gauge as "rays". The number will be numbers. If you forget what I mean by numbers while reading the explanation, just remember to refer back to this paragraph.
Drawing the gauge
I did all my drawing inside the
event, which is called every time the control is invalidated and refreshed, including when the control is loaded for the first time.
looks like this:
protected override void OnPaint(PaintEventArgs e)
protected means that the method can only be accessed from within the class, or from classes derived from it.
override means that the method overrides a member of the base class (in this case
System.Windows.Forms.UserControl) that has the same protection level, return type, and parameters. The
PaintEventArgs parameter specifies the graphics object and region to paint.
OnPaint method, I established a center point for the gauge, then made a
gp to which I added an ellipse, then made
gp the drawing region for the control:
using(GraphicsPath gp = new GraphicsPath())
this.Region = new Region(gp);
Note that the length and height of the thing are double the distance to the center on the x and y axes. This gets us a circle rather than an ellipse. Why there's no
DrawCircle method I have no idea, except that it only takes a few extra lines of code to make sure that your ellipse is circular.
After this, we define a
Brush to use, check that we have at least one number and ray for the vertical arrow to point to, then start drawing the numbered rays. We also need a
Matrix object so that we can rotate the rays as we turn about the center:
using(Brush brush = new SolidBrush(Color.Yellow))
if (numNumbers == 0)
numNumbers = 1;
for (int i = 0; i < 360; i += 360 / numNumbers)
Matrix matrix = new Matrix();
matrix.RotateAt(i + this.angle, centerPt);
g.Transform = matrix;
if(i >= redZone)
g.DrawLine(Pens.Red,center, center,center, center * 3/10);
g.DrawLine(Pens.Yellow, center, center, center, center * 3/10);
g.DrawString(((int)(i * divisor)).ToString(),
this.Font,brush, center - 6, center * 5 / 100,
for loop gives us the angle for each ray, based on 360 degrees in a circle and the center I mentioned earlier. You'll note that if the angle
i is greater than
redZone we draw the rays and numbers in red, not yellow. A
Matrix, by the way, is a cool little mathematical toy that lets you work marvelous changes to a
Graphics object, for example
g, derived here from the
PaintEventArgs argument to
OnPaint. We add the current angle to the angle
i to get the total rotation we have to undergo before drawing this particular ray/number combination. If you don't know what a
Matrix is but still want to draw things upside down or make them jump around or turn about a point, you're going to have to do some learning. After we draw each ray, in its proper color, we draw the number at its end.
divisor here is simply a
float value that I got by dividing the maximum numeric value that I wanted to display by 360. When I multiply that by the current angle
i, I get the numeric value to give to
DrawString method, which not surprisingly wants a
string. Everything in C# seems to have a
ToString() method, however silly the results may appear to us humans. The values
center - 6 and
center * 5 / 100 tell
DrawString to start drawing six pixels to the left of center on the x axis, and 1/20th of the way down from the top. Then via the magic of
Matrixes, the number appears rotated about the center at the correct location.
After I drew the gauge background, I took the much easier step of drawing the unmoving arrow:
using(GraphicsPath gp2 = new GraphicsPath())
using( Pen pen = new Pen(arrowColor, 12))
Matrix matrix = new Matrix();
g.Transform = matrix;
pen.EndCap = LineCap.ArrowAnchor;
g.DrawLine(pen, center, center, center, center / 8);
g.DrawLine(pen, center, center, (center * 9)/10, center);
g.DrawLine(pen, center, center, (center * 11)/10, center);
g.DrawLine(pen, center, center, center, (center * 11)/10);
Here we first make a quick rotation to get us back to vertical--otherwise the arrow points along the last ray drawn, one over from vertical, and rotates embarrassingly with the gauge--and start drawing fat lines with pointy, barbed linecaps. The
using directive, as the MSDN says, "Defines a scope at the end of which an object will be disposed". Please don't sue me, Bill Gates. What Bill means here is that the
Pen pen and
GraphicsPathgp2 will be cleaned up as soon as we get beyond their respective curly braces, which keeps junk from accumulating on the heap or the stack, wherever our friend .NET creates
You will also notice that this thing has some odd-looking accessors and mutators:
DescriptionAttribute("Initial angle of arm")
public float Angle
angle = (360f - value);
if (angle < 0f)
angle = 0f;
if(this.angle < 360f - this.RedZone)
This allows you to access the properties of the gauge just as you would the properties of any other control:
private CompassCard.CompassCard card = new CompassCard.CompassCard();
card.Angle = 345;
It also lets the property window in Visual Studio .NET see the attributes you want to make available to the designer, so that a client author can give a gauge the appearance and behavior s/he desires.
You'll notice that I trigger my
RedZoneHit() in the accessor/mutator (well actually I call a method that triggers the event), rather than when I redraw the gauge in
OnPaint(). That is because, as an astute reader of my first version pointed out, once the arrow is in the red, anything that triggers
OnPaint() also triggers the
RedZoneEvent, firing it repeatedly and pointlessly. Of course, my new way still means that you trigger the event again when you try to move the arrow out, but I figured that it's good to be notified every time the gauge moves while it is in the red. Once you get it out, the event stops firing and all is copasetic.
The accessor/mutator is also preceded by the odd-looking statement.
DescriptionAttribute("Initial angle of gauge")
This tells the designer that this property goes in the "Appearance" category of the control's property sheet, and that its description is "Initial angle of gauge", whatever that means. Actually it means the initial angle the gauge is rotated to, say if you wanted to start off the arrow pointing to 34 degrees rather than 0.
Another .NET peculiarity can be found at the beginning of the class definition for
public delegate void HitRedZone(object sender, EventArgs e);
public class CompassCard : System.Windows.Forms.UserControl
public event HitRedZone RedZoneEvent;
Thus is Visual Studio .NET informed that the default event, the one you are called upon to create when you double click on the control on your form, is the
RedZoneEvent. Makes things a little easier for client authors.
Using the code
If you handle the
RedZoneEvent event, remember that it will fire whenever you rotate the gauge, even when you move it back out of the red. Other than that it should be easy enough to use.
CompassCard adds the following properties to its Properties dialogue:
Angle: The starting rotation of the gauge.
Range: The maximum value represented by the gauge.
RedZone: The start of the red zone; entry triggers an event.
ArrowColor: Color of the vertical arrow.
NumNumbers: The number of values displayed on the gauge. Must be greater than one.
All appear in the Appearance section of the Properties window.
CompassCard also adds an event you can handle,
RedZoneEvent which takes the parameters
object sender, EventArgs e and lets you know when yer in the red.
Points of Interest
This was a fairly entertaining little piece of programming. Since I'm not exactly an experienced .NET developer, just getting the needle to draw on top of the gauge, without any of the radial vanes showing through, was kind of interesting.