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()
{
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)
{
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.
m_ptCenterPoint = new Point(70,70);
m_nClockRadius = 50;
m_brPerimeterPoints = Brushes.Black;
m_nSmallDotSize = 2;
m_nBigDotSize = 4;
m_nNumberOffset = -5;
m_nNumberRadius = m_nClockRadius + 12;
m_ftNumbers = new Font("Verdana",8);
m_ftNumberClock = new Font("Verdana",8);
m_bNumbers = true;
m_brNumbers = Brushes.Black;
m_nHourPointerLength = 30;
m_nMinutePointerLength = 40;
m_nSecondPointerLength = 45;
m_nHourPointerSize = 10;
m_nMinutePointerSize = 8;
m_nSecondPointerSize = 6;
m_brHourPointer = Brushes.Blue;
m_brMinutePointer = Brushes.Red;
m_brSecondPointer = Brushes.Green;
m_bNumberClock = true;
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.
int offsetx = m_nOffset;
int offsety = m_nOffset;
Point centerPoint = m_ptCenterPoint;
if (this.m_bBackImage)
pe.Graphics.DrawImage(this.imgBackGround,0,0,ClientRectangle.Width,
ClientRectangle.Height);
for(int i=1;i<=60;i++)
{
float NumberAngle =360-(360*(i/5)/12)+90;
int NumberRadius = m_nNumberRadius;
Point NumberPoint = GetPoint(centerPoint,NumberRadius,NumberAngle);
float DotAngle =360-(360*(i)/60)+90;
int DotRadius = m_nClockRadius;
Point DotPoint = GetPoint(centerPoint,DotRadius,DotAngle);
int SmallDotSize = m_nSmallDotSize;
int BigDotSize = m_nBigDotSize;
pe.Graphics.FillEllipse(m_brPerimeterPoints,DotPoint.X-SmallDotSize/2,
DotPoint.Y-SmallDotSize/2,SmallDotSize,SmallDotSize);
if (i%5==0)
{
if (m_bNumbers)
pe.Graphics.DrawString((i/5).ToString(),m_ftNumbers,m_brNumbers,
NumberPoint.X+m_nNumberOffset,NumberPoint.Y+m_nNumberOffset);
pe.Graphics.FillEllipse(m_brPerimeterPoints,DotPoint.X-BigDotSize/2,
DotPoint.Y-BigDotSize/2,BigDotSize,BigDotSize);
}
}
DateTime dt = DateTime.Now;
float min = ((float)dt.Minute)/60;
float HourAngle =360-(360*(dt.Hour+min)/12)+90;
float MinuteAngle =360-(360*dt.Minute/60)+90;
float SecondAngle =360-(360*dt.Second/60)+90;
Point HourEndPoint = GetPoint(centerPoint,m_nHourPointerLength,HourAngle);
Point MinuteEndPoint = GetPoint(centerPoint,m_nMinutePointerLength,MinuteAngle);
Point SecondEndPoint = GetPoint(centerPoint,m_nSecondPointerLength,SecondAngle);
int SecondSize = m_nSecondPointerSize;
int MinuteSize = m_nMinutePointerSize;
int HourSize = m_nHourPointerSize;
pe.Graphics.DrawLine(new Pen(m_brSecondPointer,SecondSize),centerPoint,SecondEndPoint);
pe.Graphics.FillEllipse(m_brSecondPointer,SecondEndPoint.X-SecondSize/2,
SecondEndPoint.Y-SecondSize/2,SecondSize,SecondSize);
pe.Graphics.DrawLine(new Pen(m_brMinutePointer,MinuteSize),centerPoint,MinuteEndPoint);
pe.Graphics.FillEllipse(m_brMinutePointer,MinuteEndPoint.X-MinuteSize/2,
MinuteEndPoint.Y-MinuteSize/2,MinuteSize,MinuteSize);
pe.Graphics.DrawLine(new Pen(m_brHourPointer,HourSize),centerPoint,HourEndPoint);
pe.Graphics.FillEllipse(m_brHourPointer,HourEndPoint.X-HourSize/2,
HourEndPoint.Y-HourSize/2,HourSize,HourSize);
int CenterPointSize = m_nHourPointerSize;
pe.Graphics.FillEllipse(Brushes.Black,centerPoint.X-CenterPointSize/2,
centerPoint.Y-CenterPointSize/2,CenterPointSize,CenterPointSize);
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;
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.