Code example: Analog Clock – custom control for beginners

29 בדצמבר 2007

Analog clock is very cool feature, most windows users like analog clocks, we can find them in Google-Gadgets and other desktop programs. I've written custom control a few years ago (in .Net 1.1) and rediscovered it two weeks ago in some of my projects. It is very simple (can be extended and improved) and provides clear example how to build custom controls in .Net (C#).

Challenge for those who moved to WPF: if you have some time and motivation, please write analog clock in WPF and publish the code (you can send it to my email or post in your Blog), developer that will provide working clock in WPF will get cool "Merit Certificate" in my Blog and the appreciation of the developers' community 🙂


This how it looks in VS Designer (you can see both analog and digital styles) :


analog1


 


I've made custom control "ClockControl" what overrides "OnPaint" method for drawing of clock.
"ClockControl" uses very simple trigonometry functions to calculate the positions of clock hands according to current time. Developer can drag and drop this component from VS tool bar into form or other control container. Each clock property can be accessed from property panel.











analog3


 


 

Each small movement of hand is calculated by:

Seconds: 360 / 60 = 6 deg.
Minutes: 360 / 60 = 6 deg.
Hours: 360 / 12 = 30 deg.

 


 


 


analog4


 


Point (X1, Y1) is known point (0, 0) in axes coordinates and (Width/2, Height/2) in control coordinates.

Point (X2, Y2) should be calculated by using the angle of hand according to PC clock.


(See calculations in "Draw…" methods)


 


 


 


 


                                                                             Class definition table: analog2


























































Member Name


Comments


ClockEnabled
(Boolean)


Enables the timer of the clock, each tick moves hands (redraws the control)


ClockInterval
(Integer)


Timer interval in milliseconds


ColorSeconds
(Color)


The color of seconds' hand


ColorHours
(Color)


The color of hours' hand


ColorMinutes
(Color)


The color of minutes' hand


Radius
(Integer)


The radius of clock circle


Style
(Enum)


Defines current style of the clock


SwitchStyleOnDoubleClick
(Boolean)


Flag for double-click option to switch clock style between analog and digital display


Time
(Date)


Current time


DrawAnalog
(void)


Draws analog clock graphics, calls methods: DrawSeconds, DrawMinutes, DrawHours, DrawClockCircle, DrawCaptions


DrawDigital
(void)


Draws digits of digital clock


GetX
(Integer)


Returns 'x' coordinate by given radius and angle (see description)


GetY
(Integer)


Returns 'y' coordinate by given radius and angle (see description)


InitClock
(void)


Initializes the instance of the control


InitGrafix
(void)


Initializes pens, brushes and other graphic objects for drawing


SetDrawingStyle
(void)


Sets custom drawing style parameters


ClockTick
(delTime)


Event that raised by clock on each timer's tick (very useful for external use)


 


 




 


 


 


 


 


 


 


 


Comments about class members (and nested sub-types) of the class:

















































Member (Type) Name


Comments


/// <summary>
///
Clock Style
/// </summary>
public enum ClockStyle
{
      /// <summary>
      /// Analog Style
      /// </summary>
      Analog,
 


      /// <summary>
      /// Digital Style
      /// </summary>
      Digital
}


(Enum) Represents the clock styles


 


 


 


/// <summary>
///
Sets the drawing style.
/// </summary>
private void SetDrawingStyle()
{


SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.DoubleBuffer, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
SetStyle(ControlStyles.SupportsTransparentBackColor, true);
UpdateStyles();


}


Method "SetStyle": built-in method of inherited control (base class) to define the drawing parameters.


