The Analog Clock Project

To illustrate the use of the Graphics object and GDI+ methods, you'll create a clock face with conventional hour and minute hands and a green dot in lieu of a second hand. You will also display the date rotating around the clock face, as shown in Figure 10-5. (If your copy of the book does not display the moving text, you may need to run the program itself, which you will find in Example 10-11 or Example 10-12).

Analog Clock (first image)

Figure 10-5. Analog Clock (first image)

Notice the button marked "24 Hours" in the upper-lefthand corner. Clicking that button changes the clock to a 24 hour display, as shown in Figure 10-6. Notice that in 24 hour mode, the minute hand maintains its position, but the hour hand must be adjusted.

Analog Clock 24-hour face

Figure 10-6. Analog Clock 24-hour face

This project presents a number of challenges including those listed next.

  • How do you draw a clock face?

  • How do you redraw the clock face for 24 hours?

  • How do you determine the position of and draw the hands (and the dot for the second hand?)

  • How do you draw the date around the outer circumference, and how do you move it so that it rotates around the clock?

As is often the case, each problem has many good solutions, and solving these problems will allow you to explore many details of GDI+ programming.

Drawing the Clock Face

In the first iteration of the clock program, you'll just draw the clock face, as shown in Figure 10-7. The complete source code is shown in Example 10-7 and Example 10-8. Detailed analysis follows.

Simple clock face

Figure 10-7. Simple clock face

Example 10-7. Drawing the clock face in C#

image with no caption

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
   
namespace Clock1CS
{
   // Summary description for Form1.
   public class Form1 : System.Windows.Forms.Form
   {
      // Required designer variable.
      private System.ComponentModel.Container components = null;
   
      public Form1(  )
      {
         // Required for Windows Form Designer support
         InitializeComponent(  );
   
         // use the user's choice of colors
         BackColor = SystemColors.Window;
         ForeColor = SystemColors.WindowText;
   
      }
   
      protected override void OnPaint ( PaintEventArgs e )
      {
         Graphics g = e.Graphics;
         SetScale(g);
         DrawFace(g);
         base.OnPaint(e);
      }
   
   
   
      #region Windows Form Designer generated code
      protected override void Dispose( bool disposing )
      {
         if( disposing )
         {
            if (components != null) 
            {
               components.Dispose(  );
            }
         }
         base.Dispose( disposing );
      }
   
      
      /// <summary>
      /// Required method for Designer support - do not modify
      /// the contents of this method with the code editor.
      /// </summary>
      private void InitializeComponent(  )
      {
         // 
         // Form1
         // 
         this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
         this.ClientSize = new System.Drawing.Size(292, 266);
         this.Name = "Form1";
         this.Text = "Clock1CS";
   
      }
      #endregion
   
      [STAThread]
      static void Main(  ) 
      {
         Application.Run(new Form1(  ));
      }
   
      private void SetScale(Graphics g)
      {
         // if the form is too small, do nothing
         if ( Width =  = 0 || Height =  = 0 )
            return;
   
         // set the origin at the center
         g.TranslateTransform(Width/2, Height/2);
   
         // set inches to the minimum of the width 
         // or height dividedby the dots per inch  
         float inches = Math.Min(Width / g.DpiX, Height / g.DpiX);
   
         // set the scale to a grid of 2000 by 2000 units
         g.ScaleTransform(
            inches * g.DpiX / 2000, inches * g.DpiY / 2000);
      }
   
      private void DrawFace(Graphics g)
      {
         // numbers are in forecolor except flash number in green
         // as the seconds go by.
   
         Brush brush = new SolidBrush(ForeColor);
         Font font = new Font("Arial", 40);
      
         float x, y;
   
         const int numHours = 12;
         const int deg = 360 / numHours;
         const int FaceRadius = 450;
         
         // for each of the hours on the clock face
         for (int i = 1; i <= numHours; i++)
         {
   
            // two ways to do alignment.
   
            /*
            // 1. figure out size of the string and then 
            // offset by half the height and half the width
       
            // measure the string you're going to draw given
            // the current font
            SizeF stringSize = 
               g.MeasureString(i.ToString(  ),font);
              
            x = GetCos(i*deg + 90) * FaceRadius;
            x += stringSize.Width / 2;
            y = GetSin(i*deg + 90) * FaceRadius;
            y += stringSize.Height / 2;
   
            g.DrawString(i.ToString(  ), font, brush, -x, -y);
            
            */
   
            // 2. use a StringFormat object and set 
            // its alignment to center
   
            // i = hour  30 degrees = offset per hour  
            // +90 to make 12 straight up
            x = GetCos(i*deg + 90) * FaceRadius;
            y = GetSin(i*deg + 90) * FaceRadius;
   
            StringFormat format = new StringFormat(  );
            format.Alignment = StringAlignment.Center;
            format.LineAlignment = StringAlignment.Center;
   
            g.DrawString(
               i.ToString(  ), font, brush, -x, -y,format);
         
         }   // end for loop
         brush.Dispose(  );
         font.Dispose(  );
      }      // end drawFace
   
      private static float GetSin(float degAngle)
      {
         return (float) Math.Sin(Math.PI * degAngle / 180f);
      }
   
      private static float GetCos(float degAngle)
      {
         return (float) Math.Cos(Math.PI * degAngle / 180f);
      }
   
   }   // end class
}      // end namespace

Example 10-8. Drawing the clock face in VB.NET

image with no caption

Imports System
Imports System.Drawing
Imports System.Collections
Imports System.ComponentModel
Imports System.Windows.Forms
Imports System.Data
   
Namespace ClockFace1
   
   
   
    Public Class Form1
        Inherits System.Windows.Forms.Form
   
   
#Region " Windows Form Designer generated code "
#End Region
   
        Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
            MyBase.OnPaint(e)
            Dim g As Graphics = e.Graphics
            SetScale(g)
            DrawFace(g)
        End Sub 'OnPaint
   
   
   
   
        Private Sub SetScale(ByVal g As Graphics)
            ' if the form is too small, do nothing
            If Width = 0 Or Height = 0 Then
                Return
            End If
            ' set the origin at the center
            g.TranslateTransform(Width/2, Height/2)
   
            ' set inches to the minimum of the width or height divided
            ' by the dots per inch  
            Dim inches As Single = _
               Math.Min(Width / g.DpiX, Height / g.DpiX)
   
            ' set the scale to a grid of 2000 by 2000 units
            g.ScaleTransform( _
               inches * g.DpiX / 2000, inches * g.DpiY / 2000)
        End Sub 'SetScale
   
   
        Private Sub DrawFace(ByVal g As Graphics)
            ' numbers are in forecolor except flash number in green
            ' as the seconds go by.
            Dim myBrush = New SolidBrush(ForeColor)
            Dim greenBrush = New SolidBrush(Color.Green)
            Dim myFont As New Font("Arial", 40)
            Dim x, y As Single
   
            Const numHours As Integer = 12
            Const deg As Integer = 30
            Const FaceRadius As Integer = 450
   
            ' for each of the hours on the clock face
            Dim i As Integer
            For i = 1 To numHours
   
                ' two ways to do alignment.
                ' 1. figure out size of the string and then offset by half
                ' the height and half the width
                ' measure the string you're going to draw given
                ' the current font
   
                ''Dim stringSize As SizeF = _
                      g.MeasureString(i.ToString(  ), font)
                ''x = GetCos(i * deg + 90) * FaceRadius
                ''x += stringSize.Width / 2
                ''y = GetSin(i * deg + 90) * FaceRadius
                ''y += stringSize.Height / 2
                ''g.DrawString(i.ToString(  ), font, brush, -x, -y)
   
   
                ' 2. use a StringFormat object and set its 
                ' alignment to center
                ' i = hour  30 degrees = offset per hour  
                ' +90 to make 12 straight up
                x = GetCos((i * deg + 90)) * FaceRadius
                y = GetSin((i * deg + 90)) * FaceRadius
   
                Dim format As New StringFormat(  )
                format.Alignment = StringAlignment.Center
                format.LineAlignment = StringAlignment.Center
   
                g.DrawString(i.ToString(  ), myFont, myBrush, -x, -y, format)
            Next i
        End Sub 'DrawFace
   
        Private Shared Function GetSin(ByVal degAngle As Single) As Single
            Return CSng(Math.Sin((Math.PI * degAngle / 180.0F)))
        End Function 'GetSin
   
   
        Private Shared Function GetCos(ByVal degAngle As Single) As Single
            Return CSng(Math.Cos((Math.PI * degAngle / 180.0F)))
        End Function 'GetCos
    End Class 'Form1 
End Namespace

Color

When you draw the clock face, you'll need to tell the CLR what color to use for the numbers. You might be tempted to use black, which is perfectly appropriate, but it does raise a problem. As noted in Chapter 9, however, the user may have changed the color scheme to a very dark background (even to black), which would make your clock face invisible.

A better alternative is to set the BackColor and ForeColor for your form based on the Window and WindowText colors the user has chosen. You can do so in the constructor for the form:

image with no caption

BackColor = SystemColors.Window;
ForeColor = SystemColors.WindowText;

You can now set the brush color to the foreground color and feel comfortable with your choice.

OnPaint

Each time the form is created or invalidated, its OnPaint method is called. You can override the OnPaint method to get a Graphics object to work with and paint the control as you wish.

Your override will extract the Graphics object from the PaintEventArgs object passed in as a parameter. It will then pass that Graphics object to two methods: SetScale and DrawFace, described below:

image with no caption

protected override void OnPaint ( PaintEventArgs e )
{
   Graphics g = e.Graphics;
   SetScale(g);
   DrawFace(g);
   base.OnPaint(e);
}

image with no caption

Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
    Dim g As Graphics = e.Graphics
    SetScale(g)
    DrawFace(g)
End Sub 'OnPaint

Transforming the coordinates

The job of the SetScale method is to make the world transformations to set the origin at the center of the form, and to set the scale to an arbitrary grid of 1,000 units in each of the four directions from the center:

image with no caption

