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)

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

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

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

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

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

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:

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:

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

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:

private void SetScale(Graphics g) {

Private Sub SetScale(ByVal g As Graphics)

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

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

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:

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:

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

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.

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:

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:

Brush brush = new SolidBrush(ForeColor);

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:

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

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:

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

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:

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

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:

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

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

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.

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:

StringFormat format = new StringFormat( );

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#

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

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:

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

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:

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

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:

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

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:

Pen myPen = new Pen(Color.Red);

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:

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

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:

hou

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

and set the width of the hour and minute pens:

ho

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:

Brush secondBrush = new SolidBrush(BackColor);

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

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

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

float rotation = GetSecondRotation( );

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.

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

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

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:

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:

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

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:

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

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:

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

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:

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

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

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

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:

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

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:

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:

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

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:

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

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)

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)

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.

public class ClockFace : System.Windows.Forms.Form

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

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

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:

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

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:

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

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:

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

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:

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:

Font font = cf.font;

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

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:

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

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:

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

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:

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

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.

public void DrawString(Graphics g, Brush brush) {

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:

int angle = 360 / theString.Count;

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:

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:

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:

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

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:

theLtr.X =

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

That accomplished, increment the counter:

counter++;

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:

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.

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:

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:

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

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

private StringDraw sdToday;

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:

sdToday = new StringDraw(today,this);

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.

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

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

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

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

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

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:

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

this.Invalidate( ); }

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

Категории