2018年工作之余,想起来捡起GDI方面的技术,特意在RichCodeBox项目中做了两个示例程序,其中一个就是时钟效果,纯C#开发。这个CSharpQuartz是今天上午抽出一些时间,编写的,算是偷得浮生半日休闲吧。先来看看效果图吧:
这是直接在Winform的基础上进行绘制的。接下来,我对时钟进行了封装,封装成一个名为CSharpQuartz的类,效果如下:
这是把时钟封装后,实现的一种效果,CSharpQuartz内部开辟了一个线程,与系统时间,保持同步,每秒刷新一次。所采用的技术也就是GDI和多线程及事件委托。把时钟封装成对象后,还为其添加了OnChanged事件,用于对象提供外部
处理之用。接下来就简单的说下,做次小程序的一些准备工作吧。
这也是最近偶尔听到有朋友问怎样做时钟的事,想来,其实也简单的,只是需要一些耐心和细心,这里主要还利用一些三角函数进行计算。上图看似很简单,其实也有很多小细节需要注意。我就把大致绘制的过程简单说下:
首先,我们需要定义一个圆,来作为时钟的轮廓,这里是通过设置时钟的直径及winform的宽高,来计算出时钟在窗体居中的位置。绘制圆的代码就更简单了
1 2 3 4 5 6 7 8 | 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:
1 | 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个点的位置,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | /// <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轴的线,就是我们在效果图中看到的垂直于水平两条线段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | /// <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 绘制象限 } } |
接下来,该绘制指针(时、分、秒),就是我们效果图中看到的,红绿蓝,三条长短不一的线段,秒针最长,这是和本地系统时间同步,所以要根据当前时间,计算出指针所在的位置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 | /// <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); })); } } |
最后,开辟一个线程,来同步更新时针的状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | /// <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的对象,具体代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 | 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调用示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | /// <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绘制时钟与系统时间同步的小程序就这样完成。时间仓促,某些计算方法买来得及仔细推敲,不足之处,大家多提意见。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· Ollama——大语言模型本地部署的极速利器
· DeepSeek如何颠覆传统软件测试?测试工程师会被淘汰吗?