Flag "UserPaint": control paints itself rather than the operating system (means that "OnPaint" will be called rather, required for double-buffering (prevents flickering) and required by "AllPaintingInWmPaint" flag.


Flag "AllPaintingInWmPaint": control ignores the window message WM_ERASEBKGND to reduce flicker, requires "UserPaint" flag. Disables redundant redrawing of parent background.

Flag "DoubleBuffer": prevents flicker caused by the redrawing of the control (hidden in .Net 2.0, but can be defined by using "DoubleBuffer = true" set), requires "AllPaintingInWmPaint" flag.

Flag "OptimizedDoubleBuffer": control is first drawn to a buffer rather than directly to the screen, which can reduce flicker, requires "AllPaintingInWmPaint" flag.


Flag "SupportsTransparentBackColor": control accepts a BackColor with an alpha component of less than 255 to simulate transparency (means that you can make clock with transparent background!).


/// <summary>
///
Inits the grafix.
/// </summary>
private void InitGrafix(bool _invalidate)
{


if (Bounds.IsEmpty || !IsHandleCreated) return;


// 1. set clock radius (by width or height)
if (Width < Height)
    Radius = (int) Math.Round(Width/2.0);
else
   
Radius = (int) Math.Round(Height/2.0);


// 2. set control region (drawing surface)
using (GraphicsPath p = new GraphicsPath())
{
      p.StartFigure();
     
if (style == ClockStyle.Analog)
          p.AddEllipse(ClientRectangle);
     
else
         
p.AddRectangle(ClientRectangle);        
      p.CloseAllFigures();


      if (Region != null) Region.Dispose();
      Region = new Region(p);
}


// 3. set caption (numbers) font
if (capFont != null) capFont.Dispose();
capFont = new Font(Font.FontFamily, radius * FONT_DIFF_ANALOG, Font.Style);


// 4. set digits font (used by digital style)
if (digFont != null) digFont.Dispose();
digFont = new Font(Font.FontFamily, Height * FONT_DIFF_DIGITAL, Font.Style);


// 5. initialize pens
InitPens();


// 6. set clock boundaries
clockBounds = new Rectangle(1, 1, radius*2 – 2, radius*2 – 2);
capSize = CreateGraphics().MeasureString("0", capFont);


// 7. if user redraw is required
if (_invalidate && IsHandleCreated && !Disposing)
{
      Update();
      Invalidate();
}


}


Method "InitGrafix" – initializes graphic variables.


1.       Set clock radius by difference between the height and width of the control.


2.       Set control surface (region) by clock style (analog = ellipse, digital = rectangle).
"GraphicsPath" object is used to define the shape of the region.


3.       Set caption font for analog style by using the selected font of the control.


4.       Set caption font for digital style by using the selected font of the control.


5.       Initialize pens (objects used to draw clock hands, see method "InitPens").


6.       Set clock boundaries (shape rectangle) to draw circle with.


7.       If argument "_invalidate = TRUE" invalidate (redraw) clock.


 


 


 


 


/// <summary>
///
Inits the pens.
/// </summary>
private void InitPens()
{
      InitPen(ref secPen, ColorSeconds, PEN_DIFF_SECONDS);
      InitPen(ref minPen, ColorMinutes, PEN_DIFF_MINUTES);
      InitPen(ref hourPen, ColorHours, PEN_DIFF_HOURES);
}


/// <summary>
///
Inits the pen.
/// </summary>
///
<param name="_pen">The pen.</param>
///
<param name="_clr">The pen color.</param>
///
<param name="_diff">The width diff.</param>
private void InitPen(ref Pen _pen, Color _clr, float _diff)
{
      if (_pen != null) _pen.Dispose();
      _pen = new Pen(_clr, _diff);
}


Method "InitPens" initializes 3 pens by calling "InitPen", passing reference to "Pen" object, color and pen width.


 



 


 


 


 


 


Method "InitPen" called from other places also, can be used as general method for pen initialization.


/// <summary>
///
Raises the <see cref="E:System.Windows.Forms.Control.Paint"></see> event.
/// </summary>
/// <param name="e">A <see cref="T:System.Windows.Forms.PaintEventArgs"></see> that contains the event data.</param>
protected override void OnPaint(PaintEventArgs e)
{


base.OnPaint(e);
if (!Visible || !Created || e == null || e.Graphics == null || e.ClipRectangle.IsEmpty) return;


e.Graphics.Flush(FlushIntention.Sync);
e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
e.Graphics.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;

                 if (style == ClockStyle.Analog) 
                         DrawAnalog(e.Graphics);
                else
                         DrawDigital(e.Graphics);
}

Method "OnPaint" overrides base virtual method from control class. Used to draw clock hands, digits and circle.


I'm using "PaintEventArgs e" to get access to drawing object/device that has access to Windows API to draw my graphics by using GDI+ (see previous post about the GDI Engine).


(a)    e.Graphics.Flush(FlushIntention.Sync): Specifies that all graphics operations on the stack are executed as soon as possible (means the previous drawing will be cleared before new one will drawn).


(b)   e.Graphics.SmoothingMode = SmoothingMode.AntiAlias: Specifies anti-alias rendering of drawn hands and circle.


(c)    e.Graphics.TextRenderingHint = TextRenderingHint.ClearTypeGridFit: Each character is drawn using its glyph ClearType bitmap with hinting. The highest quality setting (meand that all drawn captions will be draw by using high quality rendering with anti-alias like in windows.


(d)   "DrawAnalog" or "DrawDigital": called by clock style.


/// <summary>
///
Draws the analog style.
/// </summary>
///
<param name="g">The g.</param>
private void DrawAnalog(Graphics g)
{


DrawSeconds(g);
DrawMinutes(g);
DrawHours(g);
DrawClockCircle(g);
DrawCaptions(g);


}


Method "DrawAnalog" called to draw analog style graphics.


Calls for other methods to draw different parts of the clock.


/// <summary>
///
Draws the clock circle.
/// </summary>
///
<param name="g">The g.</param>
private void DrawClockCircle(Graphics g)
{


if (g == null || secPen == null || clockBounds.IsEmpty) return;
g.DrawArc(secPen, clockBounds, 0.0f, 360.0f);


}


Method "DrawClockCircle" draws arc from 0 degree to 360 degree, uses "DrawArc" method with defined pen "secPen", can be replced with "DrawEllipse" that draws ellipse without specification of degrees.


Values "0.0f" and "360.0f" can be replaced with other angles to draw partial arc.


/// <summary>
///
Gets the Y cord.
/// </summary>
///
<param name="_radius">The _radius.</param>
///
<param name="_angle">The _angle.</param>
///
<returns></returns>
public static int GetY(int _radius, float _angle)
{
       
return (int) Math.Round(_radius*Math.Sin(_angle));
}


/// <summary>
///
Gets the X cord.
/// </summary>
///
<param name="_radius">The _radius.</param>
///
<param name="_angle">The _angle.</param>
///
<returns></returns>
public static int GetX(int _radius, float _angle)
{
        return (int) Math.Round(_radius*Math.Cos(_angle));
}


Static function "GetY" returns calculated value for 'y' coordinate, 'y' axe calculated by sine of angle.


Static function "GetX" returns calculated value for 'x' coordinate, 'x' axe calculated by cosine of angle.


Both functions are defined "static" for access beyond the instance of "ClockControl" and can be used by other classes also.


_radius: the radius of the clock circle
_angle: current angle of hand (in radians)


/// <summary>
///
Draws the seconds.
/// </summary>
///
<param name="g">The g.</param>
private void DrawSeconds(Graphics g)
{


        if (g == null || secPen == null || clockBounds.IsEmpty) return;
        float angle = (float) (Math.PI*(6.0f*sec – 90.0f)/180.0f);
        int y = GetY(radius, angle);
        int x = GetX(radius, angle);
        Point start = new Point(radius, radius);
        Point end = new Point(radius + x, radius + y);
        g.DrawLine(secPen, start, end);
}


float angle = (float) (Math.PI*(6.0f*sec – 90.0f)/180.0f); // calculates the angle of seconds' hand (converts degrees into radians)


int y = GetY(radius, angle); // gets 'y' coord.
int x = GetX(radius, angle); // gets 'x' coord.


Point start = new Point(radius, radius); // point (X1, Y1) = half width and half height of the control or = radius of the clock
Point end = new Point(radius + x, radius + y); // calculated point (X2, Y2), according to current seconds


/// <summary>
///
Draws the minutes.
/// </summary>
///
<param name="g">The g.</param>
private void DrawMinutes(Graphics g)
{


if (g == null || minPen == null || clockBounds.IsEmpty) return;
float angle = (float) (Math.PI*(6.0f*min – 90.0f)/180.0f);
int y = (int) Math.Round(GetY(radius, angle)*0.8);
int x = (int) Math.Round(GetX(radius, angle)*0.8);
Point start = new Point(radius, radius);
Point end = new Point(radius + x, radius + y);
g.DrawLine(minPen, start, end);


}


float angle = (float) (Math.PI*(6.0f*min – 90.0f)/180.0f); // calculates the angle of minutes' hand (converts degrees into radians)


int y = (int) Math.Round(GetY(radius, angle)*0.8); // gets 'y' coord.
int x = (int) Math.Round(GetX(radius, angle)*0.8); // gets 'x' coord.
(minutes hand is about 80% length of seconds hand)


Point start = new Point(radius, radius); // point (X1, Y1) = half width and half height of the control or = radius of the clock
Point end = new Point(radius + x, radius + y); // calculated point (X2, Y2), according to current seconds


/// <summary>
///
Draws the hours.
/// </summary>
///
<param name="g">The g.</param>
private void DrawHours(Graphics g)
{


if (g == null || hourPen == null || clockBounds.IsEmpty) return;
float angle = (float) (Math.PI*(30.0f*hour – 90.0f)/180.0f);
int y = (int) Math.Round(GetY(radius, angle)*0.6);
int x = (int) Math.Round(GetX(radius, angle)*0.6);
Point start = new Point(radius, radius);
Point end = new Point(radius + x, radius + y);
g.DrawLine(hourPen, start, end);


}


float angle = (float) (Math.PI*(30.0f*hour – 90.0f)/180.0f); // calculates the angle of hours' hand (converts degrees into radians)


 


int y = (int) Math.Round(GetY(radius, angle)*0.6); // gets 'y' coord.
int x = (int) Math.Round(GetX(radius, angle)*0.6); // gets 'x' coord.
(hours hand is about 60% length of seconds hand)


Point start = new Point(radius, radius); // point (X1, Y1) = half width and half height of the control or = radius of the clock
Point end = new Point(radius + x, radius + y); // calculated point (X2, Y2), according to current seconds


/// <summary>
///
Draws the captions.
/// </summary>
///
<param name="g">The g.</param>
private void DrawCaptions(Graphics g)
{


if (g == null || capFont == null || clockBounds.IsEmpty || capSize.IsEmpty) return;


using (SolidBrush b = new SolidBrush(ColorCaptions))
{


g.DrawString("0", capFont, b, radius – capSize.Width/2, 1f);
g.DrawString("3", capFont, b, radius*2 – capSize.Width, radius – capSize.Height/2);
g.DrawString("6", capFont, b, radius – capSize.Width/2, radius*2 – capSize.Height + 1);
g.DrawString("9", capFont, b, 0f, radius – capSize.Height/2);


}


}


using (SolidBrush b = new SolidBrush(ColorCaptions)) // declares solid brush object for drawing of captions, "using" keyword will promise auto-disposing of "b" variable


 


g.DrawString("any text/digit", capFont, b, radius – capSize.Width/2, 1f); // each digit will be drawn in specific place relative to control bounds


/// <summary>
///
Draws the digital style.
/// </summary>
///
<param name="g">The g.</param>
private void DrawDigital(Graphics g)
{


if (g == null || digFont == null || clockBounds.IsEmpty) return;


string str = DateTime.Now.ToLongTimeString();  


using(Brush shadow = new SolidBrush(ColorCaptions))
        g.DrawString(str, digFont, shadow, -2f, -2f);


 


            using(Brush text = new SolidBrush(ControlPaint.LightLight(ColorCaptions)))
                   g.DrawString(str, digFont, text, -1f, -1f);


}


g.DrawString(str, digFont, shadow, -2f, -2f); // draws digits' shadow (2 padding left-top behind the text)


g.DrawString(str, digFont, text, -1f, -1f); // draws digits above the shadow (with correction of 1 pixel)


/// <summary>
///
Handles the Tick event of the timer1 control.
/// </summary>
///
<param name="sender">The source of the event.</param>
///
<param name="e">The <see cref="System.EventArgs"/> instance containing the event data.</param>
private void timer1_Tick(object sender, EventArgs e)
{


ClockEnabled = false;  


DateTime _time = DateTime.Now;
hour = _time.Hour;
min = _time.Minute;
sec = _time.Second;
milli = _time.Millisecond;
 


if (radius < 1 || secPen == null || minPen == null || hourPen == null || capFont == null || digFont == null)
      InitGrafix(false);


 


             Invalidate();


if (ClockTick != null)
      ClockTick(hour, min, sec, milli);
 


ClockEnabled = true;


}


Method "timer1_Tick" is raised by "Tick" event of timer. Tick-interval can be control thru property "ClockInterval".


Each tick sets hour, minutes and seconds values for clock hands.


ClockEnabled = false; // very crucial, if assignment process takes more then tick interval the execution of this method may cause stack-overflow (means that we stop timer at beginning of method and start it again at the end)


If some of graphic object is NULL or requires some initialization method calls to "InitGrafix" without invalidation of control.


After all parameters are set and checked for initialization method calls "Invalidate" method to redraw the clock surface.


If "ClockTick" event is attached to some event handle it will be called.


This control is very simple in everyday use, can be extended with new properties like: additional captions "1", "2", "4", "5", "7", "8", "10", "11", existing trigonometric method can be used to calculate theirs positions; new properties like "hand width" can be added also, gradient background with glow like in Google-Gadgets; digital clock can be improved too.  Control can be used as visual timer like "System.Windows.Forms.Timer".


Source code (zipped C# project) can be downloaded from here.
Any comments and code for WPF-Clock will be accepted with a big smile 🙂

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

one comment

  1. David30 בדצמבר 2007 ב 15:41

    My previous comment wasn't posted so I'll say it again: Thank you for great example, I'm downloaded your user control, learned a lot (as I wrote before I'm a beginner in .Net, moved from Delphi), do your example had great fluency on my custom control that I'm building for my project.
    I've found some links to WPF/GDI+ clocks: http://www.c-sharpcorner.com/UploadFile/devhamid/AnalogClock03262006134226PM/AnalogClock.aspx?ArticleID=e00bb8e1-4298-4c07-a9b1-f678b3b681ac, http://www.cubido.at/Blog/tabid/176/EntryID/81/Default.aspx, http://www.expresscomputeronline.com/20031006/techspace02.shtml
    maybe you can use the WPF from one of these… 🙂

    Reply