How to build a Clock control






3.67/5 (9 votes)
Sep 7, 2002
6 min read

106795

3114
This is a quick guide on how to create your own customizable clock control
Introduction
In this tutorial I will build a very customizable clock. I will explain the code as much as I can so you will understand it easily. Don't get impressed by the amount of code because it is easy to understand and I have commented almost every line.
Building The Control
First start Microsoft Visual C# and create a new Windows Application Project. From the File Menu select Add New Item and choose Custom Control from the list. From the ToolBox drag a Timer onto the Design of the CustomContol. Rename the control to "Clock" and the Timer to "timer". Set the Interval value of the timer to 1000 (The refreshing time of the Clock).
Right Click on the Design of the Clock and choose View Code. In the Clock constructor Add the following lines of code:
public Clock()
{
//Set Up the Refresh Timer
timer = new System.Windows.Forms.Timer();
timer.Interval = 1000;
timer.Enabled = true;
timer.Tick+= new System.EventHandler(this.Timer_Tick);
}
The new
statement creates the Timer. We set it's interval to
1000 milliseconds, enable it and create a new Event Handler of the Tick Event. This means that the Timer_Tick
function is Called after each Interval
of the Timer.
Now it's time to create the Timer_Tick
Function. After the constructor,
add the following lines of code:
private void Timer_Tick(object Sender, EventArgs e)
{
//Refresh the Clock
this.Invalidate();
this.Update();
}
Call the Invalidate function when you want the control to repaint itself.
Next it`s time to declare the Parameters of the Clock. I will explain later
in this Tutorial what each one does. Add them from the source code after the
GetPoint
function
In the next section I will explain what do all those variables do. Read the names of the variables carefully to understand what they represent. If you syill have trouble understanding what they mean read the comments above each one and look at the picture above.It`s time to initialize all of those parameters in the constructor. Add these lines of code after the initialization of the timer.
//The point in the center of the clock
m_ptCenterPoint = new Point(70,70);
//The Radius of the perimeter pots
m_nClockRadius = 50;
//The Brush used to draw the perimeter dots
m_brPerimeterPoints = Brushes.Black;
//The Diameter of the small dots of the perimeter
m_nSmallDotSize = 2;
//The Diameter of the big dots of the perimeter
m_nBigDotSize = 4;
//The Offset of the perimeter numbers
m_nNumberOffset = -5;
//The Radius of the perimeter numbers
m_nNumberRadius = m_nClockRadius + 12;
//The Font used to draw the perimeter numbers
m_ftNumbers = new Font("Verdana",8);
//The Font used to draw the Bottom Number Clock
m_ftNumberClock = new Font("Verdana",8);
//Boolean value indicating if the perimeter numbers are visible
m_bNumbers = true;
//The Brush used to draw the perimeter numbers
m_brNumbers = Brushes.Black;
//The Length of the Hour Pointer
m_nHourPointerLength = 30;
//The Length of the Minute Pointer
m_nMinutePointerLength = 40;
//The Length of the Second Pointer
m_nSecondPointerLength = 45;
//The Brush Size of the Hour Pointer
m_nHourPointerSize = 10;
//The Brush Size of the Minute Pointer
m_nMinutePointerSize = 8;
//The Brush Size of the Second Pointer
m_nSecondPointerSize = 6;
//The Brush used to draw the Hour Pointer
m_brHourPointer = Brushes.Blue;
//The Brush used to draw the Minute Pointer
m_brMinutePointer = Brushes.Red;
//The Brush used to draw the Second Pointer
m_brSecondPointer = Brushes.Green;
//Boolean value indicating if the Bottom Number Clock is Visible
m_bNumberClock = true;
//Boolean value indicating if the Background Image is visible
m_bBackImage = true;
I initialized the variables to default values so that if they are not changed
they have a default value.
Be careful with the m_imgBackGround
because it is not initialized
here. I will set a value to it in the Form, when I create the Clock.
Now here comes the tricky part, the Paint function
Add the lines of code in your Overloaded Paint
function after
the base.OnPaint(pe);
statement.
The lines are self explanatory if you read the comments above each one. Don`t
get confused about the long code because I will explain it step by step. You
will notice the use of a GetPoint
function. I will write the code
of that function later as well as explain it to you.
//Set the Offsets
int offsetx = m_nOffset;
int offsety = m_nOffset;
//Copy the m_ptCenterPoint in centerPoint for use in this Function
Point centerPoint = m_ptCenterPoint;
//if the Boolean Back Image is true it draws the Back Image to fit the
// Client Rectangle of the control
if (this.m_bBackImage)
pe.Graphics.DrawImage(this.imgBackGround,0,0,ClientRectangle.Width,
ClientRectangle.Height);
//This for statement Draws the perimeter Dots and Numbers
for(int i=1;i<=60;i++)
{
//This is the Angle of the Current Number to Draw
//I calculate this Angle by a formula that I came up with after a good thinking process
//I will use it in other ways to calculate the different angles that I need
float NumberAngle =360-(360*(i/5)/12)+90;
//Copy the NumberRadius for use in this function
int NumberRadius = m_nNumberRadius;
//Calculate the Pozition of the Number
Point NumberPoint = GetPoint(centerPoint,NumberRadius,NumberAngle);
//This is the Angle of the Current Dot
float DotAngle =360-(360*(i)/60)+90;
//Copy the Dot Radius for use in this function
int DotRadius = m_nClockRadius;
//Calculate the Point of the Dot
Point DotPoint = GetPoint(centerPoint,DotRadius,DotAngle);
//Copy the DotSizes for use in this function
int SmallDotSize = m_nSmallDotSize;
int BigDotSize = m_nBigDotSize;
//Draws the current small point
pe.Graphics.FillEllipse(m_brPerimeterPoints,DotPoint.X-SmallDotSize/2,
DotPoint.Y-SmallDotSize/2,SmallDotSize,SmallDotSize);
//if it`s a big Dot
if (i%5==0)
{
//if the Numbers are Visible Draw them at the calculated position
if (m_bNumbers)
pe.Graphics.DrawString((i/5).ToString(),m_ftNumbers,m_brNumbers,
NumberPoint.X+m_nNumberOffset,NumberPoint.Y+m_nNumberOffset);
//Draw the Big Dots
pe.Graphics.FillEllipse(m_brPerimeterPoints,DotPoint.X-BigDotSize/2,
DotPoint.Y-BigDotSize/2,BigDotSize,BigDotSize);
}
}
//Get the Current Local Time
DateTime dt = DateTime.Now;
//calculate the min value for use in the HourAngle
float min = ((float)dt.Minute)/60;
//Calculate the Angle of the Hour Pointer
float HourAngle =360-(360*(dt.Hour+min)/12)+90;
//Calculate the Angle of the Minute Pointer
float MinuteAngle =360-(360*dt.Minute/60)+90;
//Calculate the Angle of the Second Pointer
float SecondAngle =360-(360*dt.Second/60)+90;
//Calculate the EndPoint of the Hour Pointer
Point HourEndPoint = GetPoint(centerPoint,m_nHourPointerLength,HourAngle);
//Calculate the EndPoint of the Minute Pointer
Point MinuteEndPoint = GetPoint(centerPoint,m_nMinutePointerLength,MinuteAngle);
//Calculate the EndPoint of the Second Pointer
Point SecondEndPoint = GetPoint(centerPoint,m_nSecondPointerLength,SecondAngle);
//Copy the Sizes for use in this function
int SecondSize = m_nSecondPointerSize;
int MinuteSize = m_nMinutePointerSize;
int HourSize = m_nHourPointerSize;
//Draw the Second Pointer Line
pe.Graphics.DrawLine(new Pen(m_brSecondPointer,SecondSize),centerPoint,SecondEndPoint);
//Draw the Second Pointer Top
pe.Graphics.FillEllipse(m_brSecondPointer,SecondEndPoint.X-SecondSize/2,
SecondEndPoint.Y-SecondSize/2,SecondSize,SecondSize);
//Draw the Minute Pointer Line
pe.Graphics.DrawLine(new Pen(m_brMinutePointer,MinuteSize),centerPoint,MinuteEndPoint);
//Draw the Minute Pointer Top
pe.Graphics.FillEllipse(m_brMinutePointer,MinuteEndPoint.X-MinuteSize/2,
MinuteEndPoint.Y-MinuteSize/2,MinuteSize,MinuteSize);
//Draw the Hour Pointer Line
pe.Graphics.DrawLine(new Pen(m_brHourPointer,HourSize),centerPoint,HourEndPoint);
//Draw the Hour Pointer Top
pe.Graphics.FillEllipse(m_brHourPointer,HourEndPoint.X-HourSize/2,
HourEndPoint.Y-HourSize/2,HourSize,HourSize);
//The size of the Center Point
int CenterPointSize = m_nHourPointerSize;
//Draw the Center Point to cover the ends of the Pointers
pe.Graphics.FillEllipse(Brushes.Black,centerPoint.X-CenterPointSize/2,
centerPoint.Y-CenterPointSize/2,CenterPointSize,CenterPointSize);
//if the Number Clock is Visible Draw It
if (m_bNumberClock)
pe.Graphics.DrawString(String.Format("{0:}:{1:}:{2:}",dt.Hour,dt.Minute,dt.Second),
m_ftNumberClock,Brushes.Red,centerPoint.X-35*m_ftNumberClock.Size/12,
centerPoint.Y+m_nNumberRadius + m_ftNumbers.Size+5);
I will now explain what I have done.
I am using the pe.Graphics
to receive the Graphics object of
the Clock. At first I draw the Background image stored in the m_imgBackGround
variable but only if the user wants to (the if
statement).
Then, in the for
statement I draw the 60 dots on the clock calculating
their position by the formula there. Don't worry about the formula because I
will explain it later in this article .If 5 divides the counter(i
)
then that means that that is an important dot and we must draw a big dot and
the number corresponding with (i/5
). The numbers have an offset
because the point calculated is in the middle of the text ant I need a point
at the top-left of the number. That is what I have done in the if (i%5==0)
statement.
Next it`s time to draw the Pointers themselves. I get the Curent time (DateTime
dt = DateTime.Now;
) in the dt variable;
I will now explain how the formula of calculating the angle of the Hour Pointer works. You can skip this part if you want to.
float HourAngle =360-(360*(dt.Hour)/12)+90;
Let`s take it from the center and look at the 360*(dt.Hour)/12
part for now. Let's say that it's 6 o'clock (half of 12). this expression would
return 180 wich means half of 360. If it was 3 o'clock (a quarter of 12) the expression
would return 90 wich is a quarter of 360. As you can see this isn`t the real
angle of the pointer. This is because the direction of the angles
is counter clockwise. This is why I subtracted the expression from 360 to make
it clockwise. This is still not the angle that we are looking for because on
the circle "0" is on the right wile on the clock "0"(12)
is on top (a 90 degrees difference). This is why I added 90. The formula isn't
still as the one in the code which is:
float min = ((float)dt.Minute)/60; 360-(360*(dt.Hour+min)/12)+90;
The min variable is, as you can see always smaller than 1. I transformed the minutes in 0.something proportional to the Minutes. I added min to dt.Hour
so that the Hour Pointer tilts towards the next Hour as the minutes go by, it doesn't
stick on it's position.
Next I calculate the Minute and Second Angles by the same formula ,and the
End Points of each Pointer with the GetPoint
function that I will
explain later.
Next I draw the pointers with their specific Widths kept in SecondSize
and the other, that are equal to the member variables m_nSecondPointerSize;
through the lines of code:int SecondSize = m_nSecondPointerSize;
I draw the pointers having the centerPoint
as the first point and
the calculated end points SecondEndPoint
and their brushes using:
pe.Graphics.DrawLine(new Pen(m_brSecondPointer,SecondSize),centerPoint,SecondEndPoint);
After each Pointer I draw an elipse on the tip of the pointers to round their aspect. It has the diameter equal to the thickness of it`s pointer. Then I draw the big dot in the Center, and as the last piece, I draw the number clock at the bottom.
Now it's time for the GetPoint
function
public Point GetPoint(Point ptCenter, int nRadius, float fAngle)
{
float x = (float)Math.Cos(2*Math.PI*fAngle/360)*nRadius+ptCenter.X;
float y = -(float)Math.Sin(2*Math.PI*fAngle/360)*nRadius+ptCenter.Y;
return new Point((int)x,(int)y);
}
Let me explain this now.
Think of the circle. If you have a point on the circle and
you know it's angle (fAngle) then it's coordinates are Cos(fAngle) and Sin(fAngle).
In the Math.Cos(2*Math.PI*fAngle/360)
Cos statement I transform
from Degrees to Radians. We now have the coordinates of the point on the
circle but our circle's radius is different from 1 so I multiply the coordinates
with the radius. I then add the center`s coordinates because the point of origin
on our screen is on the top-left while on the circle is in the
center of the circle.
The last piece of the control is the ScaleToFit
function.
public void ScaleToFit(System.Drawing.Size sSize)
{
float ScaleFactor = (float)sSize.Width/(140);
m_nClockRadius =(int)(50*ScaleFactor);
m_nOffset = 0;//(int)(20*ScaleFactor);
m_nSmallDotSize = (int) ( 2*ScaleFactor);
m_nBigDotSize = (int) ( 4*ScaleFactor);
m_nNumberOffset = (int) ( -5*ScaleFactor);
m_nNumberRadius = (int) ( m_nClockRadius + 12*ScaleFactor);
m_ftNumbers = new Font("Verdana",(int)(8*ScaleFactor));
m_ftNumberClock = new Font("Verdana",(int)(8*ScaleFactor));
m_nHourPointerLength = (int) ( 30*ScaleFactor);
m_nMinutePointerLength = (int) ( 40*ScaleFactor);
m_nSecondPointerLength = (int) ( 45*ScaleFactor);
m_nHourPointerSize = (int) ( 10*ScaleFactor);
m_nMinutePointerSize = (int) ( 8*ScaleFactor);
m_nSecondPointerSize = (int) ( 6*ScaleFactor);
m_ptCenterPoint.X = (int)(sSize.Width/2);
m_ptCenterPoint.Y = (int)(sSize.Width/2);
this.m_sSize = sSize;
}
It scales all of the clock`s numerical properties by the ScaleFactor
so that the
clock will fit into the specified Size
object.
Now the control is ready to use but it isn't included in the Main form. I will
show you how to do this. Go to the Form1[Design] tab ,right click and choose View Code.
In the Constructor, after the InitializeComponent();
function add
the folowing lines of code:
Clock clock = new Clock();
clock.Location = new Point(0,0);
clock.Size = new Size(300,300);
clock.bBackImage = false;
this.Controls.Add(clock);
If you want the clock to have a Background image replace the line : clock.bBackImage
= false;
with the line :
clock.imgBackGround = Image.FromFile("/*the path name to a file*/");
If you want to scale the clock you just have to call the clock.ScaleToFit
function and give it a size ofject for the clock to fit in.
If you want to modify the properties of the clock do so by calling the clock.propertyname
and set it another value.
Now Run the project and enjoy the nice clock.