今天学习GDI+,试着想写一个模拟时钟的小程序,原以为很简单实现;但其实还有些复杂,特别是利用三角函数的那部分,让我四处找资料恶补了一下高中数学才算弄清楚,现在就回顾一下这个程序吧.
程序的目的是要模拟出时钟的效果,那首先就是要画出这个时钟的样子。不考虑美观,一个时钟最简单的组成是一个圆形的表盘,三根直线代表的时针、分针和秒针。
看起来很简单吧,但要怎么样画呢?让我们一步一步来吧:
1.画表盘
Graphics g = this.CreateGraphics(); //创建一个Graphics对象 Pen myPen = new Pen(Color.Blue,1); //创建一个自定义画笔对象 //创建一个正方形rect int myRect_X = this.ClientRectangle.Right/4; int myRect_Y = this.ClientRectangle.Bottom/4; int myRect_Width; int myRect_Height; if(this.ClientRectangle.Height< this.ClientRectangle.Width) { myRect_Width = this.ClientRectangle.Height/2; myRect_Height = this.ClientRectangle.Height/2; } else { myRect_Width = this.ClientRectangle.Width/2; myRect_Height = this.ClientRectangle.Width/2; } Rectangle rect = new Rectangle(myRect_X,myRect_Y,myRect_Width,myRect_Height); g.DrawEllipse(myPen,rect); //画出一个内切于矩形rect的圆
Graphics.DrawEllipse方法有几种重载方式,其中较常用的就是Graphics.DrawEllipse(Pen,Rectangle),这个重载方法需要提供一个Rectangle参数,然后根据这个矩形的形状内切出椭圆,若这个矩形是正方形,则切出来的是一个正圆。
这段程序最关键的地方就在于得到这个矩形了,因为它间接决定了表盘的位置与大小,Rectangle的构造函数需要四个参数,矩形左上角的X、Y坐标,宽度与高度。
这里的ClientRectangle所代表的是控件的工作区,它也是一个Rectangle类型的对象,是指控件的边界减去非工作区元素(如滚动条、边框、标题栏和菜单)。相应的Right,Bottom,Width,Height分别代表了右边缘的X坐标,下边缘的Y坐标,宽度与高度。
为了使这个表盘适应窗口的变化,这里将这些值全部设置为相对与ClientRectangle相关属性的值,如下图:
这样当窗口大小变化时,这个圆的外切矩形也随之改变,假如这个窗口的宽度比高度大太多,或反之,则可能会出现表盘的部分超出工作区域无法显示,为了防止这种情况,加入了条件判断语句。如果宽度大于高度,则圆的直径取高度值;反之则取宽度值。当然高度等于宽度时,圆是在工作区域正中间。
2.画表针
将表盘画好后,就可以开始画表针了,这也是最复杂的一步
//得到中心点坐标 int centerPoint_X = rect.Right - rect.Width/2; int centerPoint_Y = rect.Bottom - rect.Height/2; Point centerPoint = new Point(centerPoint_X,centerPoint_Y); int s_Len = rect.Width*0.45; //秒针长度 int m_Len = rect.Width*0.35//分针长度 int l_Len = rect.Width*0.25 //时针长度 //得到当前时间 int h = DateTime.Now.Hour; int m = DateTime.Now.Minute; int s = DateTime.Now.Second; //得到秒针顶点坐标 int sec_X = (int)(centerPoint.X + Math.Sin(6*Math.PI/180*s)*s_Len); int sec_Y = (int)(centerPoint.Y - Math.Cos(6*Math.PI/180*s)*s_Len); Point secPoint = new Point(sec_X,sec_Y); //得到分针顶点坐标 int min_X = (int)(centerPoint.X + Math.Sin(6*Math.PI/180*m)*m_Len); int min_Y = (int)(centerPoint.Y - Math.Cos(6*Math.PI/180*m)*m_Len); Point minPoint = new Point(min_X,min_Y); //得到时针顶点坐标 int hour_X = (int)(centerPoint.X + Math.Sin(h*30+m/2)*Math.PI/180*h_Len); int hour_Y = (int)(centerPoint.Y - Math.Cos(h*30+m/2)*Math.PI/180*h_Len); Point hourPoint = new Point(hour_X,hour_Y); //以不同的颜色连接 myPen = new Pen(Color.Blue,1); g.DrawLine(myPen,centerPoint,secPoint); //连接原点和秒针顶点 myPen = new Pen(Color.Green,2); g.DrawLine(myPen,centerPoint,minPoint); //连接原点和分针顶点 myPen = new Pen(Color.Red,3); g.DrawLine(myPen,centerPoint,hourPoint); //连接原点和时针顶点 g.Dispose(); //释放Graphics对象使用的所有资源
要画出一条直线,必须要得到这个直线的两个顶点坐标,在这个程序里,三根表针有一个共同的原点(表盘中心点),然后如何得到另外一个点就是我们接下来要做的工作了。
我们先来回顾一下三角函数的概念,sin值是 对边/斜边 得到,cos值是 邻边/斜边 得到,要求出这个点的坐标就是求出这个三角形的两条边,一条边的长度为X,另一条的长度为Y。现在我们已经知道的是斜边的长度,要求出这两条边,就要先知道弧度值。
我们先来考虑最长的秒针,它每一秒都会移动一个角度,这个角度我们可以计算出来:
360/60=6
得到6度,这不难理解,因为整个圆是360度,一般的时钟在相邻小时之间又有5个刻度,所以一共是12*5=60个刻度,然后用360/60=6得到秒针一秒走6度,在程序里必须要将角度转化成弧度才能进行sin运算,所以还要乘以 PI/180(PI为圆周率)。
有了这个弧度值,我们就可以得到每一秒秒针的顶点坐标了(以在第一区间考虑):
sec_X = centerPoint.X + sin(6*PI/180)*当前秒钟值*s_Len sce_Y = centerPoint.Y - cos(6*PI/180)*当前秒钟值*s_Len
接下来处理时针,因为我们这只是个很简单的模拟,所以就不需要对细节作过多的处理,就让它每过一分钟就跳到下一个刻度,这样求时针的顶点坐标和求秒针的就是一样的算法:
min_X = centerPoint.X + sin(6*PI/180)*当前分钟值*m_Len min_Y = centerPoint.Y - cos(6*PI/180)*当前分钟值*m_Len
时针的处理方式要不同,因为时针的取值只有12个数字,而分钟和秒针都有60个数字,如果以之前的方式处理时针,效果就是时针到点的那一瞬间它一下子转30度,当然不会有这样效果的时钟。那么我们该如何处理这个棘手的时针呢?
假如现在是06:45:30,在整6点时它的度数是30*6,整7点是30*7,为了不至于将改变全在最后刻发生,我们加入时针值来参与计算,这样当前时间就是h*30+m/2,m为60时 m/2=30 ,也就是 m/2这个值在0--30之间,当角度变化为30度时,就指到了下一个整点。
这样我们得到了时针的算法:
hour_X = centerPoint.X + sin((h*30+m/2)*PI/180)*h_Len hour_Y = centerPoint.Y - cos((h*30+m/2)*PI/180)*h_Len
最关键的代码处理了,剩下的工作就简单了,我们在程序里加入一个标签lblTime,来显示当前时间:
string TimeInString = ""; int hour = DateTime.Now.Hour; int min = DateTime.Now.Minute; int sec = DateTime.Now.Second; TimeInString = (hour<10)? "0"+hour.ToString():hour.ToString(); TimeInString += (min<10)? "0"+min.ToString():min.ToString(); TimeInString == (sec<10)? "0"+sec.ToString():sec.ToString(); lblTime.Text = TimeInString;
程序最后运行效果: