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