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

Figure 10-5. Analog Clock (first image)

figs/pnwa_1005.gif

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.

Figure 10-6. Analog Clock 24-hour face

figs/pnwa_1006.gif

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.

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

Figure 10-7. Simple clock face

figs/pnwa_1007.gif

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

figs/csharpicon.gif

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 );
 }
 
 
 /// 

/// Required method for Designer support - do not modify /// the contents of this method with the code editor. ///

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

figs/vbicon.gif

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

10.2.1.1 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:

figs/csharpicon.gif

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

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

10.2.1.2 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:

figs/csharpicon.gif

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

figs/vbicon.gif

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

10.2.1.3 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:

figs/csharpicon.gif

private void SetScale(Graphics g)
{

figs/vbicon.gif

Private Sub SetScale(ByVal g As Graphics)

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

figs/csharpicon.gif

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

figs/vbicon.gif

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.

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:

figs/csharpicon.gif

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:

figs/csharpicon.gif

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

figs/vbicon.gif

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.

figs/csharpicon.gif

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

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.

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

Computing the x,y Coordinates

Compute the x coordinate of a point on a circle by multiplying the cosine of the angle by the radius and you compute the y coordinate of a point on a circle by multiplying the sin of the angle by the radius. (see PreCalculus with Unit Circle Trigonometry by David Cohn [West Wadsworth]).

These formulae assume that the center of the circle is the origin of your coordinate system, and that the angle is measured counter clockwise from the positive x axis. They also assume that the y axis is positive above the origin and negative below.

A circle is 360 degrees; to evenly space 12 numbers around the face, each number must be 30 degrees from the previous number. The C# Cosine and Sin functions take their parameters in radians, however, not degrees. You'll need to convert degrees to radians using a simple formula: radians equal degrees times pi, divided by 180.

When creating a clock face, it is convenient to measure the degrees offset from the y axis (aligned with 12 o'clock) rather than the x axis, and to increase the angle as you move clockwise (hence the name) rather than the mathematically traditional counter-clockwise. In addition, the coordinate system you'll be using has y values that are negative above the origin, rather than positive.

You solve all three conversions (using the y axis as the zero angle, moving clockwise, and the required coordinate system) by taking advantage of the fact that the cosine of 90 plus an angle is equal to the opposite of the cosine of 90 minus the angle. Thus, to compute 2 o'clock in this system, you compute that 2 is 60 degrees clockwise from 12, add 90, and convert the resulting angle (150) to radians and take the cosine of that value. You can then multiply the result times the radius of the circle and you'll get x,y coordinates that match your coordinate system.

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:

figs/csharpicon.gif

Brush brush = new SolidBrush(ForeColor);

figs/vbicon.gif

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:

figs/csharpicon.gif

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

figs/vbicon.gif

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:

figs/csharpicon.gif

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

figs/vbicon.gif

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:

figs/csharpicon.gif

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

figs/vbicon.gif

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:

figs/csharpicon.gif

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);
}

figs/vbicon.gif

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:

figs/csharpicon.gif

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

figs/vbicon.gif

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.

figs/csharpicon.gif

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:

figs/csharpicon.gif

StringFormat format = new StringFormat( );

figs/vbicon.gif

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.

10.2.2 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#

figs/csharpicon.gif

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

figs/vbicon.gif

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

10.2.2.1 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:

figs/csharpicon.gif

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

figs/vbicon.gif

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:

figs/csharpicon.gif

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

figs/vbicon.gif

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:

figs/csharpicon.gif

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

figs/vbicon.gif

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

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

10.2.2.3 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:

figs/csharpicon.gif

Pen myPen = new Pen(Color.Red);

figs/vbicon.gif

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:

figs/csharpicon.gif

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

figs/vbicon.gif

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:

figs/csharpicon.gif

 hou

figs/csharpicon.gif

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

and set the width of the hour and minute pens:

figs/csharpicon.gif

 ho

figs/csharpicon.gif

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:

figs/csharpicon.gif

Brush secondBrush = new SolidBrush(BackColor);

figs/vbicon.gif

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.

Figure 10-8. Drawing the clock face

figs/pnwa_1008.gif

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.

Figure 10-9. Paper and ruler

figs/pnwa_1009.gif

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

figs/csharpicon.gif

float rotation = GetSecondRotation( );

