65.9K
CodeProject 正在变化。 阅读更多。
Home

如何构建时钟控件

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.67/5 (9投票s)

2002年9月7日

6分钟阅读

viewsIcon

107927

downloadIcon

3114

这是一个关于如何创建自定义时钟控件的快速指南

引言

在本教程中,我将构建一个高度可定制的时钟。我会尽可能详细地解释代码,以便您能轻松理解。不要被代码量吓到,因为它很容易理解,并且我几乎为每一行都添加了注释。

构建控件

首先,启动 Microsoft Visual C# 并创建一个新的 Windows 应用程序项目。从“文件”菜单中选择“添加新项”,然后在列表中选择“自定义控件”。从工具箱中将一个 Timer 拖到自定义控件的设计器上。将控件重命名为“Clock”,将 Timer 重命名为“timer”。将 Timer 的 Interval 设置为 1000(时钟的刷新时间)。

右键单击 Clock 的设计器,选择“查看代码”。在 Clock 的构造函数中,添加以下代码行:

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

new 语句创建了 Timer。我们将它的间隔设置为 1000 毫秒,启用它,并为 Tick 事件创建一个新的事件处理程序。这意味着 Timer 的 Tick 事件发生后,`Timer_Tick` 函数将被调用。

现在是时候创建 `Timer_Tick` 函数了。在构造函数之后,添加以下代码行:

private void Timer_Tick(object Sender, EventArgs e)
{
    //Refresh the Clock
    this.Invalidate();
    this.Update();
}

当您希望控件重新绘制自身时,调用 Invalidate 函数。

接下来是声明 Clock 的参数。我将在本教程后面解释每个参数的作用。将它们从源代码添加到 `GetPoint` 函数之后。

在下一节中,我将解释所有这些变量的作用。请仔细阅读变量名以理解它们的含义。如果您仍然难以理解它们的意思,请阅读每个变量上方的注释,并查看上面的图片。现在是时候在构造函数中初始化所有这些参数了。在 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;

我将变量初始化为默认值,这样即使它们未被更改,也有一个默认值。
请注意 `m_imgBackGround`,因为它在这里没有初始化。我将在窗体中创建 Clock 时为其设置值。

现在是棘手的部分,Paint 函数

在您的重写的 `Paint` 函数中,将代码行添加到 `base.OnPaint(pe);` 语句之后。
这些代码行如果阅读了上面的注释,就很容易理解。不要被冗长的代码弄糊涂,我将一步一步地解释它。您会注意到 `GetPoint` 函数的使用。我稍后会编写该函数以及对其进行解释。

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

我现在将解释我所做的工作。

我使用 `pe.Graphics` 来获取 Clock 的 Graphics 对象。首先,我绘制存储在 `m_imgBackGround` 变量中的背景图像,但前提是用户想要(`if` 语句)。

然后,在 `for` 语句中,我绘制时钟上的 60 个点,通过给定的公式计算它们的位置。不必担心公式,我将在本文后面解释它。如果计数器(`i`)能被 5 整除(`i%5==0`),则表示该点很重要,我们需要绘制一个大点以及对应的数字(`i/5`)。数字有偏移量,因为计算出的点位于文本的中心,而我需要数字左上角的点。这就是我在 `if (i%5==0)` 语句中所做的。

接下来是绘制指针本身。我将当前时间(`DateTime dt = DateTime.Now;`)获取到 `dt` 变量中。

我现在将解释计算小时指针角度的公式。如果您愿意,可以跳过这部分。

float HourAngle =360-(360*(dt.Hour)/12)+90;

让我们从中心开始,暂时只看 `360*(dt.Hour)/12` 部分。假设现在是 6 点(12 的一半)。这个表达式将返回 180,即 360 的一半。如果现在是 3 点(12 的四分之一),表达式将返回 90,即 360 的四分之一。正如您所见,这不是指针的真实角度。这是因为角度的方向是逆时针的。这就是为什么我从 360 中减去表达式,使其顺时针。这仍然不是我们需要的角度,因为在圆上“0”在右边,而在时钟上“0”(12)在顶部(相差 90 度)。这就是我添加 90 的原因。公式仍然与代码中的公式不同,代码中的公式是:

float min = ((float)dt.Minute)/60;
360-(360*(dt.Hour+min)/12)+90;

`min` 变量,正如您所看到的,总是小于 1。我将分钟转换为 0.something,与分钟成比例。我将 `min` 添加到 `dt.Hour` 中,这样随着分钟的推移,小时指针会向下一个小时倾斜,而不是停留在原来的位置。

接下来,我通过相同的公式计算分钟和秒的角度,并使用稍后将解释的 `GetPoint` 函数计算每个指针的终点。

接下来,我们使用 `SecondSize` 和其他(等于成员变量 `m_nSecondPointerSize`)中保存的特定宽度来绘制指针,通过以下代码行:`int SecondSize = m_nSecondPointerSize;`。我们绘制指针,第一个点是 `centerPoint`,然后是计算出的终点 `SecondEndPoint`,并使用画笔。

pe.Graphics.DrawLine(new Pen(m_brSecondPointer,SecondSize),centerPoint,SecondEndPoint);

在每个指针之后,我们在指针的尖端绘制一个椭圆来使其圆润。它的直径等于其指针的粗细。然后我们绘制中心的大圆点,最后,我们绘制底部的数字时钟。

现在是 `GetPoint` 函数了

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

我来解释一下。

想象一下圆。如果您知道圆上的一个点的角度(fAngle),那么它的坐标就是 Cos(fAngle) 和 Sin(fAngle)。在 `Math.Cos(2*Math.PI*fAngle/360)` Cos 语句中,我将角度从度转换为弧度。我们现在有了圆上点的坐标,但是我们的圆的半径不同于 1,所以我们将坐标乘以半径。然后我们加上中心的坐标,因为屏幕上的原点在左上角,而圆上的原点在圆的中心。

控件的最后一部分是 `ScaleToFit` 函数。

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;
}
它通过 `ScaleFactor` 按比例缩放时钟的所有数值属性,以便时钟能够适应指定的 `Size` 对象。

现在控件可以使用了,但它还没有包含在主窗体中。我将向您展示如何操作。转到 Form1[Design] 选项卡,右键单击并选择“查看代码”。在构造函数中,在 `InitializeComponent();` 函数之后添加以下代码行:

Clock clock = new Clock();
clock.Location = new Point(0,0);
clock.Size = new Size(300,300);
clock.bBackImage = false;
this.Controls.Add(clock);

如果您希望时钟具有背景图像,请将行:`clock.bBackImage = false;` 替换为:

clock.imgBackGround = Image.FromFile("/*the path name to a file*/");

如果您想缩放时钟,只需调用 `clock.ScaleToFit` 函数,并为其提供一个大小对象以适应。

如果您想修改时钟的属性,只需调用 `clock.propertyname` 并为其设置另一个值即可。

现在运行项目,享受这个漂亮的时钟吧。

© . All rights reserved.