A rotating gauge






4.21/5 (17 votes)
Mar 31, 2005
6 min read

106246

5710
An article on a custom control in the form of a rotating gauge.
Contents
- Contents
- Introduction
- Background
- Drawing the gauge
- Designer integration
- Using the code
- Points of Interest
Introduction
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.
Background
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 theOnPaint
event, which is called every time the control is invalidated and refreshed, including when the control is loaded for the first time. OnPaint
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.
Inside the OnPaint
method, I established a center point for the gauge, then made a GraphicsPath
called gp
to which I added an ellipse, then made gp
the drawing region for the control:
using(GraphicsPath gp = new GraphicsPath())
{
gp.AddEllipse(0,0,2*center,2*center);
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);
else
g.DrawLine(Pens.Yellow, center, center, center, center * 3/10);
g.DrawString(((int)(i * divisor)).ToString(),
this.Font,brush, center - 6, center * 5 / 100,
StringFormat.GenericTypographic);
}
}
The 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 g
's 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 Matrix
es, 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();
matrix.RotateAt(0, centerPt);
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 new
objects.
Designer integration
You will also notice that this thing has some odd-looking accessors and mutators:
[
CategoryAttribute("Appearance"),
DescriptionAttribute("Initial angle of arm")
]
public float Angle
{
get{return angle;}
set
{
angle = (360f - value);
if (angle < 0f)
angle = 0f;
if(this.angle < 360f - this.RedZone)
RedZoneHit();
this.Refresh();
}
}
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.
[
CategoryAttribute("Appearance"),
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 CompassCard
:
public delegate void HitRedZone(object sender, EventArgs e);
[DefaultEventAttribute("RedZoneEvent")]
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.