figs/vbicon.gif

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.

figs/csharpicon.gif

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

figs/vbicon.gif

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

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

figs/csharpicon.gif

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.

10.2.2.5 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:

figs/csharpicon.gif

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

figs/vbicon.gif

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:

figs/csharpicon.gif

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

figs/vbicon.gif

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:

figs/csharpicon.gif

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

figs/vbicon.gif

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;

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:

figs/csharpicon.gif

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);
}

figs/vbicon.gif

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

figs/csharpicon.gif

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

figs/vbicon.gif

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:

figs/csharpicon.gif

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

figs/vbicon.gif

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

10.2.2.6 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:

figs/vbicon.gif

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.

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.

10.2.2.7 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:

figs/csharpicon.gif

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

figs/vbicon.gif

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:

figs/csharpicon.gif

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);
 
 } 
}

figs/vbicon.gif

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.

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

figs/csharpicon.gif

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)

figs/vbicon.gif

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

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

Ferris Wheel

The Ferris Wheel was invented by George W. Ferris, a bridge builder from Pittsburgh, Pennsylvania, and shown at the 1893 World's Columbian Exposition in Chicago. The original wheel was supported by twin steel towers, each standing 140 feet tall; its 45 foot axel was the largest piece of forged steel in the world. The wheel was 250 feet in diameter, and its circumference was 825 feet. It stood 264 feet in the air and was powered by two 1,000 horsepower engines. The wheel had 36 wooden cars, each capable of holding 60 people, keeping them upright at all times. Over 1.5 million people rode the original Ferris Wheel at the Chicago fair.

 

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

figs/csharpicon.gif

public class ClockFace : System.Windows.Forms.Form

figs/csharpicon.gif

{
 //...
 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):

figs/csharpicon.gif

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

figs/vbicon.gif

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:

figs/csharpicon.gif

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

figs/vbicon.gif

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:

figs/csharpicon.gif

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

figs/vbicon.gif

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:

figs/csharpicon.gif

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;
}

figs/vbicon.gif

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:

figs/csharpicon.gif

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:

figs/csharpicon.gif

 Font font = cf.font;

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

figs/csharpicon.gif

 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:

figs/csharpicon.gif

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

figs/vbicon.gif

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

10.2.3.1.2 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:

figs/csharpicon.gif

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

figs/vbicon.gif

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:

figs/csharpicon.gif

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

figs/vbicon.gif

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.

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.

figs/csharpicon.gif

public void DrawString(Graphics g, Brush brush)
{

figs/vbicon.gif

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:

figs/csharpicon.gif

int angle = 360 / theString.Count;

figs/vbicon.gif

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:

figs/csharpicon.gif

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:

figs/csharpicon.gif

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:

figs/csharpicon.gif

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

figs/vbicon.gif

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:

figs/csharpicon.gif

theLtr.X =
 

figs/csharpicon.gif

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

That accomplished, increment the counter:

figs/csharpicon.gif

counter++;

figs/vbicon.gif

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:

figs/csharpicon.gif

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.

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

figs/csharpicon.gif

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:

figs/csharpicon.gif

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:

figs/csharpicon.gif

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

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

figs/csharpicon.gif

private StringDraw sdToday;

figs/vbicon.gif

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

10.2.3.1.4 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:

figs/csharpicon.gif

sdToday = new StringDraw(today,this);

figs/vbicon.gif

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.

figs/csharpicon.gif

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

figs/vbicon.gif

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

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

figs/csharpicon.gif

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

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

figs/csharpicon.gif

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

10.2.3.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:

figs/csharpicon.gif

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

figs/csharpicon.gif

 this.Invalidate( );
 
}

figs/vbicon.gif

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

Windows Forms and the .NET Framework

Getting Started

Visual Studio .NET

Events

Windows Forms

Dialog Boxes

Controls: The Base Class

Mouse Interaction

Text and Fonts

Drawing and GDI+

Labels and Buttons

Text Controls

Other Basic Controls

TreeView and ListView

List Controls

Date and Time Controls

Custom Controls

Menus and Bars

ADO.NET

Updating ADO.NET

Exceptions and Debugging

Configuration and Deployment



Programming. NET Windows Applications
Programming .Net Windows Applications
ISBN: 0596003218
EAN: 2147483647
Year: 2003
Pages: 148

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net