程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> C# >> C#入門知識 >> C# 一個簡單的秒表引發的窗體卡死相關問題

C# 一個簡單的秒表引發的窗體卡死相關問題

編輯:C#入門知識

一個秒表程序也是我的一個心病,因為一直想寫這樣的一個東西,但是總往GUI那邊想,所以就比較怵,可能是上學的時候學MFC搞出的後遺症吧,不過當我今天想好用Win Form(話說還是第一次寫win form)寫這麼一個東西的時候,居然so easy。

所以說,做不了不可怕,怕的是你不去做,因為你不去做,你就永遠不知道你能不能做它。事實證明,大部分你猶豫能不能做的事情,實際上你都能搞定。

雖然成功實現了一個秒表的簡單功能,即開始計時和停止。但是卻引發了一個關於win form和C#線程的問題。

下面一個一個來,先說一下秒表的類實現

namespace Utils
{
    public class Time
    {
        private int _minute;
        private int _second;
        private bool _flag;//線程標識
        private Thread _TimingThread = null;

        public Time()
        {
            this._minute = 0;
            this._second = 0;
            this._flag = true;
        }
        /// <summary>
        /// 開始計時
        /// </summary>
        public void Start()
        {
            if (_TimingThread == null)
            {
                _TimingThread = new Thread(new ThreadStart(AddSecond));
                _TimingThread.Start();
            }
        }
        /// <summary>
        /// 線程執行方法
        /// </summary>
        private void AddSecond()
        {
            while(_flag)
            {
                Thread.Sleep(1000);
                if (this._second == 59)
                {
                    this._minute++;
                    this._second = 0;
                }
                else
                {
                    this._second++;
                }
            }
        }
        /// <summary>
        /// 格式化顯示計時結果
        /// </summary>
        /// <returns></returns>
        public string FormatTimeResult()
        {
            string minute = string.Empty;
            string second = string.Empty;
            if (this._minute < 10)
            {
                minute = "0" + this._minute.ToString();
            }
            else 
            {
                minute = this._minute.ToString();
            }
            if (this._second < 10)
            {
                second = "0" + this._second.ToString();
            }
            else
            {
                second = this._second.ToString();
            }
            return minute + ":" + second;
        }
        /// <summary>
        /// 停止
        /// </summary>
        public void Stop()
        {
            this._flag = false;
        }
        /// <summary>
        /// 歸0操作
        /// </summary>
        public void Zero()
        {
            this._minute = 0;
            this._second = 0;
        }
    }
}

秒表的實現還是比較簡單的,感覺這樣寫,也方便以後做擴展。

下面說說win form方面

窗體就是這樣,一個label,兩個button

public partial class Form1 : Form { private Time mTime = null; private Thread mDisplayThread = null; public Form1() { InitializeComponent(); mTime = new Time();//實例化秒表類 } private void button_start_Click(object sender, EventArgs e) { mTime.Start(); mDisplayThread = new Thread(new ThreadStart(DisplayCurrentTime)); mDisplayThread.Start(); button_start.Enabled = false; } public void DisplayCurrentTime() { while (true) { Thread.Sleep(1000); label_Time.Text = mTime.FormatTimeResult();//對Label標簽進行實時更新 Console.WriteLine("{0}", mTime.FormatTimeResult()); } } private void button_stop_Click(object sender, EventArgs e) { mTime.Stop(); button_start.Enabled = true; } }

這樣寫感覺思路上沒什麼問題,當點擊【開始計時】按鈕的同時創建一個線程,而這個線程是用來每隔一秒去更新一下label上的顯示計時時間。

然而,之後卻報一個這樣的錯誤:Cross-thread operation not valid: Control 'label_Time' accessed from a thread other than the thread it was created on.

網上查了一下,這個錯誤貌似很常見,MSDN上也給了一個出現此錯誤的原因,是這樣說的,當您試圖從單獨的線程更新一個win form時,會出現這個錯誤。

查了一下,就是說win form上的控件屬性想要進行修改的時候,只能在創建Control的線程裡調用,不能在以外的線程被調用。而上面的

label_Time.Text = mTime.FormatTimeResult();

這段代碼呢恰恰是發生在新創建的線程之中,所以就會報錯了。

解決辦法是用delegate(委托)加上control.Invoke去聯合實現。下面看看實現部分

    public partial class Form1 : Form
    {
        private Time mTime = null;
        private Thread mDisplayThread = null;
        public delegate void UpdateLabel();//聲明一個委托
        public UpdateLabel updateLabel;//定義一個委托

        public Form1()
        {
            InitializeComponent();
            mTime = new Time();
            updateLabel = new UpdateLabel(UpdateTime);//實例化一個委托對象
        }

        private void button_start_Click(object sender, EventArgs e)
        {
            mTime.Start();
            mDisplayThread = new Thread(new ThreadStart(DisplayTimeFunc));
            mDisplayThread.Start();
            button_start.Enabled = false;

        }
        /// <summary>
        /// 線程執行方法
        /// </summary>
        public void DisplayTimeFunc()
        {
            while (true)
            {
                Thread.Sleep(1000);
                this.Invoke(this.updateLabel);
            }
        }
        /// <summary>
        /// 單獨對Label進行刷新
        /// </summary>
        public void UpdateTime()
        {
            label_Time.Text = mTime.FormatTimeResult();
        }

        private void button_stop_Click(object sender, EventArgs e)
        {
            mTime.Stop();
            button_start.Enabled = true;
        }

    }

這段代碼裡mDisplayThread線程執行了DisplayTimeFunc方法,而DisplayTimeFunc方法裡實際就是在更新label,不同的是使用了Control.Invoke方法,上面不是說對控件屬性的更改要在創建控件的線程裡才執行嗎?現在看起來好像還是老樣子。那是因為我們不了解Control.Invoke是什麼東東。MSDN上的解釋是:在擁有此控件的基礎窗口句柄的線程上執行指定的委托。OK,明白了,this.updateLabel這個委托最後還是在窗口創建的線程中執行的。

回頭想想,其實思路也比較簡單,就是先將更改控件屬性的操作放在一個方法裡,然後寫個委托,再寫個線程,在線程的執行方法中調用這個委托就OK啦。

不過到這還不算全完,還有一個小問題,就是當我計時之後,想要關閉這個窗體的時候,發現又開始報錯了:

Invoke or BeginInvoke cannot be called on a control until the window handle has been created.

研究了一下發現了出現此問題的原因,就是我們“上完廁所沒有擦PP”,上面的代碼中沒有一個操作是對 mDisplayThread 這個線程做了終止的動作。

所以我們還需要添加以下動作

        private void Form1_FormClosing(object sender, FormClosingEventArgs e)
        {
            mDisplayThread.Abort();
        }

這樣就完整了,在關閉Form1窗體之前,先把線程終止。

做這個小東西的時候居然連帶著讓我了解了一些委托和Control.Invoke以及線程的知識點。我會找個時間好好把這部分看看的,爭取能總結點什麼出來。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved