基於訊息的GUI構架
在過去的日子中,大部分程式設計語言平台的GUI構架幾乎沒有發生變化。雖然在細節上存在一些差異,比如在功能和編程風格上,但大部分都是採用了相同的構架來響應使用者輸入以及重新繪製螢幕。這種構架可以被總結為“單線程且基於訊息”。
Message msg;While(GetMessage(msg)){ TranslateMessage(msg); DispatchMessage(msg);}
這段代碼可以稱為訊息迴圈。在這個迴圈中,執行順序是串列的,一個GetMessage只能在前一個GetMessage執行完以後才能執行。
拿WPF或WindowsForm舉例,每個線程至少會建立一個擁有訊息列隊的視窗,並且這個線程的任務之一就是處理列隊中的各個訊息。只要在應用程式中調用了Application.Run,那麼執行這個方法的線程就預設被賦予了這個任務。隨後的所有GUI事件,例如使用者引發的事件(點擊按鈕,關閉視窗等),系統引發的事件(重繪視窗,調整大小等),以及應用程式中自訂群組件的特定事件等,都將把相應的訊息投遞給這個訊息列隊來實現。這意味著,在調用了run之後,隨後發生的大部分工作都是由事件處理器為了響應GUI事件而產生的。
GUI線程
Gui線程負責取走(get)和分發(dispatch)訊息,同時負責描繪介面,如果GUI線程阻塞在分發處理訊息這一步的話,那麼訊息就會在訊息佇列中積累起來,並等待GUI線程回到訊息列隊來。
如果阻塞的是一個長時間的操作,比如下載一個檔案的話,假設10秒鐘,那麼使用者在10秒鐘內都不能進行任何操作,因為線程沒法擷取新的訊息進行處理。
這就是為什麼在Windows中存在著MsgWaitForMultipleObjects的原因,這個API使得線程在等待的同時仍然可以運行訊息迴圈。在.NET中,你連這個選擇都沒有。
訊息分發時要考慮到複雜的重入性問題,很難確保一個事件處理器阻塞時,可以安全分發其他GUI事件以響應訊息。
因此,一種相對而言更容易掌握的解決方案就是只有在GUI線程中的代碼才能夠操縱GUI控制項,在更新GUI時所需要的其他資料和計算都必須在其他線程中完成,而不是在GUI線程上。
通常這意味著把工作轉交給線程池完成,然後在得到結果後把結果合并回GUI線程上。這也就是我們接下來要介紹的兩個類。
SynchronizationContext 和 BackgroundWorker
SynchronizationContext 對不同線程間的調度操作進行同步,把一些非同步作業的結果Post回GUI線程裡。
WPF中DispatcherSynchronizationContext的實現
public override void Post(SendOrPostCallback d, object state) { _dispatcher.Beginlnvoke(DispatcherPriority.Normal, d, state); } public override void Send(SendOrPostCallback d, object state) { _dispatcher.lnvoke(DispatcherPriority.Normal, d, state);}
有些情況下,如在控制台中我們不能通過SynchronizationContext類的Current屬性擷取SynchronizationContext執行個體,我們封裝了一下這個方法。
private static AsyncCallback SyncContextCallback(AsyncCallback callback) { // Capture the calling thread's SynchronizationContext-derived object SynchronizationContext sc = SynchronizationContext.Current; // If there is no SC, just return what was passed in if (sc == null) return callback; // Return a delegate that, when invoked, posts to the captured SC a method that // calls the original AsyncCallback passing it the IAsyncResult argument return asyncResult => sc.Post(result => callback((IAsyncResult)result), asyncResult);}
這個方法將一個普通的AsyncCallback方法轉換成特殊的AsyncCallback 方法,它通過SynchronizationContext 來調用。這樣無論執行緒模式中是否含有GUI線程,都可以正確的調用。
internal sealed class MyWindowsForm : Form { public MyWindowsForm() { Text = "Click in the window to start a Web request"; Width = 400; Height = 100; } protected override void OnMouseClick(MouseEventArgs e) { // The GUI thread initiates the asynchronous Web request Text = "Web request initiated"; var webRequest = WebRequest.Create("http://Wintellect.com/"); webRequest.BeginGetResponse(SyncContextCallback(ProcessWebResponse), webRequest); base.OnMouseClick(e); } private void ProcessWebResponse(IAsyncResult result) { // If we get here, this must be the GUI thread, it's OK to update the UI var webRequest = (WebRequest)result.AsyncState; using (var webResponse = webRequest.EndGetResponse(result)) { Text = "Content length: " + webResponse.ContentLength; } }}
這其實就是AsyncOperationManager的基本原理。
public static class AsyncOperationManager { public static SynchronizationContext { getj setj } public static AsyncOperation CreateOperation( object userSuppliedState ) ; }
BackGroundWorker是在前面所說的基礎上構建起來的更高層次的抽象,它對GUI程式中一些最常用的操作給出了規範的定義。有三個事件:
DoWork 、ProgressChanged 和 RunWorkerCompleted
在程式中調用RunWorkerAsync方法則會啟動DoWork事件的事件處理,當在事件處理過程中,調用 ReportProgress方法則會啟動ProgressChanged事件的事件處理,而當DoWork事件處理完成時,則會觸發 RunWorkerCompleted事件。
public partial class Form1 : Form { public Form1() { InitializeComponent(); backgroundWorker1.WorkerReportsProgress = true; backgroundWorker1.WorkerSupportsCancellation = true; } private void startAsyncButton_Click(object sender, EventArgs e) { if (backgroundWorker1.IsBusy != true) { // Start the asynchronous operation. backgroundWorker1.RunWorkerAsync(); } } private void cancelAsyncButton_Click(object sender, EventArgs e) { if (backgroundWorker1.WorkerSupportsCancellation == true) { // Cancel the asynchronous operation. backgroundWorker1.CancelAsync(); } } // This event handler is where the time-consuming work is done. private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e) { BackgroundWorker worker = sender as BackgroundWorker; for (int i = 1; i <= 10; i++) { if (worker.CancellationPending == true) { e.Cancel = true; break; } else { // Perform a time consuming operation and report progress. System.Threading.Thread.Sleep(500); worker.ReportProgress(i * 10); } } } // This event handler updates the progress. private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e) { resultLabel.Text = (e.ProgressPercentage.ToString() + "%"); } // This event handler deals with the results of the background operation. private void backgroundWorker1_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) { if (e.Cancelled == true) { resultLabel.Text = "Canceled!"; } else if (e.Error != null) { resultLabel.Text = "Error: " + e.Error.Message; } else { resultLabel.Text = "Done!"; } } }