The purpose of this article and code is to provide you with a speedometer-like gauge control. The gauge is encapsulated in a VB5 class file. It could have been made into an OCX, but this gives you some room to customize it. The class file, named "SpeedometerGauge.cls," is included with this article. Also included is a sample program called "Sample.exe" and the project used to create it.
First, I will explain a little bit about how the gauge works so that you can change how it operates if you want. The object holds a handle to the PictureBox it will use to paint the gauge. A public subroutine called "Setup" was added to the object to allow the initialization of some of the object's variables and to give the object a reference to the PicureBox. You have to pass the destination PictureBox, the minimum value for the gauge, and the maximum value for the gauge to the "Setup" subroutine. In the "Setup" routine some variables, which are used to draw the control, are calculated; you'll see how they are calculated and used later in this article. The object also has PropertyLet and PropertyGet for the gauge's "Value." This simply allows you to range-check any values that are sent to the gauge. The object also has a "Refresh" subroutine that is Private to the object. The object handles refreshing itself any time a new value is given to it. The object sets the destination PictureBox's AutoRedraw property to true so that it always retains its image (there's no need to use "Refresh" if another window covers the control).
Producing the Gauge's Image
Now, you move on to producing the gauge's image. As I said in the last paragraph, the object holds a reference to the PictureBox that the gauge uses to draw itself on. As we all know, a PictureBox is square and a speedometer is shaped like the top half of a circle. Well, to draw a speedometer in the middle of a PictureBox and not cover up controls that are located in the top two corners of the PictureBox, you have to create a region that looks like the top half of a circle and send it to the PictureBox to tell the PictureBox where it can and can't paint. This explanation is getting hairy, so here is a illustration to help you out.
Before you make this region, you need to force your PictureBox to be twice as wide as it is high. If you think about the speedometer needle going from pointing directly left and spinning to point directly right, you'll see that this limitation is necessary.
Next, you use the CreateRoundRectRegion API call to create your painting region. You create a circular region whose diameter is the width of the PictureBox (after adjustment, of course). This ciruclar region is centered horizontally in the PictureBox. Vertically, the circular region is centered at the bottom of the PictureBox (the illustration above shows this a little better). You use the "hWnd" property of the PictureBox and the SetWindowRgn API call to tell the PictureBox to only paint the speedometer and leave the top two corners transparent.
Now that you understand the shape of the image, I should talk about how the gauge paints the image. You want to be able to put a background image into your speedometer gauge so that you have a nice speedometer look (see the sample program). To get your speedometer needle to show on top of this background image, you have to paint the background image and then paint the needle on top of it every time you refresh the gauge. To do this, the "Setup" function creates a memory device context in which to store your speedometer's background image.
The PictureBox should already contain your image, so you just take the image out of the PictureBox and store it in your device context. (It is more complicated than that, but to keep this document from putting you to sleep, you can just look at the code for doing this. It should be commented well enough... I hope). Whenever the gauge needs to be repainted, you just use the BitBlt API call to draw your background image onto your speedometer and then draw your needle on top of the image.
The Math Behind the Gauge
Uh oh, here comes the math. It's pretty simple, though. In the half-circle that your gauge occupies, you have a total of 180 degrees. If you wanted your gauge's value to only range from 0 to 180, you'd be fine, but you don't want to limit yourselves like that. To get around this, you need to scale your gauge's value to fall within the range of values that you can plot (0 to 180). Say that you want your gauge to range from 0 to 360, and you need to plot the value 300. If you take your proposed range (360) and divide it by the range of values you can plot (180), you get the value .5 (or 1/2). If you multiply the value you want to plot (300) by your scale value (.5), you get the value 150. This value is within your target range (0 to 180). You can apply this scale factor to any value in your proposed range (0 to 360) and produce a value that will plot nicely in your gauge's range (0 to 180).
Next, you need to calculate the endpoints of the line you use for the speedometer's needle. The base of the needle stays horizontally centered at the bottom of the PictureBox no matter what value the gauge changes to. The trick is to calculate where the other end of the needle will be for any given value the gauge could be set at. You have to use some trigonometry for these calculations, but you are just using Sine and Cosine, so it won't be too awfully hard. If you use the base of your needle as the center (0,0) of a cartesian plane, and you use your needle length, which is calculated in the "Setup" routine, the following equations will produce the endpoints of the needle for any given value in the range of 0 to 180. I will not go into the details of these equations for the sake of boredom, but if you want more info, a trigonometry book would be a big help.
X_Offset = CInt(NEEDLE_LENGTH * -Cos(-Value * PI / 180)) Y_Offset = CInt(NEEDLE_LENGTH * Sin(-Value * PI / 180))
The values are stored in X_Offset and Y_Offset because you used your needle's base as the center of our ycartesian plane. You have to add your offsets to the needle's base coordinates to produce the correct endpoints.
I am sorry this article could not explain some of the gaps that were left out, but it would have quickly turned into a whole book instead of a small article. One of the main gaps that I left out is the trigonometry. I could not explain trig in a small article, but as I mentioned before, a nice trig book would help greatly. The other big gap was the intricate details of Windows device contexts. This would also have quickly became a book in itself. The device context functions contained in the speedometer object should be commented well enough that, if you couple them with a little knowledge of device contexts, you should be able to understand them.
Tips for Using the Speedometer Gauge Class
- The object adjusts the width of the PictureBox to be twice its height, so if you make a background image for your speedometer, make sure it is twice as wide as it is high.
- The gauge looks much better if you set the PictureBox's BorderStyle property to "None."
- The "Forecolor" and "DrawWidth" properties of the PictureBox determine the color and width of the speedometer's needle, respectively.