GDI绘制时钟效果,与系统时间保持同步,基于Winform
2018年工作之余,想起来捡起GDI方面的技术,特意在RichCodeBox项目中做了两个示例程序,其中一个就是时钟效果,纯C#开发。这个CSharpQuartz是今天上午抽出一些时间,编写的,算是偷得浮生半日休闲吧。先来看看效果图吧:
这是直接在Winform的基础上进行绘制的。接下来,我对时钟进行了封装,封装成一个名为CSharpQuartz的类,效果如下:
这是把时钟封装后,实现的一种效果,CSharpQuartz内部开辟了一个线程,与系统时间,保持同步,每秒刷新一次。所采用的技术也就是GDI和多线程及事件委托。把时钟封装成对象后,还为其添加了OnChanged事件,用于对象提供外部
处理之用。接下来就简单的说下,做次小程序的一些准备工作吧。
这也是最近偶尔听到有朋友问怎样做时钟的事,想来,其实也简单的,只是需要一些耐心和细心,这里主要还利用一些三角函数进行计算。上图看似很简单,其实也有很多小细节需要注意。我就把大致绘制的过程简单说下:
首先,我们需要定义一个圆,来作为时钟的轮廓,这里是通过设置时钟的直径及winform的宽高,来计算出时钟在窗体居中的位置。绘制圆的代码就更简单了
float w = 300f, h = 300f; float x = (this.Width - w) / 2; float y = (this.Height - h) / 2; float d = w;//直径 float r = d / 2;//半径 graphics.DrawEllipse(pen, new RectangleF(x, y, w, h));//绘制圆
接下来,我们需要计算圆的一周遍布的12个时间点。然后把这些时间点和圆心连在一起,就形成了上图我们看到的不同时间点的线段。圆心的查找非常简单,圆心的坐标点,其实就是x轴+半径r,y轴+半径r:
PointF pointEclipse = new PointF(x + r, y + r);
开始分表绘制12个点与圆心的连线,我这里是以9点为起点,此时,脑海中呈现这样的画面
时针一圈12格,每格也就是 Math.PI/6
比如我们计算10点在圆上的坐标P10.
10点所在的点,与x轴的夹角呈30度。那么10点在x上的坐标应该是,x1=x+r-r*Cos(30),以此类推,不难求出12个点的位置,具体代码如下:
/// <summary> /// <![CDATA[画时刻线 这里是以9点这个时间坐标为起点 进行360度]]> /// </summary> /// <param name="graphics"><![CDATA[画布]]></param> /// <param name="x"><![CDATA[圆x坐标]]></param> /// <param name="y"><![CDATA[圆y坐标]]></param> /// <param name="r"><![CDATA[圆x坐标]]></param> private void DrawQuartzLine(Graphics graphics, float x, float y, float r) { //圆心 PointF pointEclipse = new PointF(x + r, y + r); float labelX, labelY;//文本坐标 float angle = Convert.ToSingle(Math.PI / 6);//角度 30度 Font font = new Font(FontFamily.GenericSerif, 12); float _x, _y;//圆上的坐标点 using (Brush brush = new System.Drawing.SolidBrush(Color.Red)) { using (Pen pen = new Pen(Color.Black, 0.6f)) { //一天12H,将圆分为12份 for (int i = 0; i <= 11; i++) { PointF p10;//圆周上的点 float pAngle = angle * i; float x1, y1; //三、四象限 if (pAngle > Math.PI) { if ((pAngle - Math.PI) > Math.PI / 2)//钝角大于90度 { //第三象限 x1 = Convert.ToSingle(r * Math.Cos(Math.PI * 2 - pAngle)); y1 = Convert.ToSingle(r * Math.Sin(Math.PI * 2 - pAngle)); _x = x + r - x1; _y = y + r + y1; labelX = _x - 8; labelY = _y; } else { //第四象限 x1 = Convert.ToSingle(r * Math.Cos(pAngle - Math.PI)); y1 = Convert.ToSingle(r * Math.Sin(pAngle - Math.PI)); _x = x + r + x1; _y = y + r + y1; labelX = _x; labelY = _y; } } //一、二象限 else if (pAngle > Math.PI / 2)//钝角大于90度 { //第一象限 x1 = Convert.ToSingle(r * Math.Cos(Math.PI - pAngle)); y1 = Convert.ToSingle(r * Math.Sin(Math.PI - pAngle)); _x = x + r + x1; _y = y + r - y1; labelX = _x; labelY = _y - 20; } else { //第二象限 x1 = Convert.ToSingle(r * Math.Cos(pAngle)); y1 = Convert.ToSingle(r * Math.Sin(pAngle)); _x = x + r - x1; _y = y + r - y1; labelX = _x - 15; labelY = _y - 20; } //上半圆 分成12份,每份 30度 if (i + 9 > 12) { graphics.DrawString((i + 9 - 12).ToString(), font, brush, labelX, labelY); } else { if (i + 9 == 9) { labelX = x - 13; labelY = y + r - 6; } graphics.DrawString((i + 9).ToString(), font, brush, labelX, labelY); } p10 = new PointF(_x, _y); graphics.DrawLine(pen, pointEclipse, p10); } } } }
为了辅助计算,我又添加了x轴与y轴的线,就是我们在效果图中看到的垂直于水平两条线段。
/// <summary> /// <![CDATA[绘制象限]]> /// </summary> /// <param name="graphics"><![CDATA[画布]]></param> /// <param name="x"><![CDATA[圆x坐标]]></param> /// <param name="y"><![CDATA[圆y坐标]]></param> /// <param name="r"><![CDATA[圆半径]]></param> private void DrawQuadrant(Graphics graphics, float x, float y, float r) { float w = r * 2; float extend = 100f; using (Pen pen = new Pen(Color.Black, 1)) { #region 绘制象限 PointF point1 = new PointF(x - extend, y + r);// PointF point2 = new PointF(x + w + extend, y + r); PointF point3 = new PointF(x + r, y - extend);// PointF point4 = new PointF(x + r, y + w + extend); graphics.DrawLine(pen, point1, point2); graphics.DrawLine(pen, point3, point4); #endregion 绘制象限 } }
接下来,该绘制指针(时、分、秒),就是我们效果图中看到的,红绿蓝,三条长短不一的线段,秒针最长,这是和本地系统时间同步,所以要根据当前时间,计算出指针所在的位置。
/// <summary> /// <![CDATA[绘制时、分、秒针]]> /// </summary> /// <param name="graphics"><![CDATA[画布]]></param> /// <param name="x"><![CDATA[圆x坐标]]></param> /// <param name="y"><![CDATA[圆y坐标]]></param> /// <param name="r"><![CDATA[圆半径]]></param> private void DrawQuartzShot(Graphics graphics, float x, float y, float r) { if (this.IsHandleCreated) { this.Invoke(new Action(() => { //当前时间 DateTime dtNow = DateTime.Now; int h = dtNow.Hour; int m = dtNow.Minute; int s = dtNow.Second; float ha = Convert.ToSingle(Math.PI * 2 / 12);//每小时所弧度 360/12格=30 float hm = Convert.ToSingle(Math.PI * 2 / 60); float hs = Convert.ToSingle(Math.PI * 2 / 60); float x1, y1, offset = 60f; using (Pen pen = new Pen(Color.Green, 4)) { //时针 h = h >= 12 ? h - 12 : h; double angle = h * ha;//当前时针所占弧度 x1 = x + r + Convert.ToSingle(Math.Sin(angle) * (r - offset));//通过调整offset的大小,可以控制时针的长短 y1 = y + r - Convert.ToSingle(Math.Cos(angle) * (r - offset)); //圆心 PointF pointEclipse = new PointF(x + r, y + r); PointF pointEnd = new PointF(x1, y1); graphics.DrawLine(pen, pointEclipse, pointEnd);//画45度角 //分针 using (Pen penYellow = new Pen(Color.Red, 2)) { offset = 30; //分 double angelMinutes = hm * m;//每分钟弧度 x1 = x + r + Convert.ToSingle(Math.Sin(angelMinutes) * (r - offset));//通过调整offset的大小,可以控制时针的长短 y1 = y + r - Convert.ToSingle(Math.Cos(angelMinutes) * (r - offset)); graphics.DrawLine(penYellow, pointEclipse, new PointF(x1, y1));//画45度角 } //秒针 using (Pen penYellow = new Pen(Color.Blue, 2)) { offset = 20; //分 double angelSeconds = hs * s;//每秒钟弧度 x1 = x + r + Convert.ToSingle(Math.Sin(angelSeconds) * (r - offset));//通过调整offset的大小,可以控制时针的长短 y1 = y + r - Convert.ToSingle(Math.Cos(angelSeconds) * (r - offset)); graphics.DrawLine(penYellow, pointEclipse, new PointF(x1, y1));//画45度角 } } this.lblTime.Text = string.Format("当前时间:{0}:{1}:{2}", h, m, s); })); } }
最后,开辟一个线程,来同步更新时针的状态。
/// <summary> /// /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Quartz_Load(object sender, EventArgs e) { timer = new Thread(() => { if (_graphics == null) { _graphics = this.CreateGraphics(); _graphics.SmoothingMode = SmoothingMode.HighQuality; //高质量 _graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; //高像素偏移质量 } while (true) { _graphics.Clear(this.BackColor); DrawCaller(_graphics); System.Threading.Thread.Sleep(1000); } }); timer.IsBackground = true; }
每秒钟,更新一次,其实就是重绘。
完成了以上几个步骤,我们就完成GDI绘制时钟的工作,后来,把它封装成一个名为CSharpQuartz的对象,具体代码如下:
using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; /*================================================================================================= * * Title:C#开发的简易时钟 * Author:李朝强 * Description:模块描述 * CreatedBy:lichaoqiang.com * CreatedOn: * ModifyBy:暂无... * ModifyOn: * Company:河南天一文化传播股份有限公司 * Blog:http://www.lichaoqiang.com * Mark: * *** ================================================================================================*/ namespace WinformGDIEvent.Sample { /// <summary> /// <![CDATA[CSharpQuarz GDI时钟]]> /// </summary> public class CSharpQuartz : IDisposable { /// <summary> /// 定时器 /// </summary> private Thread timer = null; /// <summary> /// X坐标 /// </summary> public float X { get; private set; } /// <summary> /// Y坐标 /// </summary> public float Y { get; private set; } /// <summary> /// 半径 /// </summary> private float r; /// <summary> /// 画布 /// </summary> private Graphics Graphics = null; /// <summary> /// 直径 /// </summary> public float Diameter { get; private set; } /// <summary> /// /// </summary> public Form CurrentWinform { get; private set; } /// <summary> /// /// </summary> private EventHandler _OnChanged; /// <summary> /// 事件,时钟状态更新后,当前频次1秒钟 /// </summary> public event EventHandler OnChanged { add { this._OnChanged += value; } remove { this._OnChanged -= value; } } /// <summary> /// 构造函数 /// </summary> CSharpQuartz() { // timer = new Thread((() => { if (Graphics == null) { Graphics = CurrentWinform.CreateGraphics();//创建画布 Graphics.SmoothingMode = SmoothingMode.HighQuality; //高质量 Graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; //高像素偏移质量 } while (true) { //清除画布颜色,以窗体底色填充 Graphics.Clear(CurrentWinform.BackColor); DrawCaller();//绘制时钟 //事件 if (_OnChanged != null) _OnChanged(this, null); System.Threading.Thread.Sleep(1000); } })); timer.IsBackground = true; } /// <summary> /// <![CDATA[构造函数]]> /// </summary> /// <param name="x"><![CDATA[圆x坐标]]></param> /// <param name="y"><![CDATA[圆y坐标]]></param> /// <param name="d"><![CDATA[圆直径]]></param> public CSharpQuartz(Form form, float x, float y, float d) : this() { this.CurrentWinform = form; this.X = x; this.Y = y; this.Diameter = d; r = d / 2; } /// <summary> /// /// </summary> public void Start() { if (timer.IsAlive == false) timer.Start();//启动工作线程 } /// <summary> /// 终止 /// </summary> public void Stop() { if (timer.IsAlive == true) timer.Abort();//终止工作线程 } /// <summary> /// <![CDATA[调用绘图]]> /// </summary> private void DrawCaller() { Graphics.SmoothingMode = SmoothingMode.HighQuality; //高质量 Graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; //高像素偏移质量 using (Pen pen = new Pen(Color.Red, 2)) { if (this.CurrentWinform.IsHandleCreated) { this.CurrentWinform.Invoke(new Action(() => { //绘制圆 Graphics.DrawEllipse(pen, new RectangleF(X, Y, Diameter, Diameter)); //绘制象限 DrawQuadrant(); //绘制时、分、秒等针 DrawQuartzShot(); //绘制时刻线 DrawQuartzLine(); //写入版本信息 WriteVersion(); })); } } } /// <summary> /// <![CDATA[绘制象限]]> /// </summary> private void DrawQuadrant() { #region 绘制象限 float w = Diameter; float extend = 100f; using (Pen pen = new Pen(Color.Black, 1)) { PointF point1 = new PointF(X - extend, Y + r);// PointF point2 = new PointF(X + w + extend, Y + r); PointF point3 = new PointF(X + r, Y - extend);// PointF point4 = new PointF(X + r, Y + w + extend); Graphics.DrawLine(pen, point1, point2); Graphics.DrawLine(pen, point3, point4); } #endregion 绘制象限 } /// <summary> /// <![CDATA[绘制时、分、秒针]]> /// </summary> private void DrawQuartzShot() { //当前时间 DateTime dtNow = DateTime.Now; int h = dtNow.Hour; int m = dtNow.Minute; int s = dtNow.Second; float ha = Convert.ToSingle(Math.PI * 2 / 12);//每小时所弧度 360/12格=30 float radian = Convert.ToSingle(Math.PI * 2 / 60);//分秒偏移弧度 float x1, y1, offset = 60f; using (Pen pen = new Pen(Color.Green, 4)) { //时针 h = h >= 12 ? h - 12 : h; double angle = h * ha;//当前时针所占弧度 x1 = X + r + Convert.ToSingle(Math.Sin(angle) * (r - offset));//通过调整offset的大小,可以控制时针的长短 y1 = Y + r - Convert.ToSingle(Math.Cos(angle) * (r - offset)); //圆心 PointF pointEclipse = new PointF(X + r, Y + r); PointF pointEnd = new PointF(x1, y1); Graphics.DrawLine(pen, pointEclipse, pointEnd);//画45度角 //分针 using (Pen penYellow = new Pen(Color.Red, 2)) { offset = 30;//与分针长度成反比 //分 double angelMinutes = radian * m;//每分钟弧度 x1 = X + r + Convert.ToSingle(Math.Sin(angelMinutes) * (r - offset));//通过调整offset的大小,可以控制时针的长短 y1 = Y + r - Convert.ToSingle(Math.Cos(angelMinutes) * (r - offset)); Graphics.DrawLine(penYellow, pointEclipse, new PointF(x1, y1));//画45度角 } //秒针 using (Pen penYellow = new Pen(Color.Blue, 2)) { offset = 20; //分 double angelSeconds = radian * s;//每秒钟弧度 x1 = X + r + Convert.ToSingle(Math.Sin(angelSeconds) * (r - offset));//通过调整offset的大小,可以控制时针的长短 y1 = Y + r - Convert.ToSingle(Math.Cos(angelSeconds) * (r - offset)); Graphics.DrawLine(penYellow, pointEclipse, new PointF(x1, y1));//画45度角 } } } /// <summary> /// <![CDATA[绘制时刻线]]> /// </summary> private void DrawQuartzLine() { //圆心 PointF pointEclipse = new PointF(X + r, Y + r); float labelX, labelY;//文本坐标 float angle = Convert.ToSingle(Math.PI / 6);//角度 30度 using (Font font = new Font(FontFamily.GenericSerif, 12)) { float _x, _y;//圆上的坐标点 using (Brush brush = new System.Drawing.SolidBrush(Color.Red)) { using (Pen pen = new Pen(Color.Black, 0.6f)) { //一天12H,将圆分为12份 for (int i = 0; i <= 11; i++) { PointF p10;//圆周上的点 float pAngle = angle * i; float x1, y1; //三、四象限 if (pAngle > Math.PI) { if ((pAngle - Math.PI) > Math.PI / 2)//钝角大于90度 { //第三象限 x1 = Convert.ToSingle(r * Math.Cos(Math.PI * 2 - pAngle)); y1 = Convert.ToSingle(r * Math.Sin(Math.PI * 2 - pAngle)); _x = X + r - x1; _y = Y + r + y1; labelX = _x - 8; labelY = _y; } else { //第四象限 x1 = Convert.ToSingle(r * Math.Cos(pAngle - Math.PI)); y1 = Convert.ToSingle(r * Math.Sin(pAngle - Math.PI)); _x = X + r + x1; _y = Y + r + y1; labelX = _x; labelY = _y; } } //一、二象限 else if (pAngle > Math.PI / 2)//钝角大于90度 { //第一象限 x1 = Convert.ToSingle(r * Math.Cos(Math.PI - pAngle)); y1 = Convert.ToSingle(r * Math.Sin(Math.PI - pAngle)); _x = X + r + x1; _y = Y + r - y1; labelX = _x; labelY = _y - 20; } else { //第二象限 x1 = Convert.ToSingle(r * Math.Cos(pAngle)); y1 = Convert.ToSingle(r * Math.Sin(pAngle)); _x = X + r - x1; _y = Y + r - y1; labelX = _x - 15; labelY = _y - 20; } //上半圆 分成12份,每份 30度 if (i + 9 > 12) { Graphics.DrawString((i + 9 - 12).ToString(), font, brush, labelX, labelY); } else { if (i + 9 == 9) { labelX = X - 13; labelY = Y + r - 6; } Graphics.DrawString((i + 9).ToString(), font, brush, labelX, labelY); } p10 = new PointF(_x, _y); Graphics.DrawLine(pen, pointEclipse, p10); } } } } } /// <summary> /// <![CDATA[写入版本信息]]> /// </summary> private void WriteVersion() { PointF point = new PointF(X + r / 4, Y + r - 30); using (Font font = new Font(FontFamily.GenericSansSerif, 18)) { using (Brush brush = new SolidBrush(Color.Black)) { this.Graphics.DrawString("Quartz", font, brush, point); } } } /// <summary> /// <![CDATA[释放]]> /// </summary> /// <param name="isDispose"></param> private void Dispose(bool isDispose) { if (isDispose) { timer.Abort(); this.Graphics.Dispose(); } } /// <summary> /// /// </summary> public void Dispose() { this.Dispose(true); } } }
winfom调用示例
/// <summary> /// /// </summary> private CSharpQuartz sharpQuartz = null; /// <summary> /// /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void CSharpQuartzSample_Load(object sender, EventArgs e) { float w = 300f, h = 300f; float x = (this.Width - w) / 2; float y = (this.Height - h) / 2; sharpQuartz = new CSharpQuartz(this, x, y, w); sharpQuartz.OnChanged += SharpQuartz_OnChanged; sharpQuartz.Start(); } /// <summary> /// /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void SharpQuartz_OnChanged(object sender, EventArgs e) { if (lblTime.IsHandleCreated) { lblTime.Invoke(new Action(() => { lblTime.Text = DateTime.Now.ToString("当前时间:HH:mm:ss"); })); } }
这就是我们开篇第一张效果图,带有Quartz字样的,至此,关于GDI绘制时钟与系统时间同步的小程序就这样完成。时间仓促,某些计算方法买来得及仔细推敲,不足之处,大家多提意见。