開發 WinForms 應用程式時,大部分情況會遇到多執行緒控制介面控制項的問題。然而無法用傳統方法解決,下面詳細介紹幾種方案並分析優缺點。
public partial class Form1 : Form {
public Form1() { InitializeComponent(); }
private void Form1_Load(object sender, EventArgs e) {
Thread thread = new Thread(ThreadFunction);
thread.IsBackground = true;
thread.Start();
}
private void ThreadFunction() {
while (true) {
this.textBox1.Text = DateTime.Now.ToString();
Thread.Sleep(1000);
}
}
}
執行會拋出例外:Cross-thread operation not valid: Control 'textBox1' accessed from a thread other than the thread it was created on。.NET 2.0 後加強安全機制,不允許 WinForms 直接跨執行緒存取控制項屬性。
private void Form1_Load(object sender, EventArgs e) {
Control.CheckForIllegalCrossThreadCalls = false; // 關閉檢查
Thread thread = new Thread(ThreadFunction);
thread.IsBackground = true;
thread.Start();
}
程式可正常執行,但 CheckForIllegalCrossThreadCalls 是 static 屬性,修改後全域生效。專案中其他人修改此值會導致方案失效,且跨執行緒存取通常應檢查,不可取。
private delegate void FlushClient();
private void CrossThreadFlush() {
FlushClient fc = new FlushClient(ThreadFunction);
this.BeginInvoke(fc); // 調用委派
}
private void ThreadFunction() {
while (true) {
this.textBox1.Text = DateTime.Now.ToString();
Thread.Sleep(1000);
}
}
雖無跨執行緒例外,但介面無回應。原因:這相當於把新執行緒「注入」主控制執行緒,取得主執行緒控制權。只要此執行緒不返回,主執行緒永遠無法回應。即使用有限迴圈能返回,多執行緒也失去原本意義。
public partial class Form1 : Form {
private delegate void FlushClient();
public Form1() { InitializeComponent(); }
private void Form1_Load(object sender, EventArgs e) {
Thread thread = new Thread(CrossThreadFlush);
thread.IsBackground = true;
thread.Start();
}
private void CrossThreadFlush() {
while (true) {
Thread.Sleep(1000); // Sleep 與迴圈放在非同步等待外部
ThreadFunction();
}
}
private void ThreadFunction() {
if (this.textBox1.InvokeRequired) { // 等待非同步
FlushClient fc = new FlushClient(ThreadFunction);
this.Invoke(fc); // 透過委派呼叫重新整理方法
} else {
this.textBox1.Text = DateTime.Now.ToString();
}
}
}
Invoke 封送。Invoke 短暫切換到主執行緒,釋放主執行緒處理其他訊息。Invoke 同步等待完成,BeginInvoke 非同步。此處用 Invoke 確保更新順序與簡單錯誤處理。public static class UIThreadHelper {
public static void InvokeIfRequired(this Control control, Action action) {
if (control.InvokeRequired)
control.Invoke(action);
else
action();
}
}
// 使用範例
private void BackgroundWork() {
while (running) {
Thread.Sleep(1000);
string time = DateTime.Now.ToString();
this.textBox1.InvokeIfRequired(() => this.textBox1.Text = time);
}
}
執行上述正確程式碼,問題已解決。透過非同步等待,不會持續佔有主執行緒控制權,可在不發生跨執行緒呼叫例外情況下完成多執行緒對 WinForms 控制項控制。