65.9K
CodeProject is changing. Read more.
Home

Working analog clock with textures

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (12 votes)

Jan 8, 2004

4 min read

viewsIcon

118473

downloadIcon

685

Step by step instructions on how to use the new translate features.

Sample Image - clock_picture.gif

Introduction

We are going to be taking a look at the construction of an analog clock using textured backgrounds. I know this "looks" hard and intimidating, but believe me, it's anything but hard :-)

Step 1

Modifying your form's startup

First thing is, we need to change the way your form starts so we can control the loading procedure. This is fairly simple to accomplish. What you need to do is create a Shared Sub Main in your main program and set that as the startup. Don't forget to name your form frmClock.

    Shared Sub Main()
        Application.Run(New frmClock())
        Application.Exit()
    End Sub

This will start up the form named frmClock. Then, when it's done, exit the application. Now, we need to modify the new procedure for the form and insert a me.show command and an initClock command.

    Public Sub New()
        MyBase.New()
        InitializeComponent()
        initclock()
    End Sub

All that's left to do now is change the project's startup properties to make it use our Shared sub Main and not the standard startup procedure. Go to Solution Explorer and right click on your project's properties and change the Startup Object.

Step 2

Create the cHands class to hold the hands information

01:    Public Class cHands
02:    Private Angle As Integer = 0
03:    Public Path As New Drawing2D.GraphicsPath()
04:    Public filled As Boolean = False
05:
06:
07:    ' We have to set a new angle so subtract it from the old one and thats 
08:    ' how far we have to move
09:    '
10:    Public Sub setAngle(ByVal newAngle As Integer, ByVal imgCenter As PointF)
11:        Dim Rotate As Integer = newAngle - Angle
12:        Dim trans As New Drawing2D.Matrix()
13:        trans.RotateAt(Rotate, imgCenter)
14:        Path.Transform(trans)
15:        Me.Angle = newAngle
16:    End Sub
17:
18:
19:    ' Did somone say draw?
20:    '
21:    Public Sub draw(ByVal imgDraw As Graphics)
22:        If filled Then
23:            imgDraw.FillPath(Brushes.DimGray, Path)
24:            imgDraw.DrawPath(Pens.Black, Path)
25:        Else
26:            imgDraw.DrawPath(New Pen(Color.DimGray, 2), Path)
27:        End If
28:    End Sub
29: End Class
Line 1
Declare the class name.
Lines 2-4
Set up some internal variables, one to track the last angle, one to hold the graphics object and another to hold the value if we want to fill the lines.
 
Line 10
Create Sub setAngle that will take the new angle of the hand minus the old angle on the hand and rotate the object the rest of the distance.
Line 11
Figure out how much farther the line needs to rotate to get to where it needs to be.
Line 12
Create a new Transform Matrix object.
Lines 13-14
Rotate the hand the distance it needs to go and set the pivot point (rotation point) at the center of the circle.
Line 15
Set the old angle to the new angle.
Line 16
End Sub
 
Line 21
Create Sub Draw that will draw your paths.
Line 22-25
If you want the hands filled, fill them in and draw a black border around them to look nice.
Lines 26-27
Draw the line with a thickness of two, but don't fill it in.
Line 28
End Sub.

In this step, we have created a class that will be used to store information about the individual hands of the clock.

Step 3

Controls

Go to your form, add a Timer and a PictureBox control. Set the PictureBox to the name clock with a background color of black and set it as docked with the entire form. Leave the Timer control as timer1 and enable it.

Step 4

Tick tock goes the clock

First thing we need to do is create the variables for the clock hands using our cHands class and a point that will denote the center of the screen.

    Private handHour As New cHands()
    Private handMinute As New cHands()
    Private handSecond As New cHands()
    Private center = New PointF(150, 150)

Next is the part where it all starts, the initclock Sub.

    Private Sub initclock()
        Me.ClientSize = New Size(300, 300)

First things first, make sure the client area for the form is 300 x 300. Now, we initialize the hands, remember the Path object in cHands? Well, it's come time to set it, but first a few notes. We will always set the hand straight up (at the 12:00 position). From here, it will be rotated from the center of the screen, so it will look as if it is turning like a real clock. We draw our lines with points, point a connects to point b, point b connects to point c, and point c connects again with point a. These points are contained within an array of points. This can look quite overwhelming but really makes perfect sense if you step through it slowly.

    Dim drawHour As Point() = _
        {New Point(Int(clock.Width / 2), Int(clock.Height / 2)), _
        New Point(Int(clock.Width / 2 - 5), 80), _
        New Point(Int(clock.Width / 2), 50), _
        New Point(Int(clock.Width / 2 + 5), 80), _
        New Point(Int(clock.Width / 2), Int(clock.Height / 2))}
                                   
    Dim drawMinute As Point() = _
       {New Point(Int(clock.Width / 2), Int(clock.Height / 2)), _
        New Point(Int(clock.Width / 2 - 5), 60), _
        New Point(Int(clock.Width / 2), 30), _
        New Point(Int(clock.Width / 2 + 5), 60), _
        New Point(Int(clock.Width / 2), Int(clock.Height / 2))}

Why didn't we use center instead of New Point(Int(clock.Width / 2), Int(clock.Height / 2))? Well, center is a PointF (Floating point or Single) and this requires a point (integer). Now we have to associate these values with the Path object.

        handHour.Path.AddLines(drawHour)
        handHour.filled = True
        
        handMinute.Path.AddLines(drawMinute)
        handMinute.filled = True

But wait, what about the second hand? Well, the second hand is just a line. It's not a shape that needs to be filled. So, it's really easy to draw.

        handSecond.Path.AddLine(center, New PointF(Int(clock.Width / 2), 25))
    End Sub

That's it for this Sub. Now, on to the clock timer (its really complicated :-)).

    Private Sub Timer1_Tick(ByVal sender As System.Object, _
                 ByVal e As System.EventArgs) Handles Timer1.Tick
        clock.Invalidate()
    End Sub

Basically, what this says is every time the timer ticks, repaint the clock. Now we need to create the painting event for clock.

    Private Sub clock_Paint(ByVal sender As Object, _
          ByVal e As System.Windows.Forms.PaintEventArgs) Handles clock.Paint
        handHour.setAngle((360 / 60) * Val(Format(Now, "hh")), center)
        handMinute.setAngle((360 / 60) * Val(Format(Now, "mm")), center)
        handSecond.setAngle((360 / 60) * Val(Format(Now, "ss")), center)

        handHour.draw(e.graphics)
        handMinute.draw(e.graphics)
        handSecond.draw(e.graphics)
    End Sub

What this Sub does is fairly simple. It calculates the degrees the hands need to be at, then calls the draw Sub for the corresponding object.

Why not run your program now and watch the hands move across the screen?

Step 5

Dressing it up

Here, you create a Sub named drawFace. You can add anything you want to it. I opted to fill in the ellipses with textures instead of gradients (look better that way).

    Private Sub drawFace(ByVal g As Graphics)
        Dim path As String = Mid(Application.ExecutablePath, 1, _
                          InStrRev(Application.ExecutablePath, "\"))
        Dim grdFace As New TextureBrush(New Bitmap(path & "\face.jpg"))
        Dim grdWatch As New TextureBrush(New Bitmap(path & "\watch.jpg"))

        g.FillEllipse(grdWatch, New Rectangle(5, 5, 290, 290))
        g.FillEllipse(Brushes.Black, New Rectangle(15, 15, 270, 270))
        g.FillEllipse(grdFace, New Rectangle(20, 20, 260, 260))

        Dim f As New Font("Arial", 10, FontStyle.Bold)
        Dim Sze = g.MeasureString("Powered by Visual Basic.NET", f)
        g.DrawString("Powered by Visual Basic.NET", f, _
                     Brushes.Black, (300 - Sze.width) / 2, 200)

        grdFace.Dispose()
        grdWatch.Dispose()

        ' Don't dispose g !
    End Sub

The last thing you need to do is add the drawFace command to your paint Sub like this:

    Private Sub clock_Paint(ByVal sender As Object, _
         ByVal e As System.Windows.Forms.PaintEventArgs) Handles clock.Paint
       drawFace(e.Graphics)

       ...
       ...
       ...

    End Sub

Well, that's it, you should now have a working clock with a cool looking face.