private void SetScale(Graphics g)
{

image with no caption

Private Sub SetScale(ByVal g As Graphics)

Start by making sure that the form has at least some width or height:

image with no caption

if ( Width =  = 0 || Height =  = 0 )
   return;

image with no caption

If Width = 0 Or Height = 0 Then
    Return
End If

That done, you are ready to set the origin to the center. To do so, call TranslateTransform on the Graphics object received as a parameter to the method.

The TranslateTransform method is overloaded; the version you'll use takes two floating-point numbers (float in C#, single in VB.NET) as parameters: the x-component of the translation and the y-component. You want to move the origin from the upper left halfway across the form in the x-direction and halfway down the form in the y-direction.

Tip

World translations are implemented with matrices. This mathematical concept is beyond the scope of this book, and you do not need to understand the matrices to use the transformations. For more information, however, please either consult the SDK documentation or look at Charles Petzold's excellent book Programming Microsoft Windows With C# (Microsoft Press).

The form inherits two properties from Control that you'll use: Width and Height. Each returns its value in pixels:

image with no caption

g.TranslateTransform(Width/2, Height/2);

The effect is to transform the origin (0,0) to the center both horizontally and vertically.

You are now set to transform the scale from its current units (pixels by default) to an arbitrary unit. Don't worry about how large each unit is, but you do want 1,000 units in each direction from the origin, no matter what the screen resolution is. Unfortunately, the size of the units must be equal both horizontally and vertically, so you'll need to choose a size. You will thus compute which size is smaller in inches: the width or the height of the device:

image with no caption

float inches = Math.Min(Width/g.DpiX, Height/g.DpiX);

image with no caption

Dim inches As Single = Math.Min(Width/g.DpiX, Height/g.DpiX)

The variable inches now has the smaller of the width or height of the device measured in inches. Multiply that many inches times the dots per inch on the x axis to get the number of dots in the width, and divide by 2,000 to create a unit that is 1/2000th of the width of the form You'll then do the same for the y axis. If you pass these values to ScaleTransform, you'll create an arbitrary scale 2,000 units on the x axis and 2,000 units on the y axis, or 1,000 units in each direction from the center.

image with no caption

g.ScaleTransform(
   inches * g.DpiX/2000, inches * g.DpiY/2000);

Note

To see this computation for ScaleTransform more clearly, you might use interim variables:

totalDotsX = inches * g.DpiX;
numDotsIn2000UnitsX = totalDotsX / 2000;

totalDotsY = inches * g.DpiY;
numDotsIn2000UnitsY = totalDotsY / 2000;
   
g.ScaleTransform(numDotsIn2000UnitsX, numDotsIn2000UnitsY);

When this method ends, you have the grid you need to draw the clock face. The DrawFace method actually does the work.

World transforms

To draw this clock, write the strings 1 through 12 in the appropriate location. Specify the location as x,y coordinates, and these coordinates must be on the circumference of an imaginary circle.

To compute the x coordinate, take the hour and multiply it by 30, add 90, convert this value from degrees to radians, take the cosine, and then multiply that result by the radius. The formula for the y coordinate is identical, except that you use the sin rather than the cosine:

x = GetCos(i*deg + 90) * FaceRadius;

To understand why this formula works, see Sidebar 10-1.

Draw each number on the clock face with the overloaded DrawString method of the Graphics object. Table 10-15 lists the overloaded forms of the DrawString method.

Table 10-15. DrawString method overload list (C# and VB.NET)

Method

Description

void DrawString(string, Font, Brush, PointF);
sub DrawString(string, Font, Brush, PointF)

Draw the specified string using the specified font and brush at the specified point.

void DrawString(string, Font, Brush, RectangleF);
sub DrawString(string, Font, Brush, RectangleF)

Draw the specified string using the specified font and brush in the specified rectangle.

void DrawString(string, Font, Brush, PointF, 
   StringFormat);
sub DrawString(string, Font, Brush, PointF, _
   StringFormat)

Draw the specified string using the specified font and brush at the specified point using the specified StringFormat.

void DrawString(string, Font, Brush, RectangleF, 
   StringFormat);
sub DrawString(string, Font, Brush, RectangleF, _
   StringFormat)

Draw the specified string using the specified font and brush in the specified rectangle using the specified StringFormat.

void DrawString(string, Font, Brush, float, float);
sub DrawString(string, Font, Brush, float, float)

Draw the specified string using the specified font and brush at the specified x and y coordinates.

void DrawString(string, Font, Brush, float, float, 
   StringFormat);
sub DrawString(string, Font, Brush, float, float, _
   StringFormat)

Draw the specified string using the specified font and brush at the specified x and y coordinates using the specified StringFormat.

The version of DrawString you'll use in this example will take five parameters:

  • The string to draw (the numbers 1 through 12)

  • The font to draw in (e.g., Arial 8)

  • A brush to determine the color and texture of the text

  • The x coordinate of the upper-lefthand corner of the text

  • The y coordinate of the upper-lefthand corner of the text

You know you'll need a brush, and you know you want to draw in the foreground color determined by the user, so create an instance of a SolidBrush, passing in the ForeColor property of the form:

image with no caption

Brush brush = new SolidBrush(ForeColor);

image with no caption

Dim brush = New SolidBrush(ForeColor)

You also need a Font object. You'll create a font to represent the font face Arial and the size 40. This size will be relative to your new arbitrary scale, so it is arrived at by trial and error:

image with no caption

Font font = new Font("Arial", 40);

image with no caption

Dim font As New Font("Arial", 40)

Next, declare two float variables to hold the x and y coordinates that you will compute using the formula discussed earlier (see Sidebar 10-1), as well as a few useful constants:

image with no caption

float x, y;
const int numHours = 12;
const int deg = 360 / numHours;
const int FaceRadius = 450;

image with no caption

Dim x, y As Single
Const numHours As Integer = 12
Const deg As Integer = 360 / numHours
Const FaceRadius As Integer = 450

Create the string to draw by creating a for loop:

image with no caption

for (int i = 1; i <= numHours; i++)
{

image with no caption

Dim i As Integer
For i = 1 To numHours

Within that loop, draw each number in turn. The first task is to compute the x,y coordinates on the circle:

x = GetCos(i*deg + 90) * FaceRadius;
y = GetSin(i*deg + 90) * FaceRadius;

The GetCos and GetSin methods convert the degrees to radians:

image with no caption

private static float GetSin(float degAngle)
{
   return (float) Math.Sin(Math.PI * degAngle / 180f);
}
   
private static float GetCos(float degAngle)
{
   return (float) Math.Cos(Math.PI * degAngle / 180f);
}

image with no caption

Private Shared Function GetSin(ByVal degAngle As Single) As Single
    Return CSng(Math.Sin((Math.PI * degAngle / 180.0F)))
End Function 'GetSin
   
   
Private Shared Function GetCos(ByVal degAngle As Single) As Single
    Return CSng(Math.Cos((Math.PI * degAngle / 180.0F)))
End Function 'GetCos

Once you have the coordinates, you are ready to draw the numbers. The problem, however, is that the x,y coordinates you've computed will be the location of the upper-lefthand corner of the numbers you draw. This will result in a slightly lopsided clock.

To fix this, center the string around the point determined by your location formula. You can do this in two ways. In the first approach, measure the string, and then subtract half the width and height from the location. Begin by calling the MeasureString method on the Graphics object, passing in the string (the number you want to display) and the font in which you want to display it:

image with no caption

SizeF stringSize =
   g.MeasureString(i.ToString(  ),font);

image with no caption

Dim stringSize As SizeF = _
   g.MeasureString(i.ToString(  ), font)

You get back an object of type SizeF. SizeF is a struct, described earlier, that has two important properties: Width and Height. You can now compute the location of the object, and then offset the x location by half the width and the y location by half the height.

image with no caption

x = GetCos(i*deg + 90) * FaceRadius;
x += stringSize.Width / 2;
y = GetSin(i*deg + 90) * FaceRadius;
y += stringSize.Height / 2;

This works perfectly, but .NET is willing to do a lot of the work for you. The trick of the second approach is to call an overloaded version of the DrawString method that takes an additional (sixth) parameter: an object of type StringFormat:

image with no caption

StringFormat format = new StringFormat(  );

image with no caption

Dim format As New StringFormat(  )

You now set the Alignment and LineAlignment properties of the StringFormat object to set the horizontal and vertical alignment of the text you will display. These properties take one of the StringAlignment enumerated values: Center, Far, and Near. Center will center the text as you'd expect. The Near value specifies that the text is aligned near the origin, while the far value specifies that the text is displayed far from the origin. In a left-to-right layout, the near position is left and the far position is right.

format.Alignment = StringAlignment.Center;
format.LineAlignment = StringAlignment.Center;

You are now ready to display the string:

g.DrawString(i.ToString(  ), font, brush, -x, -y,format);

The StringFormat object takes care of aligning your characters, and your clock face is no longer lopsided.

Adding the Hands

Now it's time to add the hour and minute hands to the clock. You will also implement the second "hand" as a ball that will rotate around the circumference of the clock. To see this work, add a timer to update the time every second. Also add the button that switches between the 24- and 12-hour clock.

The complete source code is provided in Example 10-9 and Example 10-10. A detailed analysis follows.

Example 10-9. Clock face 2 in C#

image with no caption

using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Timers;
using System.Windows.Forms;
   
   
namespace Clock2CS
{
   // Summary description for Form1.
   public class Form1 : System.Windows.Forms.Form
   {
      // Required designer variable.
      private System.ComponentModel.Container components = null;
   
      private int FaceRadius = 450;   // size of the clock face
      private bool b24Hours = false;  // 24 hour clock face?
   
      private System.Windows.Forms.Button btnClockFormat;   
      private DateTime currentTime;      // used in more than one method
   
      public Form1(  )
      {
         // Required for Windows Form Designer support
         InitializeComponent(  );
   
         // use the user's choice of colors
         BackColor = SystemColors.Window;
         ForeColor = SystemColors.WindowText;
   
         // update the clock by timer
         System.Timers.Timer timer = new System.Timers.Timer(  );
         timer.Elapsed += new System.Timers.ElapsedEventHandler(OnTimer);
         timer.Interval = 500;
         timer.Enabled = true;
      }
   
      protected override void OnPaint ( PaintEventArgs e )
      {
         base.OnPaint(e);
         Graphics g = e.Graphics;
         SetScale(g);
         DrawFace(g);
         DrawTime(g,true);   // force an update
      }
   
      // every time the timer event fires, update the clock
      public void OnTimer(Object source, ElapsedEventArgs e)
      {
         Graphics g = this.CreateGraphics(  );
   
         SetScale(g);
         DrawFace(g);
         DrawTime(g,false);
         g.Dispose(  );      
   
      }
   
   
      #region Windows Form Designer generated code
      #endregion
   
      [STAThread]
      static void Main(  ) 
      {
         Application.Run(new Form1(  ));
      }
   
      private void SetScale(Graphics g)
      {
         // if the form is too small, do nothing
         if ( Width =  = 0 || Height =  = 0 )
            return;
   
         // set the origin at the center
         g.TranslateTransform(Width/2, Height/2);
   
         // set inches to the minimum of the width 
         // or height dividedby the dots per inch  
         float inches = Math.Min(Width / g.DpiX, Height / g.DpiX);
   
         // set the scale to a grid of 2000 by 2000 units
         g.ScaleTransform(
            inches * g.DpiX / 2000, inches * g.DpiY / 2000);
      }
   
      private void DrawFace(Graphics g)
      {
         // numbers are in forecolor except flash number in green
         // as the seconds go by.
         Brush brush = new SolidBrush(ForeColor);
         Font font = new Font("Arial", 40);
         float x, y;
   
         // new code
         int numHours = b24Hours ? 24 : 12;
         int deg = 360 / numHours;
         
         // for each of the hours on the clock face
         for (int i = 1; i <= numHours; i++)
         {
            // i = hour  30 degrees = offset per hour  
            // +90 to make 12 straight up
            x = GetCos(i*deg + 90) * FaceRadius;
            y = GetSin(i*deg + 90) * FaceRadius;
   
            StringFormat format = new StringFormat(  );
            format.Alignment = StringAlignment.Center;
            format.LineAlignment = StringAlignment.Center;
   
            g.DrawString(
               i.ToString(  ), font, brush, -x, -y,format);
         
         }   // end for loop
      }      // end drawFace
   
   
      private void DrawTime(Graphics g, bool forceDraw)
      {
   
         //  length of the hands
         float hourLength = FaceRadius * 0.5f;
         float minuteLength = FaceRadius * 0.7f;
         float secondLength = FaceRadius * 0.9f;
   
         // set to back color to erase old hands first
         Pen hourPen = new Pen(BackColor);
         Pen minutePen = new Pen(BackColor);
         Pen secondPen = new Pen(BackColor);
   
         // set the arrow heads
         hourPen.EndCap = LineCap.ArrowAnchor;
         minutePen.EndCap = LineCap.ArrowAnchor;
   
         // hour hand is thicker
         hourPen.Width = 30;
         minutePen.Width = 20;
   
         // second hand 
         Brush secondBrush = new SolidBrush(BackColor);
         const int EllipseSize = 50;
   
         GraphicsState state;   // to protect and to serve
   
   
         // Step 1.  Delete the old time
   
         // delete the old second hand
         // figure out how far around to rotate to draw the second hand
         // save the current state, rotate, draw and then restore the 
         // state
         float rotation = GetSecondRotation(  );
         state = g.Save(  );
         g.RotateTransform(rotation);
         g.FillEllipse(
            secondBrush,
            -(EllipseSize/2),
            -secondLength,
            EllipseSize,
            EllipseSize);
         g.Restore(state);
   
         DateTime newTime = DateTime.Now;
         bool newMin = false;   // has the minute changed?
   
         // if the minute has changed, set the flag
         if ( newTime.Minute != currentTime.Minute )
            newMin = true;
   
   
         // if the minute has changed or you must draw anyway then you 
         // must first delete the old minute and hour hand
         if ( newMin  || forceDraw )
         {
   
            // figure out how far around to rotate to draw the minute hand
            // save the current state, rotate, draw and then 
            // restore the state
            rotation = GetMinuteRotation(  );
            state = g.Save(  );
            g.RotateTransform(rotation);
            g.DrawLine(minutePen,0,0,0,-minuteLength);
            g.Restore(state);
   
            // figure out how far around to rotate to draw the hour hand
            // save the current state, rotate, draw and then 
            // restore the state
            rotation = GetHourRotation(  );
            state = g.Save(  );
            g.RotateTransform(rotation);
            g.DrawLine(hourPen,0,0,0,-hourLength);
            g.Restore(state);
         }
   
         // step 2 - draw the new time
         currentTime = newTime;
   
         hourPen.Color = Color.Red;
         minutePen.Color = Color.Blue;
         secondPen.Color = Color.Green;
         secondBrush = new SolidBrush(Color.Green);
   
         // draw the new second hand
         // figure out how far around to rotate to draw the second hand
         // save the current state, rotate, draw and then restore the 
         // state
         state = g.Save(  );
         rotation = GetSecondRotation(  );
         g.RotateTransform(rotation);
         g.FillEllipse(
            secondBrush,
            -(EllipseSize/2),
            -secondLength,
            EllipseSize,
            EllipseSize);
         g.Restore(state);
   
         // if the minute has changed or you must draw anyway then you 
         // must draw the new minute and hour hand
         if ( newMin || forceDraw )
         {
   
            // figure out how far around to rotate to draw the minute hand
            // save the current state, rotate, draw and then 
            // restore the state
            state = g.Save(  );
            rotation = GetMinuteRotation(  );
            g.RotateTransform(rotation);
            g.DrawLine(minutePen,0,0,0,-minuteLength);
            g.Restore(state);
   
            // figure out how far around to rotate to draw the hour hand
            // save the current state, rotate, draw and then 
            // restore the state
            state = g.Save(  );
            rotation = GetHourRotation(  );
            g.RotateTransform(rotation);
            g.DrawLine(hourPen,0,0,0,-hourLength);
            g.Restore(state);
         }
      }
   
      // determine the rotation to draw the hour hand
      private float GetHourRotation(  )
      {
         // degrees depend on 24 vs. 12 hour clock
         float deg = b24Hours ? 15 : 30;
         float numHours = b24Hours ? 24 : 12;
         return( 360f * currentTime.Hour / numHours +
            deg * currentTime.Minute / 60f);
      }
   
      private float GetMinuteRotation(  )
      {
         return( 360f * currentTime.Minute / 60f ); 
      }
   
      private float GetSecondRotation(  )
      {
         return(360f * currentTime.Second / 60f);
      }
   
      private static float GetSin(float degAngle)
      {
         return (float) Math.Sin(Math.PI * degAngle / 180f);
      }
   
      private static float GetCos(float degAngle)
      {
         return (float) Math.Cos(Math.PI * degAngle / 180f);
      }
   
      private void btnClockFormat_Click(object sender, System.EventArgs e)
      {
         btnClockFormat.Text = b24Hours ? "24 Hour" : "12 Hour";
         b24Hours = ! b24Hours;
         this.Invalidate(  );
   
      }
   
   }   // end class
}      // end namespace

Example 10-10. Clock face 2 in VB.NET

image with no caption

Imports System
Imports System.Collections
Imports System.ComponentModel
Imports System.Data
Imports System.Drawing
Imports System.Drawing.Drawing2D
Imports System.Timers
Imports System.Windows.Forms
   
Namespace ClockFace1
   
   
    Public Class Form1
        Inherits System.Windows.Forms.Form
   
        Private FaceRadius As Integer = 450 ' size of the clock face
        Private b24Hours As Boolean = False ' 24 hour clock face?
        Private currentTime As DateTime
        Private WithEvents btnClockFormat as Button
   
   
   
        Public Sub New(  )
            MyBase.New(  )
   
            'This call is required by the Windows Form Designer.
            InitializeComponent(  )
            ' use the user's choice of colors
            BackColor = SystemColors.Window
            ForeColor = SystemColors.WindowText
   
            ' redraw when resized
            Me.ResizeRedraw = True
   
            ' update the clock by timer
            Dim timer As New System.Timers.Timer(  )
            AddHandler timer.Elapsed, AddressOf OnTimer
            timer.Interval = 500
            timer.Enabled = True
        End Sub
   
   
        ' every time the timer event fires, update the clock
        Public Sub OnTimer( _
          ByVal source As Object, ByVal e As ElapsedEventArgs)
            Dim g As Graphics = Me.CreateGraphics(  )
   
            SetScale(g)
            DrawFace(g)
            DrawTime(g, False)
            g.Dispose(  )
        End Sub 'OnTimer
   
#Region " Windows Form Designer generated code "
#End Region
   
        Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
            MyBase.OnPaint(e)
            Dim g As Graphics = e.Graphics
            SetScale(g)
            DrawFace(g)
            DrawTime(g, True) ' force an update
        End Sub 'OnPaint
   
        Private Sub SetScale(ByVal g As Graphics)
            ' if the form is too small, do nothing
            If Width = 0 Or Height = 0 Then
                Return
            End If
            ' set the origin at the center
            g.TranslateTransform(Width / 2, Height / 2)
   
            ' set inches to the minimum of the width or height divided
            ' by the dots per inch  
            Dim inches As Single = _
              Math.Min(Width / g.DpiX, Height / g.DpiX)
   
            ' set the scale to a grid of 2000 by 2000 units
            g.ScaleTransform(inches * g.DpiX / 2000, _
                  inches * g.DpiY / 2000)
        End Sub 'SetScale
   
   
        Private Sub DrawFace(ByVal g As Graphics)
            ' numbers are in forecolor except flash number in green
            ' as the seconds go by.
            Dim brush = New SolidBrush(ForeColor)
            Dim font As New Font("Arial", 40)
            Dim x, y As Single
   
            Dim numHours As Integer
            If b24Hours Then
                numHours = 24
            Else
                numHours = 12
            End If
            Dim deg As Integer = 360 / numHours
            Const FaceRadius As Integer = 450
   
            ' for each of the hours on the clock face
            Dim i As Integer
            For i = 1 To numHours
                ' i = hour  30 degrees = offset per hour  
                ' +90 to make 12 straight up
                x = GetCos((i * deg + 90)) * FaceRadius
                y = GetSin((i * deg + 90)) * FaceRadius
   
                Dim format As New StringFormat(  )
                format.Alignment = StringAlignment.Center
                format.LineAlignment = StringAlignment.Center
   
                g.DrawString(i.ToString(  ), font, brush, -x, -y, format)
            Next i
        End Sub 'DrawFace
   
        Private Sub DrawTime( _
           ByVal g As Graphics, ByVal forceDraw As Boolean)
   
            '  length of the hands
            Dim hourLength As Single = FaceRadius * 0.5F
            Dim minuteLength As Single = FaceRadius * 0.7F
            Dim secondLength As Single = FaceRadius * 0.9F
   
            ' set to back color to erase old hands first
            Dim hourPen As New Pen(BackColor)
            Dim minutePen As New Pen(BackColor)
            Dim secondPen As New Pen(BackColor)
   
            ' set the arrow heads
            hourPen.EndCap = LineCap.ArrowAnchor
            minutePen.EndCap = LineCap.ArrowAnchor
   
            ' hour hand is thicker
            hourPen.Width = 30
            minutePen.Width = 20
   
            ' second hand is in green
            Dim secondBrush = New SolidBrush(BackColor)
            Const EllipseSize As Single = 50
   
            Dim rotation As Single ' how far around the circle?
            Dim state As GraphicsState ' to to protect and to serve
            Dim newTime As DateTime = DateTime.Now
            Dim newMin As Boolean = False ' has the minute changed?
            ' if the minute has changed, set the flag
            If newTime.Minute <> currentTime.Minute Then
                newMin = True
            End If
            ' 1 - delete the old time
            ' delete the old second hand
            ' figure out how far around to rotate to draw the second hand
            ' save the current state, rotate, draw and then 
            ' restore the state
            rotation = GetSecondRotation(  )
            state = g.Save(  )
            g.RotateTransform(rotation)
            g.FillEllipse( _
                secondBrush, _
                -(EllipseSize / 2), _
                -secondLength, _
                EllipseSize, _
                EllipseSize)
            g.Restore(state)
   
            ' if the minute has changed or you must draw anyway then you 
            ' must first delete the old minute and hour hand
            If newMin Or forceDraw Then
   
                ' how far around to rotate to draw the minute hand
                ' save the current state, rotate, draw and then 
                ' restore the state
                rotation = GetMinuteRotation(  )
                state = g.Save(  )
                g.RotateTransform(rotation)
                g.DrawLine(minutePen, 0, 0, 0, -minuteLength)
                g.Restore(state)
   
                ' figure out how far around to rotate to draw the 
                ' hour hand save the current state, rotate, draw and then 
                ' restore the state
                rotation = GetHourRotation(  )
                state = g.Save(  )
                g.RotateTransform(rotation)
                g.DrawLine(hourPen, 0, 0, 0, -hourLength)
                g.Restore(state)
            End If
   
            ' step 2 - draw the new time
            currentTime = newTime
   
            hourPen.Color = Color.Red
            minutePen.Color = Color.Blue
            secondPen.Color = Color.Green
            secondBrush = New SolidBrush(Color.Green)
   
            ' draw the new second hand
            ' figure out how far around to rotate to draw the second hand
                ' save the current state, rotate, draw and then 
                ' restore the state
            state = g.Save(  )
            rotation = GetSecondRotation(  )
            g.RotateTransform(rotation)
            g.FillEllipse( _
                secondBrush, _
                -(EllipseSize / 2), _
                -secondLength, _
                EllipseSize, _
                EllipseSize)
            g.Restore(state)
   
            ' if the minute has changed or you must draw anyway then you 
            ' must draw the new minute and hour hand
            If newMin Or forceDraw Then
   
                ' how far around to rotate to draw the minute hand
                ' save the current state, rotate, draw and then 
                ' restore the state
                state = g.Save(  )
                rotation = GetMinuteRotation(  )
                g.RotateTransform(rotation)
                g.DrawLine(minutePen, 0, 0, 0, -minuteLength)
                g.Restore(state)
   
                ' figure out how far around to rotate to draw the hour hand
                ' save the current state, rotate, draw and then 
                ' restore the state
                state = g.Save(  )
                rotation = GetHourRotation(  )
                g.RotateTransform(rotation)
                g.DrawLine(hourPen, 0, 0, 0, -hourLength)
                g.Restore(state)
            End If
        End Sub 'DrawTime
   
   
        ' determine the rotation to draw the hour hand
        Private Function GetHourRotation(  ) As Single
            ' degrees depend on 24 vs. 12 hour clock
            Dim deg As Single
            Dim numHours As Single
            If b24Hours Then
                deg = 15
                numHours = 24
            Else
                deg = 30
                numHours = 12
            End If
   
            Return 360.0F * currentTime.Hour / _
              numHours + deg * currentTime.Minute / 60.0F
        End Function 'GetHourRotation
   
   
        Private Function GetMinuteRotation(  ) As Single
            Return 360.0F * currentTime.Minute / 60.0F
        End Function 'GetMinuteRotation
   
   
        Private Function GetSecondRotation(  ) As Single
            Return 360.0F * currentTime.Second / 60.0F
        End Function 'GetSecondRotation
   
   
        Private Shared Function GetSin(ByVal degAngle As Single) As Single
            Return CSng(Math.Sin((Math.PI * degAngle / 180.0F)))
        End Function 'GetSin
   
   
        Private Shared Function GetCos(ByVal degAngle As Single) As Single
            Return CSng(Math.Cos((Math.PI * degAngle / 180.0F)))
        End Function 'GetCos
   
        Private Sub btnClockFormat_Click( _
           ByVal sender As System.Object, _
           ByVal e As System.EventArgs) _
           Handles btnClockFormat.Click
   
            If b24Hours Then
                btnClockFormat.Text = "24 Hours"
                b24Hours = False
            Else
                btnClockFormat.Text = "12 Hours"
                b24Hours = True
            End If
   
            Me.Invalidate(  )
   
        End Sub
    End Class 'Form1 
End Namespace

Creating the timer

One of the most significant changes in this version of the program is the use of a timer to tick off the seconds. You instantiate the timer in the constructor:

image with no caption

System.Timers.Timer timer = new System.Timers.Timer(  );

image with no caption

Dim timer As New System.Timers.Timer(  )

Set its event handler by passing in the name of the method you want called when the interval you'll specify has elapsed:

image with no caption

timer.Elapsed += new System.Timers.ElapsedEventHandler(OnTimer);

image with no caption

AddHandler timer.Elapsed, AddressOf OnTimer

The interval is set in milliseconds; in this case you'll update the timer every 500 milliseconds (every half second):

timer.Interval = 500;

Finally, kick off the timer by enabling it:

timer.Enabled = true;

Implementing OnTimer

The event handler you've passed to the timer's Elapsed event is OnTimer( ). The implementation of OnTimer is similar to that of OnPaint: set the scale, draw the face, and then draw the hands. The latter operation occurs in a new method named DrawTime, discussed next:

image with no caption

public void OnTimer(Object source, ElapsedEventArgs e
{
   Graphics g = this.CreateGraphics(  );
   
   SetScale(g);
   DrawFace(g);
   DrawTime(g,false);
   
   g.Dispose(  );      
}

image with no caption

Public Sub OnTimer( _
  ByVal source As Object, ByVal e As ElapsedEventArgs)
    Dim g As Graphics = Me.CreateGraphics(  )
   
    SetScale(g)
    DrawFace(g)
    DrawTime(g, False)
   
    g.Dispose(  )
End Sub 'OnTimer

The key difference between OnTimer and OnPaint is that the EventArgs structure passed to OnTimer does not have a Graphics object. You'll get one from the form by calling CreateGraphics (highlighted in the code snippet).

This Graphics object then invokes the same methods invoked in OnPaint. When you are done with the Graphics object obtained by CreateGraphics, you must dispose of it through a call to its Dispose method (also highlighted in the snippet).

DrawTime method

After OnTimer calls DrawFace, it calls DrawTime (OnPaint has been modified to call DrawTime as well). DrawTime is responsible for drawing the hands on the clock to correspond to the current time.

In the DrawTime method, you will first delete the hands from their current positions and then draw them in their new positions. You will draw the hands as lines and put an arrow at the end of the line to simulate an old fashioned clock's hand. Deleting the hands is accomplished by drawing the hands with a brush set to the color of the background (thus making them invisible).

Drawing the hands

You will draw the hands of the clock with a Pen object. The Pen class has properties and methods, described previously in Table 10-9.

Pass the pen to a drawing method, and that method determines how long a line to draw and what direction to draw in. The line you draw will have the Color, Width, and other characteristics you set with the Pen's properties.

The EndCap property is of type LineCap, an enumeration listed in Table 10-12. In addition to the ArrowAnchor used in these examples, you can chose to create a Round, Square, Triangle, or Flat line cap, or you can create a RoundAnchor, SquareAnchor, or NoAnchor.

You instantiate a Pen with a color as follows:

image with no caption

Pen myPen = new Pen(Color.Red);

image with no caption

dim myPen as new Pen(Color.Red)

Deleting the existing line

Now that you have the necessary tools in hand, it is time to update the clock face. First, delete the hands from their old position. Start by creating three pens, one each to draw the hour, minute, and second hands. Each pen will use the background color:

image with no caption

Pen hourPen = new Pen(BackColor);
Pen minutePen = new Pen(BackColor);
Pen secondPen = new Pen(BackColor);

image with no caption

Dim hourPen As New Pen(BackColor)
Dim minutePen As New Pen(BackColor)
Dim secondPen As New Pen(BackColor)

Next, set the hour and minute pen to use an ArrowAnchor:

image with no caption

 hou

image with no caption

rPen.EndCap = LineCap.ArrowAnchor;
minutePen.EndCap = LineCap.ArrowAnchor;

and set the width of the hour and minute pens:

image with no caption

 ho

image with no caption

urPen.Width = 30;
minutePen.Width = 20;

You do not need to set the EndCap or Width of the second hand because you'll just draw a dot for the second hand (shown below). What you do need for drawing the second hand, however, is a brush:

image with no caption

Brush secondBrush = new SolidBrush(BackColor);

image with no caption

Dim secondBrush = New SolidBrush(BackColor)

Begin by deleting the second hand. To do so, you must determine the position in which to draw the second hand. Here you'll use an interesting approach. Rather than computing the x,y location of the second hand, assume that the second hand is always at 12 o'clock. How can this work? The answer is to rotate the world around the center of the clock face.

Picture a simple clock face with an x,y grid superimposed on it, as shown in Figure 10-8.

Drawing the clock face

Figure 10-8. Drawing the clock face

One way to draw a second hand at 2 o'clock is to compute the x,y coordinates of 2 o'clock (as you did when drawing the clock face). An alternative approach is to rotate the clock the appropriate number of degrees, and then draw the second hand straight up.

One way to think about this is to picture the clock face and a ruler, as shown in Figure 10-9. You can move the ruler to the right angle, or you can keep the ruler straight up and down and rotate the clock face under it. In the next example, use this second technique to draw the hands of the clock.

Paper and ruler

Figure 10-9. Paper and ruler

Create a method GetSecondRotation( ) to return a floating-point number, indicating how much the "paper" should be turned.

image with no caption

float rotation = GetSecondRotation(  );

image with no caption

Dim rotation As Single
rotation = GetSecondRotation(  )

The helper method GetSecondRotation uses the current time member field. Notice that the currentTime field has not yet been updated, so it has the same "current time" that you had when you drew the hands.

Divide the current second by 60 (60 seconds per minute), and then multiply by 360 (360 degrees in a circle). For example, at 15 seconds past the minute, GetSecondRotation( ) will return 90 because 360 * 15 / 60 = 90.

image with no caption

private float GetSecondRotation(  )
{
   return(360f * currentTime.Second / 60f);
}

image with no caption

Private Function GetSecondRotation(  ) As Single
    Return 360.0F * currentTime.Second / 60.0F
End Function 'GetSecondRotation

RotateTransform

You now know how much you want to rotate the world (i.e., rotate the paper under the ruler) to draw the second hand. The steps are:

  1. Save the current state of the Graphics object

  2. Rotate the world

  3. Draw the second hand

  4. Restore the state of the Graphics object

It is as if you spin your paper, draw the dot, and then spit it back to the way it was. The code snippet you need to accomplish this is (the VB.NET is virtually identical):

image with no caption

state = g.Save(  );
g.RotateTransform(rotation);
//...do stuff here
g.Restore(state);

The transform method for rotating the world is called RotateTransform, and it takes a single argument: the number of degrees to rotate.

FillElipse

The method you'll use to draw the dot representing the second hand is FillElipse. This method of the Graphics object is overloaded; the version used here takes five parameters:

  • The brush that will determine the color and texture of the ellipse

  • The x coordinate of the upper-lefthand corner of the bounding rectangle

  • The y coordinate of the upper-lefthand corner of the bounding rectangle

  • The width of the bounding rectangle

  • The height of the bounding rectangle

You'll use the brush you created earlier, named secondBrush. When you are deleting, secondBrush will be set to the background color. When you are drawing the second hand, it will be set to green.

The x and y coordinates of the second hand are determined so that the second hand is straight up from the origin, centered on the y axis (remember, you've turned the paper under the ruler. Now you should draw along the ruler).

The y coordinate is easy; you'll use the constant you've defined for the length of the second hand. Remember, however, that in this world, the y coordinates are negative above the origin, and since you want to draw straight up to 12 o'clock, you must use a negative value.

The x coordinate is just a bit trickier. The premise was that you'd just draw straight up, along the y axis. Unfortunately, this will place the upper-lefthand corner of the bounding rectangle along the y axis, and you'll want to center the ellipse on the y axis. You thus pass an x coordinate that is half the size of the bounding rectangle (e.g., 25) and set that negative so that the ball will be centered on the y axis.

Since you want your ellipse to be circular, the bounding rectangle will be square, with each side set to 50:

image with no caption

const int EllipseSize = 50;
state = g.Save(  );
rotation = GetSecondRotation(  );
g.RotateTransform(rotation);
g.FillEllipse(
   secondBrush,
   -(EllipseSize/2),
   -secondLength,
   EllipseSize,
   EllipseSize);
g.Restore(state);

image with no caption

Const EllipseSize As Single = 50
state = g.Save(  )
rotation = GetSecondRotation(  )
g.RotateTransform(rotation)
g.FillEllipse( _
    secondBrush, _
    -(EllipseSize / 2), _
    -secondLength, _
    EllipseSize, _
    EllipseSize)
g.Restore(state)

Having drawn the second hand, go on to draw the minute and hour hand. If you redraw them both every second, however, the clock face flickers annoyingly. Therefore, redraw these two hands only if the minute has changed. To test this, compare the new time with the old time and determine whether the minute value has changed:

image with no caption

DateTime newTime = DateTime.Now;
bool newMin = false;   // has the minute changed?
   
if ( newTime.Minute != currentTime.Minute )
   newMin = true;

image with no caption

Dim newTime As DateTime = DateTime.Now
Dim newMin As Boolean = False ' has the minute changed?
   
If newTime.Minute <> currentTime.Minute Then
    newMin = True
End If

You can then test the newMin Boolean value before updating the minute and hour hands:

image with no caption

if ( newMin  || forceDraw )
{
   // draw the minute and hour hands
}

image with no caption

If newMin Or forceDraw Then
    ' draw the minute and hour hands
End If

The test is that either the minute has changed or the forceDraw parameter passed into the DrawTime method is true. This allows onPaint to ensure that the hands are drawn on a repaint by calling DrawTime and passing in true for the Boolean value.

The implementation of drawing the minute and hour hands is nearly identical to that for drawing the second hand. This time, however, rather than drawing an ellipse, you actually draw a line. You do so with the DrawLine method of the Graphics object, passing in a pen and four integer values.

The first two values represent the x,y coordinates of the origin of the line, and the second set of two values represent the x,y coordinates of the end of the line. In each case, the origin of the line will be the center of the clock face, 0,0. The x coordinate of the end of the line will be 0 because you'll draw along the y axis. The y coordinate of the end of the line will be the length of the hour hand. Once again, because the y coordinates are negative above the origin, you'll pass it as a negative number.

The length of the hour and minute hands are defined at the top of the method, as is the distance from the origin for the ellipse representing the second hand:

float hourLength = FaceRadius * 0.5f;
float minuteLength = FaceRadius * 0.7f;
float secondLength = FaceRadius * 0.9f;

Tip

You may notice that you are drawing the line along the y axis (as you might run a pen along a ruler) rather than centered on the y axis. This keeps the code a bit simpler, but you are free to determine the width of the line and then to offset the drawing by that amount. This is left as an exercise for the obsessive-compulsive reader.

If the minute has advanced (or if forceDraw is true), you will determine the rotation for the minute, save the state of the Graphics object, rotate the world, draw the line, and restore the state of the Graphics object. You can then do the same thing for the hour hand:

image with no caption

if ( newMin  || forceDraw )
{
   rotation = GetMinuteRotation(  );
   state = g.Save(  );
   g.RotateTransform(rotation);
   g.DrawLine(minutePen,0,0,0,-minuteLength);
   g.Restore(state);
   
   rotation = GetHourRotation(  );
   state = g.Save(  );
   g.RotateTransform(rotation);
   g.DrawLine(hourPen,0,0,0,-hourLength);
   g.Restore(state);
}

image with no caption

If newMin Or forceDraw Then
    rotation = GetMinuteRotation(  )
    state = g.Save(  )
    g.RotateTransform(rotation)
    g.DrawLine(minutePen, 0, 0, 0, -minuteLength)
    g.Restore(state)
   
    rotation = GetHourRotation(  )
    state = g.Save(  )
    g.RotateTransform(rotation)
    g.DrawLine(hourPen, 0, 0, 0, -hourLength)
    g.Restore(state)
End If

The two helper methods, GetMinuteRotation and GetHourRotation, simply determine the degrees to rotate the world for the current minute and hour. GetMinuteRotation is simple, it multiplies the 360 degrees of the clock by the current minute and divides by 60 (60 minutes in an hour):

image with no caption

private float GetMinuteRotation(  )
{
   return( 360f * currentTime.Minute / 60f );
}

image with no caption

Private Function GetMinuteRotation(  ) As Single
    Return 360.0F * currentTime.Minute / 60.0F
End Function

The GetHourRotation method is more complicated because in this version you may have set the face to 24 hour mode, and the angle for the hour hand will be different if there are 24 hours around the clock face rather than 12.

Each hour will be 30 degrees from the previous hour if the clock face has 12 hours, or 15 degrees if the clock face has 24. To get the angle for the hour, multiply 360 by the current hour and divide by the number of hours (12 or 24) on the clock face.

You should also move the hour hand a bit more to allow for the number of minutes past the hour. For example, at 12:30 the hour hand should be halfway between the 12 and the 1.

To accomplish this adjustment, add another rotation computed by multiplying the number of degrees between hours (15 or 30) by the current number of minutes past the hour and dividing by 60:

image with no caption

private float GetHourRotation(  )
{
   float deg = b24Hours ? 15 : 30;
   float numHours = b24Hours ? 24 : 12;
   return( 360f * currentTime.Hour / numHours +
      deg * currentTime.Minute / 60f);
}

image with no caption

Private Function GetHourRotation(  ) As Single
   Dim deg As Single
    Dim numHours As Single
    If b24Hours Then
        deg = 15
        numHours = 24
    Else
        deg = 30
        numHours = 12
    End If
   
    Return 360.0F * currentTime.Hour / _
      numHours + deg * currentTime.Minute / 60.0F
End Function 'GetHourRotation

Drawing the new time

Once you've done all the work shown so far, you've drawn the second hand, the minute hand, and the hour hand in the background color, effectively erasing them. Next, set the currentTime variable to the new time, and set the pen and brush colors to the colors you want to draw:

image with no caption

currentTime = newTime
   
hourPen.Color = Color.Red
minutePen.Color = Color.Blue
secondPen.Color = Color.Green
secondBrush = New SolidBrush(Color.Green)

You are now ready to redraw these hands using the same technique shown above: save the state, rotate, draw the hand, and restore the state.

Note

Notice the use of the Boolean variable newMin. Here's why it is required.

Imagine that you test the time when you are ready to erase the hands, but it is not a new minute. You thus do not erase the minute and hour hands, but test the time again when it is time to draw the hands with their correct colors. You might have just passed the minute mark, and now the minute values for current time and new time would be different, and you would draw the new hands without having erased them first. Suddenly the minute and hour hands get fatter.

You can avoid this bug by setting the newMin Boolean variable before erasing, and then using that Boolean when redrawing.

Implementing the 24 hour clock button

The event handler for the 24 hour clock button is straightforward: it toggles the b24Hour Boolean member variable and toggles the text. Finally, it invalidates the form so the clock is redrawn:

image with no caption

private void btnClockFormat_Click(object sender, System.EventArgs e)
{
   btnClockFormat.Text = b24Hours ? "24 Hour" : "12 Hour";
   b24Hours = ! b24Hours;
   this.Invalidate(  );
}

image with no caption

Private Sub btnClockFormat_Click( _
   ByVal sender As System.Object, _
   ByVal e As System.EventArgs) _
   Handles btnClockFormat.Click
   
    If b24Hours Then
        btnClockFormat.Text = "24 Hours"
        b24Hours = False
    Else
        btnClockFormat.Text = "12 Hours"
        b24Hours = True
    End If
   
    Me.Invalidate(  )
   
End Sub

The only remaining change you need to make to the code is to update the DrawFace method to draw either the 24 hour or the 12 hour clock face:

image with no caption

private void DrawFace(Graphics g)
{
   Brush brush = new SolidBrush(ForeColor);
   Font font = new Font("Arial", 40);
   float x, y;
   
   int numHours = b24Hours ? 24 : 12;
   int deg = 360 / numHours;
   
   for (int i = 1; i <= numHours; i++)
   {
      x = GetCos(i*deg + 90) * FaceRadius;
      y = GetSin(i*deg + 90) * FaceRadius;
   
      StringFormat format = new StringFormat(  );
      format.Alignment = StringAlignment.Center;
      format.LineAlignment = StringAlignment.Center;
   
      g.DrawString(
         i.ToString(  ), font, brush, -x, -y,format);
   
   }   
}

image with no caption

Private Sub DrawFace(ByVal g As Graphics)
    Dim brush = New SolidBrush(ForeColor)
    Dim font As New Font("Arial", 40)
    Dim x, y As Single
   
    Dim numHours As Integer
    If b24Hours Then
        numHours = 24
    Else
        numHours = 12
    End If
    Dim deg As Integer = 360 / numHours
    Const FaceRadius As Integer = 450
   
    ' for each of the hours on the clock face
    Dim i As Integer
    For i = 1 To numHours
        x = GetCos((i * deg + 90)) * FaceRadius
        y = GetSin((i * deg + 90)) * FaceRadius
   
        Dim format As New StringFormat(  )
        format.Alignment = StringAlignment.Center
        format.LineAlignment = StringAlignment.Center
   
        g.DrawString(i.ToString(  ), font, brush, -x, -y, format)
    Next i
End Sub 'DrawFace

The new code is shown in bold. The trick is to set the numHours variable to 12 or 24, based on the value of the member variable b24Hours. You then set the deg variable based on dividing the 360 degrees in the circle by the number of hours you are showing on the clock face. Then compute the Sin and Cosine value accordingly.

Drawing the Animated Date

In the third and final version of the program, you will add code to draw the date around the clock face and animate it. While you're at it, you'll also let the user click on the form to create a new center: by moving the clock's center to the location of the mouse when the user left-clicks. The complete source is shown in Example 10-11 and Example 10-12.

Example 10-11. Final version clock face (CS)

image with no caption

using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Timers;
using System.Windows.Forms;
   
   
namespace Clock3CS
{
   // Rename the class
   public class ClockFace : System.Windows.Forms.Form
   {
      // Required designer variable.
      private System.ComponentModel.Container components = null;
   
      private int FaceRadius = 450;      // size of the clock face
      private bool b24Hours = false;      // 24 hour clock face?
      private System.Windows.Forms.Button btnClockFormat;      
      private DateTime currentTime;      // used in more than one method
   
      // new
      private int xCenter;            // center of the clock
      private int yCenter;
      private static int DateRadius = 600; // outer circumference for date
      private static int Offset = 0;      // for moving the text 
      Font font = new Font("Arial", 40);   // use the same font throughout
      private StringDraw sdToday;         // the text to animate
   
      public ClockFace(  )
      {
         // Required for Windows Form Designer support
         InitializeComponent(  );
   
         // use the user's choice of colors
         BackColor = SystemColors.Window;
         ForeColor = SystemColors.WindowText;
   
      
         // *** begin new
         string today = System.DateTime.Now.ToLongDateString(  );
         today = " " + today.Replace(",","");
         
         // create a new stringdraw object with today's date
         sdToday = new StringDraw(today,this);
         currentTime = DateTime.Now;
   
   
         // set the current center based on the
         // client area
         xCenter = Width / 2;
         yCenter = Height / 2;
   
   
         // *** end new
   
   
         // update the clock by timer
         System.Timers.Timer timer = new System.Timers.Timer(  );
         timer.Elapsed += new System.Timers.ElapsedEventHandler(OnTimer);
         timer.Interval = 5;  // shorter interval - more movement
         timer.Enabled = true;
   
   
      }
   
      protected override void OnPaint ( PaintEventArgs e )
      {
         base.OnPaint(e);
         Graphics g = e.Graphics;
         SetScale(g);
         DrawFace(g);
         DrawTime(g,true);   // force an update
      }
   
      // every time the timer event fires, update the clock
      public void OnTimer(Object source, ElapsedEventArgs e)
      {
         Graphics g = this.CreateGraphics(  );
   
         SetScale(g);
         DrawFace(g);
         DrawTime(g,false);
         DrawDate(g);
         g.Dispose(  );      
         
      }
   
   
      #region Windows Form Designer generated code
      #endregion
   
      [STAThread]
      static void Main(  ) 
      {
         Application.Run(new ClockFace(  ));
      }
   
      private void SetScale(Graphics g)
      {
         // if the form is too small, do nothing
         if ( Width =  = 0 || Height =  = 0 )
            return;
   
         // set the origin at the center
         g.TranslateTransform(xCenter, yCenter);  // use the members vars
   
         // set inches to the minimum of the width 
         // or height dividedby the dots per inch  
         float inches = Math.Min(Width / g.DpiX, Height / g.DpiX);
   
         // set the scale to a grid of 2000 by 2000 units
         g.ScaleTransform(
            inches * g.DpiX / 2000, inches * g.DpiY / 2000);
      }
   
      private void DrawFace(Graphics g)
      {
         // numbers are in forecolor except flash number in green
         // as the seconds go by.
         Brush brush = new SolidBrush(ForeColor);
         float x, y;
   
         // new code
         int numHours = b24Hours ? 24 : 12;
         int deg = 360 / numHours;
         
         // for each of the hours on the clock face
         for (int i = 1; i <= numHours; i++)
         {
            // i = hour  30 degrees = offset per hour  
            // +90 to make 12 straight up
            x = GetCos(i*deg + 90) * FaceRadius;
            y = GetSin(i*deg + 90) * FaceRadius;
   
            StringFormat format = new StringFormat(  );
            format.Alignment = StringAlignment.Center;
            format.LineAlignment = StringAlignment.Center;
   
            g.DrawString(
               i.ToString(  ), font, brush, -x, -y,format);
         
         }   // end for loop
      }      // end drawFace
   
   
      private void DrawTime(Graphics g, bool forceDraw)
      {
   
         //  length of the hands
         float hourLength = FaceRadius * 0.5f;
         float minuteLength = FaceRadius * 0.7f;
         float secondLength = FaceRadius * 0.9f;
   
         // set to back color to erase old hands first
         Pen hourPen = new Pen(BackColor);
         Pen minutePen = new Pen(BackColor);
         Pen secondPen = new Pen(BackColor);
   
         // set the arrow heads
         hourPen.EndCap = LineCap.ArrowAnchor;
         minutePen.EndCap = LineCap.ArrowAnchor;
   
         // hour hand is thicker
         hourPen.Width = 30;
         minutePen.Width = 20;
   
         // second hand 
         Brush secondBrush = new SolidBrush(BackColor);
         const int EllipseSize = 50;
   
         GraphicsState state;   // to to protect and to serve
   
   
         // 1 - delete the old time
   
         // delete the old second hand
         // figure out how far around to rotate to draw the second hand
         // save the current state, rotate, draw and then restore the state
         float rotation = GetSecondRotation(  );
         state = g.Save(  );
         g.RotateTransform(rotation);
         g.FillEllipse(
            secondBrush,
            -(EllipseSize/2),
            -secondLength,
            EllipseSize,
            EllipseSize);
         g.Restore(state);
   
         DateTime newTime = DateTime.Now;
         bool newMin = false;   // has the minute changed?
   
         // if the minute has changed, set the flag
         if ( newTime.Minute != currentTime.Minute )
            newMin = true;
   
   
         // if the minute has changed or you must draw anyway then you 
         // must first delete the old minute and hour hand
         if ( newMin  || forceDraw )
         {
   
            // figure out how far around to rotate to draw the minute hand
            // save the current state, rotate, draw and 
            // then restore the state
            rotation = GetMinuteRotation(  );
            state = g.Save(  );
            g.RotateTransform(rotation);
            g.DrawLine(minutePen,0,0,0,-minuteLength);
            g.Restore(state);
   
            // figure out how far around to rotate to draw the hour hand
            // save the current state, rotate, draw and 
            // then restore the state
            rotation = GetHourRotation(  );
            state = g.Save(  );
            g.RotateTransform(rotation);
            g.DrawLine(hourPen,0,0,0,-hourLength);
            g.Restore(state);
         }
   
         // step 2 - draw the new time
         currentTime = newTime;
   
         hourPen.Color = Color.Red;
         minutePen.Color = Color.Blue;
         secondPen.Color = Color.Green;
         secondBrush = new SolidBrush(Color.Green);
   
         // draw the new second hand
         // figure out how far around to rotate to draw the second hand
         // save the current state, rotate, draw and then restore the state
         state = g.Save(  );
         rotation = GetSecondRotation(  );
         g.RotateTransform(rotation);
         g.FillEllipse(
            secondBrush,
            -(EllipseSize/2),
            -secondLength,
            EllipseSize,
            EllipseSize);
         g.Restore(state);
   
         // if the minute has changed or you must draw anyway then you 
         // must draw the new minute and hour hand
         if ( newMin || forceDraw )
         {
   
            // figure out how far around to rotate to draw the minute hand
            // save the current state, rotate, draw and 
            // then restore the state
            state = g.Save(  );
            rotation = GetMinuteRotation(  );
            g.RotateTransform(rotation);
            g.DrawLine(minutePen,0,0,0,-minuteLength);
            g.Restore(state);
   
            // figure out how far around to rotate to draw the hour hand
            // save the current state, rotate, draw and 
            // then restore the state
            state = g.Save(  );
            rotation = GetHourRotation(  );
            g.RotateTransform(rotation);
            g.DrawLine(hourPen,0,0,0,-hourLength);
            g.Restore(state);
         }
      }
   
      // determine the rotation to draw the hour hand
      private float GetHourRotation(  )
      {
         // degrees depend on 24 vs. 12 hour clock
         float deg = b24Hours ? 15 : 30;
         float numHours = b24Hours ? 24 : 12;
         return( 360f * currentTime.Hour / numHours +
            deg * currentTime.Minute / 60f);
      }
   
      private float GetMinuteRotation(  )
      {
         return( 360f * currentTime.Minute / 60f ); 
      }
   
      private float GetSecondRotation(  )
      {
         return(360f * currentTime.Second / 60f);
      }
   
      private static float GetSin(float degAngle)
      {
         return (float) Math.Sin(Math.PI * degAngle / 180f);
      }
   
      private static float GetCos(float degAngle)
      {
         return (float) Math.Cos(Math.PI * degAngle / 180f);
      }
   
      private void btnClockFormat_Click(object sender, System.EventArgs e)
      {
         btnClockFormat.Text = b24Hours ? "24 Hour" : "12 Hour";
         b24Hours = ! b24Hours;
         this.Invalidate(  );
      }
   
      private void DrawDate(Graphics g)
      {
         Brush brush = new SolidBrush(ForeColor);
         sdToday.DrawString(g,brush);
      }
   
      private void ClockFace_MouseDown(
         object sender, System.Windows.Forms.MouseEventArgs e)
      {
         xCenter = e.X;
         yCenter = e.Y;
         this.Invalidate(  );
      
      }
   
   
      // each letter in the outer string knows how to draw itself
      private class LtrDraw
      {
         char myChar;      // the actual letter i draw
         float x;         // current x coordinate
         float y;         // current y coordinate
         float oldx;         // old x coordinate (to delete)
         float oldy;         // old y coordinate (to delete)
         
   
         // constructor
         public LtrDraw(char c)
         {
            myChar = c;
         }
   
         // property for X coordinate
         public float X
         {
            get { return x; }
            set { oldx = x; x = value; }
         }
   
         // property for Y coordinate
         public float Y
         {
            get { return y; }
            set { oldy = y; y = value; }
         }
   
         // get total width of the string
         public float GetWidth(Graphics g, Font font)
         {
            SizeF stringSize = g.MeasureString(myChar.ToString(  ),font);
            return stringSize.Width;
         }
   
         // get total height of the string
         public float GetHeight(Graphics g, Font font)
         {
            SizeF stringSize = g.MeasureString(myChar.ToString(  ),font);
            return stringSize.Height;
         }
   
   
         // get the font from the control and draw the current character
         // First delete the old and then draw the new
         public void DrawString(Graphics g, Brush brush, ClockFace cf)
         {
            Font font = cf.font;
            Brush blankBrush = new SolidBrush(cf.BackColor);
            g.DrawString(myChar.ToString(  ),font,blankBrush,oldx,oldy);
            g.DrawString(myChar.ToString(  ),font,brush,x,y);
         }
   
      }
   
      // holds an array of LtrDraw objects
      // and knows how to tell them to draw
      private class StringDraw
      {
         ArrayList theString = new ArrayList(  );
         LtrDraw l;
         ClockFace theControl;
   
         // constructor takes a string, populates the array
         // and stashes away the calling control (ClockFace)
         public StringDraw(string s, ClockFace theControl)
         {
            this.theControl = theControl;
            foreach (char c in s)
            {
               l = new LtrDraw(c);
               theString.Add(l);
            }
         }
   
         // divide the circle by the number of letters
         // and draw each letter in position
         public void DrawString(Graphics g, Brush brush)
         {
            int angle = 360 / theString.Count;
            int counter = 0;
   
            foreach (LtrDraw theLtr in theString)
            {
               // 1. To find the X coordinate, take the Cosine of the angle
               // and multiply by the radius.
               // 2. To compute the angle, start with the base angle 
               // (360 divided by the number of letters)
               // and multiply by letter position.
               // Thus if each letter is 10 degrees, and this is the third
               // letter, you get 30 degrees. 
               // Add 90 to start at 12 O'clock.
               // Each time through, subtract the clockFace offset to move 
               // the entire string around the clock on each timer call
               float newX = GetCos(
                   angle  * counter + 90 - 
                        ClockFace.Offset) * ClockFace.DateRadius ;
               float newY = GetSin(
                     angle * counter + 90 - 
                     ClockFace.Offset) * ClockFace.DateRadius ;
               theLtr.X = 
                  newX - (theLtr.GetWidth(g,theControl.font) / 2);
               theLtr.Y = 
                  newY - (theLtr.GetHeight(g,theControl.font) / 2);
               counter++;
               theLtr.DrawString(g,brush,theControl);
            }
            ClockFace.Offset += 1;  // rotate the entire string
         }
      }
   }   // end class
}      // end namespace

Example 10-12. Final version clock face (VB.NET)

image with no caption

Imports System
Imports System.Collections
Imports System.ComponentModel
Imports System.Data
Imports System.Drawing
Imports System.Drawing.Drawing2D
Imports System.Timers
Imports System.Windows.Forms
   
Namespace Clock3VB
   
    Public Class ClockFace
        Inherits System.Windows.Forms.Form
   
        Private FaceRadius As Integer = 450 ' size of the clock face
        Private b24Hours As Boolean = False ' 24 hour clock face?
        Private currentTime As DateTime ' used in more than one method
        ' new
        Private xCenter As Integer ' center of the clock
        Private yCenter As Integer
        ' outer circumference for date
        Private Shared DateRadius As Integer = 600         
        Private Shared offset As Integer = 0 ' for moving the text 
        ' use the same font throughout
        Private myFont As New font("Arial", 40)
        Private sdToday As StringDraw
   
        Public Sub New(  )
            ' Required for Windows Form Designer support
            InitializeComponent(  )
   
            ' use the user's choice of colors
            BackColor = SystemColors.Window
            ForeColor = SystemColors.WindowText
   
   
            ' *** begin new code
            Dim today As String = System.DateTime.Now.ToLongDateString(  )
            today = " " + today.Replace(",", "")
   
            ' create a new stringdraw object with today's date
            sdToday = New StringDraw(today, Me)
            currentTime = DateTime.Now
   
   
            ' set the current center based on the
            ' client area
            xCenter = Width / 2
            yCenter = Height / 2
   
   
            ' *** end new code
   
            ' update the clock by timer
            Dim timer As New System.Timers.Timer(  )
            AddHandler timer.Elapsed, AddressOf OnTimer
            timer.Interval = 5 ' shorter interval - more movement
            timer.Enabled = True
        End Sub 'New
   
   
   
   
   
        ' every time the timer event fires, update the clock
        Public Sub OnTimer( _
          ByVal source As Object, ByVal e As ElapsedEventArgs)
            Dim g As Graphics = Me.CreateGraphics(  )
   
            SetScale(g)
            DrawFace(g)
            DrawTime(g, False)
            DrawDate(g)
            g.Dispose(  )
        End Sub 'OnTimer
   
#Region " Windows Form Designer generated code "
   
#End Region
        Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs)
            myBase.OnPaint(e)
            Dim g As Graphics = e.Graphics
            SetScale(g)
            DrawFace(g)
            DrawTime(g, True) ' force an update
        End Sub 'OnPaint
   
        Private Sub SetScale(ByVal g As Graphics)
            ' if the form is too small, do nothing
            If Width = 0 Or Height = 0 Then
                Return
            End If
            ' set the origin at the center
            g.TranslateTransform(xCenter, yCenter) ' use the members vars
            ' set inches to the minimum of the width 
            ' or height dividedby the dots per inch  
            Dim inches As Single = _ 
               Math.Min(Width / g.DpiX, Height / g.DpiX)
   
            ' set the scale to a grid of 2000 by 2000 units
            g.ScaleTransform( _
              inches * g.DpiX / 2000, inches * g.DpiY / 2000)
        End Sub 'SetScale
   
   
        Private Sub DrawFace(ByVal g As Graphics)
            ' numbers are in forecolor except flash number in green
            ' as the seconds go by.
            Dim brush = New SolidBrush(ForeColor)
            Dim x, y As Single
   
            ' new code
   
            Dim numHours As Integer
            If (b24Hours) Then
                numHours = 24
            Else
                numHours = 12
            End If
   
            Dim deg As Integer = 360 / numHours
   
            ' for each of the hours on the clock face
            Dim i As Integer
            For i = 1 To numHours
                ' i = hour  30 degrees = offset per hour  
                ' +90 to make 12 straight up
                x = GetCos((i * deg + 90)) * FaceRadius
                y = GetSin((i * deg + 90)) * FaceRadius
   
                Dim format As New StringFormat(  )
                format.Alignment = StringAlignment.Center
                format.LineAlignment = StringAlignment.Center
   
                g.DrawString(i.ToString(  ), myFont, brush, -x, -y, format)
            Next i
        End Sub 'DrawFace
   
        ' end for loop
        ' end drawFace
   
        Private Sub DrawTime( _
            ByVal g As Graphics, ByVal forceDraw As Boolean)
   
            '  length of the hands
            Dim hourLength As Single = FaceRadius * 0.5F
            Dim minuteLength As Single = FaceRadius * 0.7F
            Dim secondLength As Single = FaceRadius * 0.9F
   
            ' set to back color to erase old hands first
            Dim hourPen As New Pen(BackColor)
            Dim minutePen As New Pen(BackColor)
            Dim secondPen As New Pen(BackColor)
   
            ' set the arrow heads
            hourPen.EndCap = LineCap.ArrowAnchor
            minutePen.EndCap = LineCap.ArrowAnchor
   
            ' hour hand is thicker
            hourPen.Width = 30
            minutePen.Width = 20
   
            ' second hand 
            Dim secondBrush = New SolidBrush(BackColor)
            Const EllipseSize As Integer = 50
            Dim halfEllipseSize As Integer = EllipseSize / 2
   
            Dim state As GraphicsState ' to to protect and to serve
   
            ' 1 - delete the old time
            ' delete the old second hand
            ' figure out how far around to rotate to draw the second hand
            ' save the current state, rotate, draw 
            ' and then restore the state
            Dim rotation As Single = GetSecondRotation(  )
            state = g.Save(  )
            g.RotateTransform(rotation)
   
            g.FillEllipse( _
                  secondBrush, -(halfEllipseSize), _
                  -secondLength, EllipseSize, EllipseSize)
            g.Restore(state)
   
            Dim newTime As DateTime = DateTime.Now
            Dim newMin As Boolean = False ' has the minute changed?
            ' if the minute has changed, set the flag
            If newTime.Minute <> currentTime.Minute Then
                newMin = True
            End If
   
            ' if the minute has changed or you must draw anyway then you 
            ' must first delete the old minute and hour hand
            If newMin Or forceDraw Then
   
                ' figure out how far around to rotate to 
                ' draw the minute hand
                ' save the current state, rotate, draw 
                ' and then restore the state
                rotation = GetMinuteRotation(  )
                state = g.Save(  )
                g.RotateTransform(rotation)
                g.DrawLine(minutePen, 0, 0, 0, -minuteLength)
                g.Restore(state)
   
                ' figure out how far around to rotate to draw the hour hand
                ' save the current state, rotate, draw 
                ' and then restore the state
                rotation = GetHourRotation(  )
                state = g.Save(  )
                g.RotateTransform(rotation)
                g.DrawLine(hourPen, 0, 0, 0, -hourLength)
                g.Restore(state)
            End If
   
            ' step 2 - draw the new time
            currentTime = newTime
   
            hourPen.Color = Color.Red
            minutePen.Color = Color.Blue
            secondPen.Color = Color.Green
            secondBrush = New SolidBrush(Color.Green)
   
            ' draw the new second hand
            ' figure out how far around to rotate to draw the second hand
            ' save the current state, rotate, draw 
            ' and then restore the state
            state = g.Save(  )
            rotation = GetSecondRotation(  )
            g.RotateTransform(rotation)
            g.FillEllipse( _
              secondBrush, -(halfEllipseSize), _
              -secondLength, EllipseSize, EllipseSize)
            g.Restore(state)
   
            ' if the minute has changed or you must draw anyway then you 
            ' must draw the new minute and hour hand
            If newMin Or forceDraw Then
   
                ' figure out how far around to rotate to 
                ' draw the minute hand
                ' save the current state, rotate, draw 
                ' and then restore the state
                state = g.Save(  )
                rotation = GetMinuteRotation(  )
                g.RotateTransform(rotation)
                g.DrawLine(minutePen, 0, 0, 0, -minuteLength)
                g.Restore(state)
   
                ' figure out how far around to rotate to draw the hour hand
                ' save the current state, rotate, draw 
                ' and then restore the state
                state = g.Save(  )
                rotation = GetHourRotation(  )
                g.RotateTransform(rotation)
                g.DrawLine(hourPen, 0, 0, 0, -hourLength)
                g.Restore(state)
            End If
        End Sub 'DrawTime
   
   
        ' determine the rotation to draw the hour hand
        Private Function GetHourRotation(  ) As Single
            ' degrees depend on 24 vs. 12 hour clock
            Dim deg As Single
            Dim numHours As Single
            If (b24Hours) Then
                deg = 15
                numHours = 24
            Else
                deg = 30
                numHours = 12
            End If
            Return 360.0F * currentTime.Hour / _
              numHours + deg * currentTime.Minute / 60.0F
        End Function 'GetHourRotation
   
   
        Private Function GetMinuteRotation(  ) As Single
            Return 360.0F * currentTime.Minute / 60.0F
        End Function 'GetMinuteRotation
   
   
        Private Function GetSecondRotation(  ) As Single
            Return 360.0F * currentTime.Second / 60.0F
        End Function 'GetSecondRotation
   
   
        Private Shared Function GetSin(ByVal degAngle As Single) As Single
            Return CSng(Math.Sin((Math.PI * degAngle / 180.0F)))
        End Function 'GetSin
   
   
        Private Shared Function GetCos(ByVal degAngle As Single) As Single
            Return CSng(Math.Cos((Math.PI * degAngle / 180.0F)))
        End Function 'GetCos
   
   
        Private Sub btnClockFormat_Click( _
             ByVal sender As System.Object, _
             ByVal e As System.EventArgs) _
             Handles btnClockFormat.Click
            If (b24Hours) Then
                btnClockFormat.Text = "24 Hour"
            Else
                btnClockFormat.Text = "12 Hour"
            End If
            b24Hours = Not b24Hours
            Me.Invalidate(  )
        End Sub 'btnClockFormat_Click
   
   
        Private Sub DrawDate(ByVal g As Graphics)
            Dim brush = New SolidBrush(ForeColor)
            sdToday.DrawString(g, brush)
        End Sub 'DrawDate
   
   
        Private Sub ClockFace_MouseDown( _
          ByVal sender As Object, _
          ByVal e As System.Windows.Forms.MouseEventArgs) _
          Handles MyBase.MouseDown
            xCenter = e.X
            yCenter = e.Y
            Me.Invalidate(  )
        End Sub 'ClockFace_MouseDown
       _ 
   
   
        ' each letter in the outer string knows how to draw itself
        Private Class LtrDraw
            Private myChar As Char ' the actual letter i draw
            Private _x As Single ' current x coordinate
            Private _y As Single ' current y coordinate
            Private oldx As Single ' old x coordinate (to delete)
            Private oldy As Single
            ' old y coordinate (to delete)
   
            ' constructor
            Public Sub New(ByVal c As Char)
                myChar = c
            End Sub 'New
   
            ' property for X coordinate
   
            Public Property X(  ) As Single
                Get
                    Return _x
                End Get
                Set(ByVal Value As Single)
                    oldx = _x
                    _x = Value
                End Set
            End Property
            ' property for Y coordinate
   
            Public Property Y(  ) As Single
                Get
                    Return _y
                End Get
                Set(ByVal Value As Single)
                    oldy = _y
                    _y = Value
                End Set
            End Property
   
            ' get total width of the string
            Public Function GetWidth( _
               ByVal g As Graphics, ByVal myFont As Font) As Single
                Dim stringSize As SizeF = _
                   g.MeasureString(myChar.ToString(  ), myFont)
                Return stringSize.Width
            End Function 'GetWidth
   
   
            ' get total height of the string
            Public Function GetHeight( _
                ByVal g As Graphics, ByVal myFont As Font) As Single
                Dim stringSize As SizeF = _
                   g.MeasureString(myChar.ToString(  ), myFont)
                Return stringSize.Height
            End Function 'GetHeight
   
   
   
            ' get the font from the control and draw the current character
            ' First delete the old and then draw the new
            Public Sub DrawString( _
              ByVal g As Graphics, ByVal brush As Brush, _
               ByVal ctrl As ClockFace)
                Dim myFont As Font = ctrl.myFont
                Dim blankBrush = New SolidBrush(ctrl.BackColor)
                g.DrawString( _
                   myChar.ToString(  ), myFont, blankBrush, oldx, oldy)
                g.DrawString(myChar.ToString(  ), myFont, brush, X, Y)
            End Sub 'DrawString
        End Class 'LtrDraw
       _ 
   
        ' holds an array of LtrDraw objects
        ' and knows how to tell them to draw
        Private Class StringDraw
            Private theString As New ArrayList(  )
            Private l As LtrDraw
            Private theControl As ClockFace
   
   
            ' constructor takes a string, populates the array
            ' and stashes away the calling control (ClockFace)
            Public Sub New( _
                ByVal s As String, ByVal theControl As ClockFace)
                Me.theControl = theControl
                Dim c As Char
                For Each c In s
                    l = New LtrDraw(c)
                    theString.Add(l)
                Next c
            End Sub 'New
   
   
            ' divide the circle by the number of letters
            ' and draw each letter in position
            Public Sub DrawString( _
                ByVal g As Graphics, ByVal brush As Brush)
                Dim angle As Integer = 360 / theString.Count
                Dim counter As Integer = 0
   
                Dim theLtr As LtrDraw
                For Each theLtr In theString
                    ' 1. To find the X coordinate, 
                    ' take the Cosine of the angle
                    ' and multiply by the radius.
                    ' 2. To compute the angle, start with the base angle 
                    ' (360 divided by the number of letters)
                    ' and multiply by letter position.
                    ' Thus if each letter is 10 degrees, 
                    ' and this is the third
                    ' letter, you get 30 degrees. 
                    ' Add 90 to start at 12 O'clock.
                    ' Each time through, subtract the clockFace 
                    ' offset to move the entire string around 
                    ' the clock on each timer call
                    Dim newX As Single = _
                       GetCos((angle * counter + 90 - ClockFace.offset)) _
                       * ClockFace.DateRadius
                    Dim newY As Single = _
                       GetSin((angle * counter + 90 - ClockFace.offset)) _
                       * ClockFace.DateRadius
                    theLtr.X = newX - _
                       theLtr.GetWidth(g, theControl.myFont) / 2
                    theLtr.Y = newY - _
                       theLtr.GetHeight(g, theControl.myFont) / 2
                    counter += 1
                    theLtr.DrawString(g, brush, theControl)
                Next theLtr
                ClockFace.offset += 1 ' rotate the entire string
            End Sub 'DrawString
        End Class 'StringDraw
    End Class 'ClockFace 
End Namespace 'Clock3CS ' end class

Animating the string

In the previous examples, you saw two ways to manage drawing text at a specific location. In the first, you determined the x,y coordinates and then used the DrawString method to draw the characters at that location (clock face). In the second, you rotated the world a set rotation, and then used DrawString to draw each text character to a specific location (e.g., centered on the y axis, a fixed distance from the origin, as seen when using DrawTime).

In the next example, however, you want the date to move around the clock face, and more importantly, you want the letters to act as cars on a Ferris Wheel, maintaining their up-down orientation as they rotate around the center.

The LtrDraw class

To accomplish this design goal, each letter in the date will be encapsulated by an instance of the LtrDraw class that you will define. The LtrDraw class will be used only by methods of ClockFace, so LtrDraw will be declared as a nested class within the ClockFace class.

image with no caption

public class ClockFace : System.Windows.Forms.Form

image with no caption

{
   //...
   private class LtrDraw
   {
      // ...
   }         // end nested class
}               // end outer class

This class will have, as member variables, both the character you want to draw and the x,y coordinates of where to draw it. In fact, the LtrDraw instance will know two sets of x,y coordinates: where the letter was (so you can erase the old letter) and where it is (so you can draw the letter in its new location):

image with no caption

private class LtrDraw
{
   char myChar;      
   float x;         
   float y;         
   float oldx;         
   float oldy;

image with no caption

Private Class LtrDraw
    Private myChar As Char 
    Private _x As Single  
    Private _y As Single  
    Private oldx As Single 
    Private oldy As Single

The LtrDraw constructor initializes the myChar member variable:

image with no caption

public LtrDraw(char c)
{
   myChar = c;
}

image with no caption

Public Sub New(ByVal c As Char)
    myChar = c
End Sub 'New

The x,y coordinates are accessed through properties. The get accessor just returns the member variable's value, but the set accessor first stores the current value in the oldx/oldy members:

image with no caption

public float X
{
   get { return x; }
   set { oldx = x; x = value; }
}
   
public float Y
{
   get { return y; }
   set { oldy = y; y = value; }
}

image with no caption

Public Property X(  ) As Single
    Get
        Return _x
    End Get
    Set(ByVal Value As Single)
        oldx = _x
        _x = Value
    End Set
End Property
   
Public Property Y(  ) As Single
    Get
        Return _y
    End Get
    Set(ByVal Value As Single)
        oldy = _y
        _y = Value
    End Set
End Property

The LtrDraw class also provides methods that return the letter's Width and Height. These two methods delegate the actual measurement to the MeasureString method of the Graphics object, passing in the character the object holds in the myChar member variable and the font that is passed in to the method:

image with no caption

public float GetWidth(Graphics g, Font font)
{
   SizeF stringSize = g.MeasureString(myChar.ToString(  ),font);
   return stringSize.Width;
}
public float GetHeight(Graphics g, Font font)
{
   SizeF stringSize = g.MeasureString(myChar.ToString(  ),font);
   return stringSize.Height;
}

image with no caption

Public Function GetWidth( _
   ByVal g As Graphics, ByVal myFont As Font) As Single
    Dim stringSize As SizeF = _
       g.MeasureString(myChar.ToString(  ), myFont)
    Return stringSize.Width
End Function 'GetWidth
   
   
' get total height of the string
Public Function GetHeight( _
    ByVal g As Graphics, ByVal myFont As Font) As Single
    Dim stringSize As SizeF = _
       g.MeasureString(myChar.ToString(  ), myFont)
    Return stringSize.Height
End Function 'GetHeight

Finally, the LtrDraw class knows how to draw the letter via the DrawString method, given a Brush and a reference to the ClockFace object:

image with no caption

public void DrawString(Graphics g, Brush brush, ClockFace cf)
{

The first task is to get a reference to the font held by the ClockFace as a member variable:

image with no caption

   Font font = cf.font;

Next, create a blank brush and use it to delete the character from its old position:

image with no caption

   Brush blankBrush = new SolidBrush(cf.BackColor);
   g.DrawString(myChar.ToString(  ),font,blankBrush,oldx,oldy);

Finally, you are ready to draw the character in the new position, using the font you've extracted from the ClockFace and the brush you were given:

image with no caption

   g.DrawString(myChar.ToString(  ),font,brush,x,y);
}

image with no caption

Public Sub DrawString( _
  ByVal g As Graphics, ByVal brush As Brush, ByVal ctrl As ClockFace)
    Dim myFont As Font = ctrl.myFont
    Dim blankBrush = New SolidBrush(ctrl.BackColor)
    g.DrawString(myChar.ToString(  ), myFont, blankBrush, oldx, oldy)
   g.DrawString(myChar.ToString(  ), myFont, brush, X, Y)
End Sub 'DrawString

The StringDraw class

The LtrDraw class encapsulates a single letter. For the entire string, create a collection class to hold an array of LtrDraw objects. The StringDraw class uses an ArrayList to allow you to build up an array of LtrDraw objects and it holds a reference to the ClockFace object. StringDraw will be a nested class within ClockFace as well:

image with no caption

private class StringDraw
{
   ArrayList theString = new ArrayList(  );
   LtrDraw l;
   ClockFace theControl;

image with no caption

Private Class StringDraw
    Private theString As New ArrayList(  )
    Private l As LtrDraw
    Private theControl As ClockFace

Use the member variable l, the reference to a LtrDraw object, in the constructor to create instances of LtrDraw that you can add to the collection:

image with no caption

public StringDraw(string s, ClockFace theControl)
{
   this.theControl = theControl;
   foreach (char c in s)
   {
      l = new LtrDraw(c);
      theString.Add(l);
   }
}

image with no caption

Public Sub New(ByVal s As String, ByVal theControl As ClockFace)
    Me.theControl = theControl
    Dim c As Char
    For Each c In s
        l = New LtrDraw(c)
        theString.Add(l)
    Next c
End Sub 'New

You are passed a string and a reference to a ClockFace object. Stash the reference in the member variable theControl. Then treat the string as an array of characters, and iterate through the array using the foreach (for each) construct. For each letter you retrieve from the string, create an instance of the LtrDraw class, and then add that instance to the ArrayList member.

Tip

Reusing the LtrDraw reference (l) is safe because a reference to the new object is kept in the ArrayList.

The only method in the StringDraw class is cleverly named DrawString. This method takes two arguments: a Graphics object and a Brush.

image with no caption

public void DrawString(Graphics g, Brush brush)
{

image with no caption

Public Sub DrawString(ByVal g As Graphics, ByVal brush As Brush)

This method first sets the angle by which each letter will be separated. Ask the string for the count of characters and use that value to divide the 360 degrees of the circle into equal increments:

image with no caption

int angle = 360 / theString.Count;

image with no caption

Dim angle As Integer = 360 / theString.Count

Your job now is to iterate through the members of the ArrayList. For each LtrDraw object, compute the new x and y coordinates.

Do so by multiplying the angle value computed above by what amounts to the i-based index of the letter (that is, 1 for the second letter, 2 for the third, and so forth). Then add 90 to start the string at 12 o'clock (this is not strictly necessary, since the string will rotate around the clock face). Take the cosine of this value (using your old friend GetCos, which converts the angle to radians and then returns the cosine of that angle), and multiply by the constant DateRadius defined in the ClockFace class:

image with no caption

float newX =
  GetCos(angle  * counter + 90) * ClockFace.DateRadius ;

To make the string move, however, you have one more task. In the ClockFace class, declare a static (shared) member variable named offset. Modify your computation of the angle to subtract this value from the computed angle:

image with no caption

float newX =
  GetCos(angle  * counter + 90 - ClockFace.Offset) * ClockFace.DateRadius ;

Each time this method is invoked, you'll increment the offset value so that each time you run this method, the string will be drawn using an angle one degree less than the previous time.

You can compute the new y coordinate in much the same way:

image with no caption

float newY =
   GetSin(angle * counter + 90 - ClockFace.Offset) * ClockFace.DateRadius ;

image with no caption

Dim newX As Single = _
   GetCos((angle * counter + 90 - ClockFace.offset)) _
   * ClockFace.DateRadius
Dim newY As Single = _
   GetSin((angle * counter + 90 - ClockFace.offset)) _
   * ClockFace.DateRadius

Once again, however, you've computed the upper-lefthand corner of the bounding rectangle for the character you are going to draw. To center the character at this location, you must compute the width and height of the character and adjust your coordinates accordingly:

image with no caption

theLtr.X =
   

image with no caption

newX - (theLtr.GetWidth(g,theControl.font) / 2);
theLtr.Y = 
   newY - (theLtr.GetHeight(g,theControl.font) / 2);

That accomplished, increment the counter:

image with no caption

counter++;

image with no caption

counter += 1

and you are ready to tell the LtrDraw object to draw itself:

theLtr.DrawString(g,brush,theControl);

Once the loop is completed, increment the static Offset member of the ClockFace:

image with no caption

ClockFace.Offset += 1;

To encourage the date to move around the clock face quickly and smoothly, change the timer interval from 500 milliseconds to 50 milliseconds. Do this in the constructor, where you'll make a few other changes as well, shown below.

New member variables

Before examining the constructor, you'll need to add six new member variables.

The xCenter and yCenter variables will hold the x and y coordinates of the center of the clock.

image with no caption

private int xCenter;
private int yCenter;

You previously computed these values by dividing the width and height of the form by 2 (dividing in half), and that is how you'll compute the initial values for xCenter and yCenter as well, as you'll see in the new code in the constructor, below.

You'll add a new static value for the radius of the date string and add the static value Offset, discussed above:

image with no caption

private static int DateRadius = 600;
private static int Offset = 0;

Because you want to use the same font in many places, make the font a member variable of the ClockFace class:

image with no caption

Font font = new Font("Arial", 40);

Finally, give your ClockFace class an instance of the nested class StringDraw:

image with no caption

private StringDraw sdToday;

image with no caption

Private xCenter As Integer
Private yCenter As Integer
Private Shared DateRadius As Integer = 600 
Private Shared offset As Integer = 0 
Private myFont As New font("Arial", 40) 
Private sdToday As StringDraw

Modifying the constructor

You are now ready to implement the changes to the ClockFace constructor. You will instantiate the StringDraw object by passing in two parameters: a string representing the current date and a reference to the current ClockFace object:

image with no caption

sdToday = new StringDraw(today,this);

image with no caption

sdToday = New StringDraw(today, Me)

You create the today string by getting the current date from the System.DateTime.Now property, calling the ToLongDateString( ) method.

image with no caption

string today = System.DateTime.Now.ToLongDateString(  );

image with no caption

Dim today As String = System.DateTime.Now.ToLongDateString(  )

For aesthetic reasons, remove commas from this string by calling the Replace( ) method of String:

image with no caption

today = " " + today.Replace(",","");

The only other changes in the constructor initialize the current time and the x,y coordinates:

image with no caption

currentTime = DateTime.Now;
xCenter = Width / 2;
yCenter = Height / 2;

Resetting the center

You want the user to be able to move the clock by clicking on the form. Use the xCenter and yCenter member variables to change the center of the clock, in response to a mousedown. The event handler will readjust the xCenter and yCenter to the values returned by the X and Y properties of the MouseEventArgs object passed in to the handler:

image with no caption

private void ClockFace_MouseDown(
   object sender, System.Windows.Forms.MouseEventArgs e)
{
   xCenter = e.X;
   yCenter = e.Y;

Once this is done, call Invalidate( ) to force a call to Paint( ):

image with no caption

   this.Invalidate(  );
   
}

image with no caption

Private Sub ClockFace_MouseDown( _
 ByVal sender As Object, _
  ByVal e As System.Windows.Forms.MouseEventArgs) _
  Handles MyBase.MouseDown
    xCenter = e.X
    yCenter = e.Y
    Me.Invalidate(  )
End Sub 'ClockFace_MouseDown
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset