11,415,390 members (85,362 online)

# Yet Another Analog Clock

, 8 Jun 2005
 Rate this:
Yet another analog clock control in C#, but this one is different.

## Introduction

There are many 'Analog clocks' at CodeProject and this is yet another (hence the title), but I have designed this one to be slight different. This analog clock is very much customizable (besides being pretty ). You can resize it, change the colors, time, rendering quality add a background image and a lot. Take a look at the list of properties. Much of the inspiration comes from A Vector Graphics Rendered Animated Clock and Public Joe.

## Background Mathematics

Let us delve back into some basic mathematics before getting involved in the programming aspects. You all know that a circle is of 360° degrees or 2Π in radians and that there are 12 digits on the face of the clock from one to twelve. To draw the twelve digits on the face, we need to place each of them at same degree from each other, so 360/12 = 30, so we need to place each digit 30° apart from the other. This means starting from 0° , 30°, 60° up to 360°. Also recall that angles are measured positive clockwise and negative anticlockwise. Now how are the minute, hour and second hand drawn. Quite simple, recall that we can calculate any point by using the formulas as seen in the picture below:

We need to know the angle θ and then we can draw a line from the centre of the circle. I.e., from origin to that point for a given θ. θ is the angle or should I say rotation. Note that a second hand takes 60 ticks to complete one complete rotation. So what should be the angle of rotation when sec = 1? Well it should be:

`Second Rotation = 2.0 *  * sec/60.0`

Think of the term sec/60.0 as the percentage of how much rotation must be given for the given value of second. For example:

```when sec = 0 , rotation is 0° ( i.e. the second hand is at 12)
when sec = 15 , rotation is 25% ( i.e. the second hand is at 3)
when sec = 30 , rotation is 50% ( i.e. the second hand is at 6)```

Now that we have the value of θ or rotation, we can find the value of x and y and draw a line from the origin to the point P using the above mentioned formula.

`Minute Rotation = 2.0 *  * ( currentmin + currentsec/60.0 )/60.0`

Now again think of the term ( currentmin + currentsec/60.0 )/60.0 as the percentage of how much rotation must be given for the given value of currentmin and currentsec. Note that in the above term we have also added second rotation so that the minute hand is updated every second. But updating minute hand every second doesn't look neat as it is updated only as significant changes occur in the value of minute rotation and it would look like that the minute hand moves unevenly. If you wish to update the minute hand after every minute only, just remove the term currentsec/60.0. Now when the second hand completes one rotation, the minute hand will move ahead. (In order to see what I am trying to tell, download the source code and observe how the minute hand behaves in the clock that displays the time for Honk Kong and the rest.)

`Hour Rotation = 2.0 *  * ( currenthour + currentmin/60.0 )/12.0`

Similar technique here. The hour hand is updated every minute so we have added the minute rotation too. You can think of how the clock would behave if we didn't update the hour hand every minute.

Now let's move on to the implementation details. All the drawing is done in the function `DrawClock: private void DrawClock(Graphics e)`. The Analog Clock exposes a property named `Time`. Whenever the value of this property is changed, the clock is updated.

## Enabling Double Buffering

In the constructor of the UserControl, I have enabled Double-buffering on the control which prevents flicker caused by the redrawing of the control. To fully enable double-buffering, we must also set the `UserPaint` and `AllPaintingInWmPaint` style bits to `true` (to read more about Double Buffering read this).

```this.SetStyle(ControlStyles.DoubleBuffer, true);
this.SetStyle(ControlStyles.UserPaint,true);
this.SetStyle(ControlStyles.AllPaintingInWmPaint,true);```

## Enabling Anti-aliasing

To make the clock look smooth, I have set the `SmoothingMode` and `TextRenderingHint` property of the graphics object to Anti-alias. The `SmoothingMode` property makes the face of the clock look smooth and the `TextRenderingHint` property makes the clock numerals look smooth. Please note that `SmoothingMode` has no effect on the numeral that is drawn on the clock.

```grfx.SmoothingMode = smoothingMode;
grfx.TextRenderingHint = textRenderingHint;
grfx.InterpolationMode =
System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; ```

Here is how the analog clock appears without anti-aliasing.

## Drawing the clock face

Next I have made four rectangles inside which circles are drawn. The drop shadow is also an eclipse which is at a slightly offset from the face eclipse. (Please note that there can be other ways to implement this, such as drawing eclipse directly).

```//Define rectangles inside which we will draw circles.
Rectangle rect = new Rectangle(0+10 ,0+10 ,(int)x-20 ,(int)y-20);
Rectangle rectinner = new Rectangle(0+40 ,0+40 ,(int)x-80 ,(int)y-80);
Rectangle rectrim = new Rectangle(0+20 ,0+20 ,(int)x-40 ,(int)y-40);
Rectangle rectdropshadow = new Rectangle(0+10 ,0+10 ,(int)x-17 ,(int)y-17);```

Next I have drawn a gradient filled eclipse inside all these rectangles,

```//Drop Shadow

//Face
if(this.drawRim)
grfx.FillEllipse(gb, rect);

//Rim
grfx.FillEllipse(gb, rectrim);```

This gives the basic face of the clock as follows:

## Drawing the circular face image

To draw a circular image within the face of the clock, we have to define a circular clipping region and draw image inside it. Once we have done that, we'll have to reset the clipping region. Here is how it is done:

```//Define a circular clip region and draw the image inside it.
GraphicsPath path = new GraphicsPath();
grfx.SetClip(path);
if(this.img != null)
grfx.DrawImage(this.img, rect);
path.Dispose();
//Reset clip region
grfx.ResetClip();```

where `img` represents the image. It is declared as:

`private Image img;`

The output is something like this:

## Drawing the Numerals

Next we need to draw the numerals. But before we do that, we need to move the origin of the control to the center of the client area. (By default it is at the top right corner). We can do so by using the graphic object's `TranslateTransform` method. Here's how it is done.

`grfx.TranslateTransform(midx, midy);`

I have already defined `midx` and `midy` as the center of the control. Next we draw the numerals:

```StringFormat format = new StringFormat();
format.Alignment = StringAlignment.Center;
format.LineAlignment = StringAlignment.Center;

//Draw Numerals on the Face
int deg = 360/12;
if(drawnumerals)
{
for(int i=1; i <= 12; i++)
{
grfx.DrawString(i.ToString() , textFont ,stringBrush ,
-1 * GetX(i * deg +90) , -1 * GetY(i * deg + 90),format); ```

As you can see, we have multiplied both the value of x and y by -1 so that the numeral 12 appears on the top, other wise it'll appear on bottom as seen below.

## Drawing the Second Hand

First, we will draw the hour hand, then the minute hand and finally the second hand. The order is important because it is the same in real world clocks.

Our second hand can tick in two different ways -- as explained earlier, one is normal tick style which we usually see in quartz clocks and the other is smooth style usually seen in automatic clocks. I have defined enumerations for both of these styles which is used to determine the value of the rotation.

```//Draw Minute hand
if(drawMinuteHand)
{

if(minHandTickStyle == TickStyle.Smooth)
minuteAngle = (float)(2.0 * Math.PI* ( min + sec/60.0 )/60.0);
else
minuteAngle = (float)(2.0 * Math.PI* ( min /60.0));

pen.EndCap = LineCap.Round;
pen.StartCap = LineCap.RoundAnchor;
//--End Minute Hand
```

The drop shadow of the second hand is nothing but another second hand.

```//Drop shadow
centre.Offset(2, 2);
pen.Color = Color.Gray;

{
}

centre.X = centre.Y = 0;
pen.Color = minHandColor;
Point minHand = new Point( (int)( radius * Math.Sin(minuteAngle) ),
grfx.DrawLine(pen, centre, minHand);
}```

Similar technique prevails for the hour and minute hand.

## How to use the Analog Clock.

Add the `AnalogClock` control to your VS.NET toolbox. Then place the analog clock and a timer to your form. Set an appropriate interval for the timer and enable it. Double click the timer and add the following code:

`analogClock1.Time = DateTime.Now;`

## List of Properties for the Analog Clock control

• `DrawDropShadow` - Determines whether drop shadow for the clock is drawn or not.
• `DrawHourHand` - Determines whether the hour Hand is shown.
• `DrawHourHandShadow` - Determines whether the hour hand casts a drop shadow for added realism.
• `DrawMinuteHand` - Determines whether the minute hand is shown.
• `DrawMinuteHandShadow` - Determines whether the minute hand casts a drop shadow for added realism.
• `DrawNumerals` - Determines whether the numerals are drawn on the clock face.
• `DrawRim` - Determines whether the clock rim is drawn or not.
• `DrawSecondHand` - Determines whether the second hand is shown.
• `DrawSecondHandShadow` - Determines whether the second hand casts a drop shadow for added realism.
• `DropShadowColor` - Sets or gets the color of the drop Shadow.
• `DropShadowOffset` - Gets or sets the drop shadow offset.
• `FaceColorHigh` - Determines the first color of the clock face gradient.
• `FaceColorLow` - Determines the second color of the clock face gradient.
• `FaceGradientMode` - Gets or sets the direction of the clock face gradient.
• `FaceImage` - The Background image used in the clock face.
• `HourHandColor` - Gets or sets the color of the hour hand.
• `HourHandDropShadowColor` - Sets or gets the color of the hour hand drop shadow.
• `MinuteHandColor` - Gets or sets the color of the minute hand.
• `MinuteHandDropShadowColor` - Sets or gets the color of the minute hand drop shadow.
• `MinuteHandTickStyle` - Defines the minute hand tick style.
• `NumeralColor` - Sets or gets the color of the clock numerals.
• `RimColorHigh` - Determines the first color of the rim gradient.
• `RimColorLow` - Determines the second color of the rim face gradient.
• `RimGradientMode` - Gets or sets the direction of the rim gradient.
• `SecondHandColor` - Gets or sets the color of the seconds hand.
• `SecondHandDropShadowColor` - Sets or gets the color of the second hand drop shadow.
• `SecondHandEndCap` - Determines the seconds hand end line shape.
• `SecondHandTickStyle` - Defines the second hand tick style.
• `SmoothingMode` - Sets or gets the rendering quality of the clock.
• `TextRenderingHint` - Sets or gets the text rendering mode used for the clock numerals.
• `Time` - The `System.DateTime` structure which is used to display time.

## Know Problems/Issues

• Using a large image for the `FaceImage` property results in poor performance and increased memory consumption, since the image is redrawn every time the `Time` property is changed.

• Using large font size on a large clock results in numerals going out the clock face. I haven't given much attention to this detail. This is a design flaw.

• The width of the rim remains constant and doesn't change with the size of the clock. This is yet another design flaw.

• The numeral font size doesn't change automatically with the size of the clock. This hasn't been implemented.

## History

• v1.0 - 08 June 2005

A list of licenses authors might use can be found here

## Share

United States
Obaid ur Rehman is an undergraduate Computer Science student at the University of Karachi.

 First PrevNext
 My vote of 5 Chana Winkler13-Dec-10 22:41 Chana Winkler 13-Dec-10 22:41
 oscillating effect yyxl25-Nov-10 15:04 yyxl 25-Nov-10 15:04
 I did it too gajatko24-Aug-07 9:15 gajatko 24-Aug-07 9:15
 Re: I did it too gajatko24-Aug-07 9:19 gajatko 24-Aug-07 9:19
 Ah! This is my code (just copy & paste to your IDE and see the effect; it was designed in VS 2005, however it should compile in C# 1.0 too): Clock.cs: ```using System; using System.Collections.Generic; using System.ComponentModel; using System.Drawing; using System.Data; using System.Text; using System.Windows.Forms; namespace Calendar { public partial class Clock : UserControl { public Clock() { InitializeComponent(); timerHandle = Microsoft.Win32.SystemEvents.CreateTimer(1000); Microsoft.Win32.SystemEvents.TimerElapsed += new Microsoft.Win32.TimerElapsedEventHandler(SystemEvents_TimerElapsed); updateFonts(); Pen p = new Pen(Color.Black); p.EndCap = System.Drawing.Drawing2D.LineCap.ArrowAnchor; ClockStyle = ClockStyles.BlackWhite; } const int MIN_FONT_SIZE = 10; private TimeSpan time = DateTime.Now.TimeOfDay; private long timeOffSet = 0; private Brush timeBrush = new SolidBrush(Color.DarkRed); private OwnerDrawModes ownerDrawMode = OwnerDrawModes.None; private HandUpdateStyles handUpdateStyle = HandUpdateStyles.MinuteSmooth | HandUpdateStyles.SecondJumping; private Brush digitsBrush; private Pen circlePen; private Pen handPen; private ClockStyles clockStyle; Font textFont; Font digitsFont; int padding = 3; IntPtr timerHandle; clockFlags flags = clockFlags.AutoTimeUpdate; public enum Hands { Hour, Minute, Second } public enum OwnerDrawModes { Hands, None } public enum ClockStyles { BlackWhite, WhiteBlack, Custom } [Flags()] public enum HandUpdateStyles { MinuteSmooth = 1, MinuteJumping = 2, SecondSmooth = 4, SecondJumping = 8 } [Flags()] enum clockFlags { AutoTimeUpdate = 1 } public event EventHandler DrawHand; #region Props /// Gets or sets a Pen used to draw hands of the clock. [Browsable(false)] public Pen HandPen { get { return handPen; } set { handPen = value; clockStyle = ClockStyles.Custom; } } /// Gets or sets a Brush used to draw digits. [Browsable(false)] public Brush DigitsBrush { get { return digitsBrush; } set { digitsBrush = value; clockStyle = ClockStyles.Custom; } } /// Gets or sets a Brush used to draw a current time. [Browsable(false)] public Brush TimeBrush { get { return timeBrush; } set { timeBrush = value; clockStyle = ClockStyles.Custom; } } /// Gets or sets a Pen used to outline a face of the clock. [Browsable(false)] public Pen CirclePen { get { return circlePen; } set { circlePen = value; clockStyle = ClockStyles.Custom; } } /// Applies one of default styles to the Clock control. [DefaultValue(ClockStyles.BlackWhite)] public ClockStyles ClockStyle { get { return clockStyle; } set { clockStyle = value; switch (value) { case ClockStyles.BlackWhite: digitsBrush = new SolidBrush(Color.Gray); circlePen = new Pen(Color.DarkGray, 4); handPen = new Pen(Color.Black, 4); BackColor = Color.White; break; case ClockStyles.WhiteBlack: digitsBrush = new SolidBrush(Color.Silver); circlePen = new Pen(Color.LightGray, 4); handPen = new Pen(Color.White, 4); BackColor = Color.Black; break; default: break; } } } /// Gets or sets hand update mode [DefaultValue(HandUpdateStyles.MinuteSmooth | HandUpdateStyles.SecondJumping)] public HandUpdateStyles HandUpdateStyle { get { return handUpdateStyle; } set { if ((handUpdateStyle & HandUpdateStyles.SecondSmooth) == 0) { if ((value & HandUpdateStyles.SecondSmooth) != 0) timerHandle = Microsoft.Win32.SystemEvents.CreateTimer(20); else timerHandle = Microsoft.Win32.SystemEvents.CreateTimer(1000); } handUpdateStyle = value; } } /// Determines whether a time exposed by a clock should be changed automatically. [DefaultValue(true)] public bool AutoTimeUpdate { get { return (flags & clockFlags.AutoTimeUpdate) != 0; } set { if (value) flags |= clockFlags.AutoTimeUpdate; else flags &= ~clockFlags.AutoTimeUpdate; } } /// Gets or sets an owner-drawing mode [DefaultValue(OwnerDrawModes.None)] public OwnerDrawModes OwnerDrawMode { get { return ownerDrawMode; } set { ownerDrawMode = value; } } /// Gets or sets a current time [Browsable(false)] public TimeSpan Time { get { return time; } set { time = value; Invalidate(); } } /// Gets or sets an offset between computer time and this displayed on the clock. This has sense only if AutoTimeUpdate is set to true. [DefaultValue(0)] public long TimeOffSet { get { return timeOffSet; } set { timeOffSet = value; } } /// Gets a radius of clock's face. public int Radius { get { return ClientSize.Height / 2 - 2*padding; } } /// Gets a center point of clock's face. public PointF CenterPoint { get { return new PointF(Radius + padding, Radius + padding); } } #endregion public class DrawHandEventArgs : EventArgs { private PointF centerPoint; private PointF pointOnArc; private Hands hand; private PaintEventArgs paintArgs; private bool drawDefault = false; public DrawHandEventArgs(PointF _centerPoint, PointF _pointOnArc, Hands _hand, PaintEventArgs _paintArgs) { centerPoint = _centerPoint; pointOnArc = _pointOnArc; hand = _hand; paintArgs = _paintArgs; } #region Props public bool DrawDefault { get { return drawDefault; } set { drawDefault = value; } } public PaintEventArgs PaintArgs { get { return paintArgs; } } public Hands Hand { get { return hand; } } public PointF PointOnArc { get { return pointOnArc; } } public PointF CenterPoint { get { return centerPoint; } } #endregion } protected override void OnResize(EventArgs e) { base.OnResize(e); Height = Width; updateFonts(); } protected override void OnPaint(PaintEventArgs e) { if (AutoTimeUpdate) time = DateTime.Now.AddTicks(timeOffSet).TimeOfDay; Graphics g = e.Graphics; g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;// System.Drawing.Drawing2D.SmoothingMode.AntiAlias; DrawFace(e); DrawHands(e); string timeStr = time.Hours.ToString().PadLeft(2, '0') + " : " + time.Minutes.ToString().PadLeft(2, '0') + " : " + time.Seconds.ToString().PadLeft(2, '0'); SizeF strSize = g.MeasureString(timeStr, textFont); g.DrawString(timeStr, textFont, timeBrush, new PointF(-strSize.Width + Width, -strSize.Height + Height)); base.OnPaint(e); } void DrawFace(PaintEventArgs e) { Graphics g = e.Graphics; int r = Radius; g.DrawEllipse(circlePen, new Rectangle(padding, padding, 2*r, 2*r)); PointF arc; SizeF strSize; for (int i = 1; i <= 12; i++) { g.DrawLine(circlePen, getPointOnArc((double)i * 360 / 12, r), getPointOnArc((double)i * 360 / 12, r*9/10)); arc = getPointOnArcInMathArrangement((double)i * 360 / 12, r*8/10); strSize = g.MeasureString(i.ToString(), digitsFont); g.DrawString(i.ToString(), digitsFont, digitsBrush, toWinFormsArrangement(new PointF(arc.X - strSize.Width/2, arc.Y + strSize.Height/2))); } Pen p = (Pen)circlePen.Clone(); p.Width--; for (int i = 0; i < 60; i++) if (i%5 != 0) g.DrawLine(p, getPointOnArc((double)i * 360 / 60, r), getPointOnArc((double)i * 360 / 60, r*95/100)); } void updateFonts() { textFont = new Font(Font.FontFamily, Math.Max(MIN_FONT_SIZE, (float)ClientSize.Height / 30), GraphicsUnit.Pixel); digitsFont = new Font(Font.FontFamily, Math.Max(MIN_FONT_SIZE, (float)ClientSize.Height / 20), GraphicsUnit.Pixel); } void DrawHands(PaintEventArgs e) { int r = Radius; PointF center = CenterPoint; if (ownerDrawMode == OwnerDrawModes.Hands && DrawHand != null) { DrawHand(this, new DrawHandEventArgs(center, getPointOnArc(time.TotalHours * 360d / 12d, r / 2), Hands.Hour, e)); DrawHand(this, new DrawHandEventArgs(center, getPointOnArc(time.TotalMinutes * 360d / 60d, r), Hands.Minute, e)); DrawHand(this, new DrawHandEventArgs(center, getPointOnArc(time.TotalSeconds * 360d / 60d, r), Hands.Second, e)); } else { Graphics g = e.Graphics; Pen pen = (Pen)handPen.Clone(); g.DrawLine(pen, center, getPointOnArc(time.TotalHours * 360d / 12d, r / 2)); pen.Width--; if ((handUpdateStyle & HandUpdateStyles.MinuteJumping) != 0) g.DrawLine(pen, center, getPointOnArc(time.Minutes * 360d / 60d, r)); else g.DrawLine(pen, center, getPointOnArc(time.TotalMinutes * 360d / 60d, r)); pen.Width--; if ((handUpdateStyle & HandUpdateStyles.SecondJumping) != 0) g.DrawLine(pen, center, getPointOnArc(time.Seconds * 360d / 60d, r)); else g.DrawLine(pen, center, getPointOnArc(time.TotalSeconds * 360d / 60d, r)); } } protected override void DestroyHandle() { Microsoft.Win32.SystemEvents.KillTimer(timerHandle); base.DestroyHandle(); } void SystemEvents_TimerElapsed(object sender, Microsoft.Win32.TimerElapsedEventArgs e) { if (e.TimerId == timerHandle) { Invalidate(); } } /// Angle in degrees, returns a coordinates of a point on the arc, in Windows Forms arrangement. /// Angle in degrees. /// Radius in pixels PointF getPointOnArc(double angle, int radius) { return toWinFormsArrangement(getPointOnArcInMathArrangement(angle, radius)); } /// Returns a coordinates of a point on the arc, in mathematical arrangement. /// Angle in degrees. /// Radius in pixels PointF getPointOnArcInMathArrangement(double angle, int radius) { return new PointF( (float)(Math.Sin(toRadians(angle)) * radius), (float)(Math.Cos(toRadians(angle)) * radius)); } /// Converts coordinates form math arrangement to Windows Forms arrangement. /// Point in mathematical arrangement. PointF toWinFormsArrangement(PointF inMathArr) { return new PointF(inMathArr.X + Radius + circlePen.Width, -inMathArr.Y + Radius + circlePen.Width); } double toRadians(double angleInDegrees) { return Math.PI * (double)angleInDegrees / 180; } } } ``` Clock.designer.cs: ```namespace Calendar { partial class Clock { /// /// Required designer variable. /// private System.ComponentModel.IContainer components = null; /// /// Clean up any resources being used. /// /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); } #region Component Designer generated code /// /// Required method for Designer support - do not modify /// the contents of this method with the code editor. /// private void InitializeComponent() { this.SuspendLayout(); // // Clock // this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.BackColor = System.Drawing.Color.White; this.DoubleBuffered = true; this.Name = "Clock"; this.Size = new System.Drawing.Size(256, 214); this.ResumeLayout(false); } #endregion } } ``` Greetings - Gajatko
 Round Control flow19659-Apr-07 9:38 flow1965 9-Apr-07 9:38
 HI /-\ \/\/ /-\ /\/ [GUY]13-Mar-07 8:01 /-\ \/\/ /-\ /\/ [GUY] 13-Mar-07 8:01
 HI /-\ \/\/ /-\ /\/ [GUY]13-Mar-07 7:33 /-\ \/\/ /-\ /\/ [GUY] 13-Mar-07 7:33
 How to add to toolbox? wizkid126-Aug-06 16:31 wizkid1 26-Aug-06 16:31
 Re: Chews up CPU wizkid126-Aug-06 17:45 wizkid1 26-Aug-06 17:45
 Re: Chews up CPU wizkid126-Aug-06 17:55 wizkid1 26-Aug-06 17:55
 Last Visit: 31-Dec-99 19:00     Last Update: 27-Apr-15 15:22 Refresh 1234 Next »