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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM