Public Class AnalogClock
Private Sub AnalogClock_Paint(ByVal sender As System.Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles MyBase.Paint
End Sub
End Class
The Paint() event supplies a Graphics surface via “e.Graphics” that allows us to draw on the UserControl. In the .Net Framework, the default coordinate system places the origin at the upper left corner with x values increasing as you move to the right, and y values increasing as you move down. It would be much nicer if we could instead treat the center of our analog clock as the origin, thus allowing us to easily rotate around the center of the clock. This can be achieved using the Graphics.TranslateTransforDim center As New Point(Me.ClientSize.Width / 2, Me.ClientSize.Height / 2)
Next we move the origin by passing the X and Y values of our center point to TranslateTransform():
e.Graphics.TranslateTransform(center.X, center.Y)
Now the point (0, 0) is located at the center of our UserControl! Let’s use this fact to draw the circle of our clock at the origin. First we need to compute an appropriate radius for our circle by selecting the smaller value between the width and height of our control, and then taking a percentage of that:
Dim radius As Integer = Math.Min(Me.ClientSize.Width, Me.ClientSize.Height) / 2 * 0.8
I’ve used 0.8, or 80%, to ensure that our circle will always be completely visible and have a small margin between it and the edges of the control. The next step is to create a bounding box that our circle will be drawn in. We start with a rectangle located at (0, 0) and having a size of 1x1 pixel. Then we use the Inflate() method and our computed radius to make the rectangle expand to our desired target size while still keeping the same center point (the origin). Lastly, we render the circle with DrawEllipse():
Dim clock As New Rectangle(New Point(0, 0), New Size(1, 1))
clock.Inflate(radius, radius)
e.Graphics.DrawEllipse(Pens.Black, clock)
At this point, the code in our analog clock control should look like this:
Public Class AnalogClock
Private Sub AnalogClock_Paint(ByVal sender As System.Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles MyBase.Paint
Dim center As New Point(Me.ClientSize.Width / 2, Me.ClientSize.Height / 2)
Dim radius As Integer = Math.Min(Me.ClientSize.Width, Me.ClientSize.Height) / 2 * 0.8
e.Graphics.TranslateTransform(center.X, center.Y)
Dim clock As New Rectangle(New Point(0, 0), New Size(1, 1))
clock.Inflate(radius, radius)
e.Graphics.DrawEllipse(Pens.Black, clock)
End Sub
End Class
This should produce a circle in the center of our control no matter what its width or height is. To verify this, in the menus, click on Build --> Build Solution. Once compiled, double click on your default Form in Solution Explorer to go back to design mode. You should now find a new UserControl called “AnalogClock” at the top of your Toolbox: Dim Hour As Integer = IIf(DateTime.Now.Hour >= 12, DateTime.Now.Hour - 12, DateTime.Now.Hour)
The IIf() function evaluates the first parameter and returns the second parameter if the first is true, or the third parameter if the first is false. Note that 12 in the afternoon becomes a zero, since 12 minus 12 equals zero. Experienced programmers will immediately point out that this can be accomplished with much shorter code using the Mod function:
Dim Hour As Integer = DateTime.Now.Hour Mod 12
Use whatever you’re more comfortable with, as both versions are valid. One will likely make your eyebrow rise like Dr. Spock, while the other will magically seem to just “make more sense”. With the Hour component scaled down to values between 0 and 11, we can compute what percentage of 360 it represents:
Dim HourAngle As Integer = Hour / 12 * 360
Pretend the time is currently 1:00 PM. The Hour value would return 13, which we scale down to 1. Next we divide 1 by 12, and multiply that by 360 to get the angle of 30. A value of 30 degrees would be pointing down and to the right with respect to the center of our control (see the angles video above). Unfortunately, this isn’t where the hour hand should be on an analog clock for 1:00 PM! To get the hour hand at the correct angle, we have to rotate it 90 degrees in the counter-clockwise direction. This is done by subtracting 90 from the previous calculation making the correct formula for the angle of the hour hand:
Dim HourAngle As Integer = Hour / 12 * 360 – 90
The following lists the correct angles for all 24 hours as computed by the above formula:
e.Graphics.RotateTransform(HourAngle)
Now that we have the angle computed and the surface rotated, let’s actually draw the hour hand on our clock. Previously I had stated that I would draw all the hands as horizontal lines on the x-axis, extending from the origin and to the right. So the line will start at (0, 0) and extend to some point on the right (x, 0). The length of the hand should obviously be less than the radius of our circle, but what is a good value? Just as we calculated the radius as a percentage of the UserControl size, our hand length can be calculated as a percentage of the radius. It should be less than radius but allow enough room for the minute hand to be longer. On most clocks the hour hand is shorter than the minute hand, so I will use 65% for the hour hand and 80% for the minute and second hands. Here is the code I used to calculate the length of the hour hand and draw it on the clock face in red:
x = radius * 0.65
e.Graphics.DrawLine(Pens.Red, 0, 0, x, 0)
Since the surface had already been rotated, drawing on the x-axis to the desired length will make the line appear at the correct angle instead of as a horizontal line (unless the current hour angle happened to be zero). In preparation for drawing the next hand of the clock, I immediately undo the rotation by passing in the opposite of HourAngle to RotateTransform(). This will leave the graphics surface at a net rotation of zero degrees:
e.Graphics.RotateTransform(-HourAngle)
Put all together, the code for computing the angle, rotating the surface, and drawing the hour hand is:
x = radius * 0.65
Dim Hour As Integer = IIf(DateTime.Now.Hour >= 12, DateTime.Now.Hour - 12, DateTime.Now.Hour)
Dim HourAngle As Integer = Hour / 12 * 360 - 90
e.Graphics.RotateTransform(HourAngle)
e.Graphics.DrawLine(Pens.Red, 0, 0, x, 0)
e.Graphics.RotateTransform(-HourAngle)
The code for drawing the minute and seconds hands is very similar except that we don’t have to do any scaling of the values before we use them. Simply compute the percentage of 60 that the current minute/second value is and multiply that by 360. There are six degrees between each position of the minute/second hands. Just as with the hour hand, we still need to offset it by -90 degrees so that zero is pointing straight up instead of to the right. I draw the minute hand in blue and the second hand in black. Since the hour hand is shorter than the minute hand, we should draw the minute hand first and the hour hand second so that it will be visible when they are the same value. Here is the complete Paint() event code that draws the clock circle along with hour, minute, and second hands:
Private Sub BareBonesAnalogClock_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
Dim center As New Point(Me.ClientSize.Width / 2, Me.ClientSize.Height / 2)
Dim radius As Integer = Math.Min(Me.ClientSize.Width, Me.ClientSize.Height) / 2 * 0.8
e.Graphics.TranslateTransform(center.X, center.Y)
' the clock circle
Dim clock As New Rectangle(New Point(0, 0), New Size(1, 1))
clock.Inflate(radius, radius)
e.Graphics.DrawEllipse(Pens.Black, clock)
' draw the minute hand
Dim x As Integer = radius * 0.8
Dim MinuteAngle As Integer = DateTime.Now.Minute / 60 * 360 - 90
e.Graphics.RotateTransform(MinuteAngle)
e.Graphics.DrawLine(Pens.Blue, 0, 0, x, 0)
e.Graphics.RotateTransform(-MinuteAngle)
' draw the hour hand
x = radius * 0.65
Dim Hour As Integer = IIf(DateTime.Now.Hour >= 12, DateTime.Now.Hour - 12, DateTime.Now.Hour)
Dim HourAngle As Integer = Hour / 12 * 360 - 90
e.Graphics.RotateTransform(HourAngle)
e.Graphics.DrawLine(Pens.Red, 0, 0, x, 0)
e.Graphics.RotateTransform(-HourAngle)
' draw the second hand
x = radius * 0.8
Dim SecondAngle As Integer = DateTime.Now.Second / 60 * 360 - 90
e.Graphics.RotateTransform(SecondAngle)
e.Graphics.DrawLine(Pens.Black, 0, 0, x, 0)
e.Graphics.RotateTransform(-SecondAngle)
End Sub
Recompile the UserControl by hitting Build --> Build Solution again and the AnalogClock control on your form should now look something like this: Private WithEvents Tmr As New System.Windows.Forms.Timer
Next, across the top of the code editor, change the left dropdown to “Tmr” and the right dropdown to “Tick”. This should add the blank stub event handler shown below; to which I’ve added one line of code, “Me.Refresh()”, to make the UserControl repaint itself whenever the Tick() event fires:
Private Sub Tmr_Tick(ByVal sender As Object, ByVal e As System.EventArgs) Handles Tmr.Tick
Me.Refresh()
End Sub
For the Timer to do its job properly we need to set its Interval() property and turn it on. One good place to do this from would be the Load() event of the UserControl which fires before the control becomes visible for the first time. To get this event, across the top of the code editor, change the left dropdown to “(AnalogClock Events)” and the right dropdown to “Load”. In the stub inserted for me, I set the Timer Interval to 1000 to make it fire once every second, and called the Start() method to make it start firing:
Private Sub AnalogClock_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
Tmr.Interval = 1000
Tmr.Start()
End Sub
That’s it! We now have a working analog clock UserControl that shows the correct time. Here is the complete code listing for the “bare bones” version of the control:
Public Class AnalogClock
Private WithEvents Tmr As New System.Windows.Forms.Timer
Private Sub AnalogClock_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
Tmr.Interval = 1000
Tmr.Start()
End Sub
Private Sub Tmr_Tick(ByVal sender As Object, ByVal e As System.EventArgs) Handles Tmr.Tick
Me.Refresh()
End Sub
Private Sub AnalogClock_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
Dim center As New Point(Me.ClientSize.Width / 2, Me.ClientSize.Height / 2)
Dim radius As Integer = Math.Min(Me.ClientSize.Width, Me.ClientSize.Height) / 2 * 0.8
e.Graphics.TranslateTransform(center.X, center.Y)
' the clock circle
Dim clock As New Rectangle(New Point(0, 0), New Size(1, 1))
clock.Inflate(radius, radius)
e.Graphics.DrawEllipse(Pens.Black, clock)
' draw the minute hand
Dim x As Integer = radius * 0.8
Dim MinuteAngle As Integer = DateTime.Now.Minute / 60 * 360 - 90
e.Graphics.RotateTransform(MinuteAngle)
e.Graphics.DrawLine(Pens.Blue, 0, 0, x, 0)
e.Graphics.RotateTransform(-MinuteAngle)
' draw the hour hand
Dim Hour As Integer = IIf(DateTime.Now.Hour >= 12, DateTime.Now.Hour - 12, DateTime.Now.Hour)
Dim HourAngle As Integer = Hour / 12 * 360 - 90
e.Graphics.RotateTransform(HourAngle)
x = radius * 0.65
e.Graphics.DrawLine(Pens.Red, 0, 0, x, 0)
e.Graphics.RotateTransform(-HourAngle)
' draw the second hand
Dim SecondAngle As Integer = DateTime.Now.Second / 60 * 360 - 90
x = radius * 0.8
e.Graphics.RotateTransform(SecondAngle)
e.Graphics.DrawLine(Pens.Black, 0, 0, x, 0)
e.Graphics.RotateTransform(-SecondAngle)
End Sub
End Class
Admittedly, this is a pretty unimpressive clock! The control may not be immediately recognizable as an analog clock, and it’s hard to read. It is true, though, to the original description of “a circle with several lines in it”. In the next article, we will transform this “bare bones” control into an analog clock that is actually usable in a project: Have a question about something in this article? You can receive help directly from the article author. Sign up for a free trial to get started.
Comments (8)
Commented:
I downloaded your "Kids stopwatch" from youtube and loved it.... but, I am eager to learn how to code some thing similar... I searched the internet and found a lot of stop watch projects, but nothing so elegantly done..... espcially the way you control the clock handles to se the time.
I hope you will turn this into a project since you are putting it up for free.....
Thank you
Author
Commented:Commented:
You do some of the best work I've seen on EE.
Nice Article & a "Yes" vote.
Commented:
I was surprised by the degree of clarity and logically lucid step by step approach which was used in the article. Why not start a book in the form of teaching by example.
A collection of projects that you explain in the manner employed in your article.
Just an eBook put on sale at a reasonable price.
It's an idea worth thinking about and would really fill a gap in the current flood of books that teach programming.
As far as I know there is nothing on the market in this form...!
Commented:
I know we make far too many plans for a holiday like Christmas.
Usually the holiday comes and goes and we discover it wasn't long enough to do half of what we intended...
We the above in mind, I ask if we are going to see a second part for the analogue clock article...?
Thanks
View More