Click here to Skip to main content
15,891,184 members
Articles / Desktop Programming / Windows Forms
Article

A rotating gauge

Rate me:
Please Sign up or sign in to vote.
4.21/5 (17 votes)
10 May 20056 min read 105K   5.7K   57   9
An article on a custom control in the form of a rotating gauge.

Sample Image

Contents

  1. Contents
  2. Introduction
  3. Background
  4. Drawing the gauge
  5. Designer integration
  6. Using the code
  7. 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 the OnPaint 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:

C#
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:

C#
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:

C#
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 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:

C#
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:

C#
[
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:

C#
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.

C#
[
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:

C#
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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Praiseinspirational Pin
Southmountain22-May-22 14:30
Southmountain22-May-22 14:30 
GeneralAddition to Compass display Pin
KD0YU21-Nov-12 4:16
KD0YU21-Nov-12 4:16 
GeneralWonderful control Pin
BeauGauge00122-Mar-11 4:05
BeauGauge00122-Mar-11 4:05 
GeneralRaising events from within OnPaint Pin
John Brett13-Apr-05 3:59
John Brett13-Apr-05 3:59 
GeneralRe: Raising events from within OnPaint Pin
Bill Mercer8-May-05 12:37
Bill Mercer8-May-05 12:37 
GeneralProblems Pin
Derek Price5-Apr-05 3:28
Derek Price5-Apr-05 3:28 
GeneralMore Pin
Judah Gabriel Himango31-Mar-05 5:12
sponsorJudah Gabriel Himango31-Mar-05 5:12 
GeneralRe: More Pin
Paul Brower31-Mar-05 10:49
Paul Brower31-Mar-05 10:49 
GeneralRe: More Pin
Judah Gabriel Himango31-Mar-05 10:54
sponsorJudah Gabriel Himango31-Mar-05 10:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.