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繪制時鍾與系統時間同步的小程序就這樣完成。時間倉促,某些計算方法買來得及仔細推敲,不足之處,大家多提意見。