一個秒表程序也是我的一個心病,因為一直想寫這樣的一個東西,但是總往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以及線程的知識點。我會找個時間好好把這部分看看的,爭取能總結點什麼出來。