Subscribe to Martyr2's Programming Underground        RSS Feed
***** 2 Votes

Drawing Analog Style Clocks In VB.NET

Icon Leave Comment
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!

What We Need

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.

The NewPanel Class

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).

What is with the Image Converting Function?

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.

The Last of the Code

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! :)

0 Comments On This Entry

 

October 2014

S M T W T F S
    1 234
567891011
12131415161718
19202122232425
262728293031