A number of years ago I showed an example of creating a typical digital clock using picture numbers and a little bit of VB.NET code. Fast forward to today and I have decided to show you an example of creating an analog style clock in VB.NET 2012. These types of clocks you may know very well. They are the ones with the hands and the ones your watch is probably using… unless you have one of those all digital ones from the 1980′s. If you are new to programming, and how to draw things on a Panel, this little sample code can get you started. So let’s get to the project!
To get started we need to have a nice graphic of a clock face without the hands. I have decided to put one in this article for you. I chose a graphic that has a nice uniform look and should sync up nicely with our hands when we draw them on. Putting them in the project as a resource (Project >> Properties >> Resources tab) we gave it a name of “clockface”. That way in the project we can quickly refer to the graphic when drawing. It is a nice big graphic which will allow us to scale the clock to as small and big as we want.
We are probably going to want a timer control available in the project set to a 1 second (aka 1000 millisecond) interval and its tick event will then refresh the clock panel. You can start the timer on form load, via a button or perhaps you want to toss it right into the class we are going to create. It is up to you to play with it and figure out which design would be best for your application needs.
Our solution is going to take advantage of double buffering a panel. Since panels don’t support double buffering right out of the box, we have to inherit from the existing Panel class and then implement the double buffering. To do this we set the double buffering property in the constructor and then we override the paint event so that we control the drawing on the panel. Setting up this double buffering is going to prevent any flickering because it takes advantage of a secondary buffer image to draw the hands on our graphic and then simply paint over the existing image. This is over the traditional method of invalidating the control, erasing the graphic and then drawing the image (the clearing and redrawing causes the flicker).
Public Class ClockPanel : Inherits Panel Public Sub New() Me.DoubleBuffered = True End Sub ' Find a point on a circle's circumference given the circle's origin, radius and degrees. Private Function FindPointOnCircle(originPoint As Point, radius As Double, angleDegrees As Double) As Point Dim x As Double = radius * Math.Cos(Math.PI * angleDegrees / 180.0) + originPoint.X Dim y As Double = radius * Math.Sin(Math.PI * angleDegrees / 180.0) + originPoint.Y Return New Point(x, y) End Function ' Draw an individual hand on the clock given the origin and the point on the clock. Private Sub DrawHand(originPoint As Point, endPoint As Point, g As Graphics, Optional aPen As Pen = Nothing) If aPen Is Nothing Then Using BlackPen = New Pen(Brushes.Black) BlackPen.Width = 8 g.DrawLine(BlackPen, originPoint, endPoint) End Using Else g.DrawLine(aPen, originPoint, endPoint) End If End Sub Private Function DrawClock() As Image Dim dt As DateTime = DateTime.Now Dim clockImage As Image = ConvertImageToRGBFormat(My.Resources.clockface) Dim clockGraphicsObj As Graphics = Graphics.FromImage(clockImage) ' Radius of minute hand 70% of half the width of the panel Dim radius As Double = (clockImage.Width / 2) * 0.7 ' Origin half of width and height of panel Dim origin As New Point(clockImage.Width / 2, clockImage.Height / 2) ' Calculate degrees for each tick of the hand. 6 degrees for minutes and seconds (360 / 60) ' And 30 degrees for each hour tick (360 / 12) ' Subtract 90 to start hand from Noon/Midnight Dim degreesMinutes As Double = (dt.Minute * 6) - 90.0 Dim degreesHours As Double = (dt.Hour * 30) - 90.0 Dim degreesSeconds As Double = (dt.Second * 6) - 90.0 ' Find the point on the circle the hand needs to point to ' Hour hand is half the length of the other two hands. Dim minutesPoint As Point = FindPointOnCircle(origin, radius, degreesMinutes) Dim hoursPoint As Point = FindPointOnCircle(origin, radius / 2, degreesHours) Dim secondsPoint As Point = FindPointOnCircle(origin, radius, degreesSeconds) ' Draw minutes and hours with normal default black pen DrawHand(origin, minutesPoint, clockGraphicsObj) DrawHand(origin, hoursPoint, clockGraphicsObj) ' Seconds hand is drawn with a red pen of width 4 Using p As New Pen(Brushes.Red) p.Width = 4 DrawHand(origin, secondsPoint, clockGraphicsObj, p) End Using Return clockImage End Function ' Function handles converting images to an 32 bit RGB pixel format Private Function ConvertImageToRGBFormat(img As Image) As Image If Not img.PixelFormat = System.Drawing.Imaging.PixelFormat.Format32bppRgb Then Dim temp As Bitmap = New Bitmap(img.Width, img.Height, System.Drawing.Imaging.PixelFormat.Format32bppRgb) Dim g As Graphics = Graphics.FromImage(temp) g.DrawImage(img, New Rectangle(0, 0, img.Width, img.Height), 0, 0, img.Width, img.Height, GraphicsUnit.Pixel) g.Dispose() Return temp End If Return img End Function ' Override the Panel's paint method, draw the clock and then call the base paint event Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs) MyBase.OnPaint(e) e.Graphics.DrawImage(DrawClock(), 0, 0, Me.Width, Me.Height) End Sub End Class
The heart of this program is the DrawClock() method which is called each time the panel needs to refresh / repaint itself. It is responsible for finding all the points we need to draw our hands. Each hand starts its line from the center of the panel and then radiates out to some point on the circle. This point is calculated by a great little function called “FindPointOnCircle”. That might be a function you want to toss into your library if you don’t have it already. Given an origin, a radius and an angle it will calculate a point that lies on the circle. This gives us the end point of each hand on our clock. Once we have the origin and this point, we can draw a hand. Based on the current time we calculate where the hand should be pointing on the circle and then draw it in the DrawHand() function.
Controlling the radius to the FindPointOnCircle function will determine how long a given clock hand will be. I use a percentage method here so that no matter what size we make the control, the hand won’t extend pass the outside of the clock (unless of course you make the clock width greater than its height and distort the clock).
Looking at the clock panel you will notice a little function called ConvertImageToRGBFormat(). I created this function to handle the case where the clock face image, which is in an index pixel format, needs to be drawn. The graphics drawing methods had an issue with the graphic being indexed and this function converts it into a 32bit RGB making it easier to work with. If you choose another graphic that is already RGB (or convert our gif to an RGB format) then you will probably not need this function. I kept it in to give you another nice little function for your library and show you how to change the image pixel format for future projects.
With our class in place, all that is left is to create an instance of it, set its width, height and location. Then we can start the timer and have it refresh the panel every second, redrawing the hands each time.
Private clock As ClockPanel ' Every second, refresh the clock Private Sub clockTimer_Tick(sender As Object, e As EventArgs) Handles clockTimer.Tick clock.Refresh() End Sub Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load clock = New ClockPanel() clock.Name = "newpanel" clock.Width = 200 clock.Height = 200 clock.Left = 50 clock.Top = 280 Me.Controls.Add(clock) clockTimer.Start() End Sub
That is all there is to it. You will then be able to easily add multiple clocks to your project and manipulate them any way you see fit. Color the hands, adjust the sizing dynamically as well as the timing based on your own metrics. It can be used for stop watches, dashboards, code timing (using a higher resolution timer and preferably different thread of course) etc. This project is great for those looking to get familiar with drawing on panels, using a double buffering panel, timers and a little bit of geometry. Here is a little shot of what it looks like in action. I hope you enjoyed it and thanks for reading!