當WPF應用程序運行時,默認會創建一個UI主線程(因為至少需要一個),並在該UI線程上啟動消息循環。直到消息循環結束,應用程序就隨即退出。那么,問題就來了,能不能創建新線程,然后在新線程上打開一個新窗口實例?這樣可以讓不同窗口運行在不同的線程上,一定程度上可以相互“獨立”。
其實呢,完全的獨立運轉似乎不太可能,畢竟嘛,線程是搶占 CPU 時間片的,即各個線程間是交替運行的,現在處理器基本是N核的,可以結合並發一起用(在.net 中,使用 Task 可以自動並發)。不管怎么說吧,對UI的響應能力應該能有所改善的。
有大伙伴一定會說,這TMD Easy了,來直接上一段 Code。
Task theTask = new Task(() => { SecondWindow wind = new SecondWindow(); wind.Show(); }); theTask.Start();
然后你滿懷信心,春光滿面地按下了【F5】鍵,結果……
是了,不知道大伙伴以前在創建 WinForms 項目時,有沒有注意 Main 方法上面的一個 Attribute 的應用。
[System.STAThreadAttribute()] public static void Main(string[] args) { }
不僅僅是COM組件,Windows的 UI 調用,也需要 STA 線程單元。可是 Task 類沒有公開相關的成員讓我們設置,只有 Thread類有一個 SetApartmentState 方法,可以用 ApartmentState 枚舉來進行線程單元設置。
其實,你可以直接用 Thread 類,比如這樣。
Thread t = new Thread(() => { SecondWindow win = new SecondWindow(); win.Show(); }); t.SetApartmentState(ApartmentState.STA); t.Start();
如果,你還想結合 Task 類一起用,可以封裝成一個方法。
private Task RunNewWindowAsync<TWindow>() where TWindow:System.Windows.Window, new() { TaskCompletionSource<object> tc = new TaskCompletionSource<object>(); // 新線程 Thread t = new Thread(() => { TWindow win = new TWindow(); win.Show(); // 這句話是必須的,設置Task的運算結果 // 但由於此處不需要結果,故用null tc.SetResult(null); }); t.SetApartmentState(ApartmentState.STA); t.Start(); // 新線程啟動后,將Task實例返回 // 以便支持 await 操作符 return tc.Task; }
TaskCompletionSource 類可以從其他來源獲得代碼執行結果,然后生成一個帶Result 的Task實例,有了這個Task實例就可以使用異步等待語法了(await操作符)。由於我們這個地方只是Show一個窗口就完事了,不需要產生執行結果,但是,TaskCompletionSource類有一個泛型參數,用以指定執行結果。
這里咱們可以這樣,實例化TaskCompletionSource時設定泛型參數類型為object,然后把代碼的執行結果設置為 null。要設置執行結果,請調用 SetResult 方法,要得到生成的Task實例,請訪問 Task 屬性。
故,上面代碼可以這樣改:
private Task RunNewWindowAsync<TWindow>() where TWindow:System.Windows.Window, new() { TaskCompletionSource<object> tc = new TaskCompletionSource<object>(); // 新線程 Thread t = new Thread(() => { TWindow win = new TWindow(); win.Show(); // 這句話是必須的,設置Task的運算結果 // 但由於此處不需要結果,故用null tc.SetResult(null); }); t.SetApartmentState(ApartmentState.STA); t.Start(); // 新線程啟動后,將Task實例返回 // 以便支持 await 操作符 return tc.Task; }
還要注意,一定要調用Thread實例的 SetApartmentState 方法把線程單元設置為STA,一定要在線程 Start 之前設置,Start 之后就不能改了。最后把生成的Task實例從方法返回。
好了,到了這一步,窗口可以在新線程上打開了,但是,你又會發現一個問題——窗口打開后,閃一下就關閉了。那是因為我們沒有在新線程上開啟消息循環。大伙伴皆知,WPF 中有一個類專門調度UI線程,對,就是那個家伙:Dispatcher。Dispatcher 類公開了一個靜態方法,叫Run,只要在相應的線程上調用該方法,新的消息循環就會開啟。
來,咱們改一個代碼。
Thread t = new Thread(() => { TWindow win = new TWindow(); win.Show(); // Run 方法必須調用,否則窗口一打開就會關閉 // 因為沒有啟動消息循環 System.Windows.Threading.Dispatcher.Run(); // 這句話是必須的,設置Task的運算結果 // 但由於此處不需要結果,故用null tc.SetResult(null); });
在主窗口的代碼中,如此調用上面的 RunNewWindowAsync 方法。
Button b = e.Source as Button; b.IsEnabled = false; await RunNewWindowAsync<SecondWindow>(); //可異步等待 b.IsEnabled = true;
在等待之前,我禁用了按鈕,只是為了不讓同一個窗口打開多個實例而已,等新窗口結束后,按鈕就會重新啟用。
現在這個示例已基本接近我們的預期。但是,你運行后又會發現新問題——新窗口被關閉后,主窗口上的按鈕依然不可用,那是因為新線程上的消息循環仍在繼續,咋辦呢?很簡單,窗口不是有個 Closed 事件嗎,我們加一個 handler ,當窗口關閉后馬上把線程上的消息循環結束,這樣Task就能馬上返回。
TWindow win = new TWindow(); win.Closed += (d, k) => { // 當窗口關閉后馬上結束消息循環 System.Windows.Threading.Dispatcher.ExitAllFrames(); }; win.Show();
老周在不久前的一篇爛文中介紹過 DispatcherFrame 這個東東,還記得吧,前面咱們說過,向調度隊列中插入一個 frame 就會開啟一個消息循環,所以,調用 Dispatcher 的 ExitAllFrames 方法,可以馬上結束當前線程上的所有 frame,就相當於跳出所有消息循環。這樣處理后,當新打開的窗口被關閉后,Task任務馬上完成,按鈕就可以及時恢復可用。
好了,好了,到這一步,咱們的預期效果就達到了。看看結果吧。
===================================================================
說一句題外話,最近老周的博客更新得較慢,特特說明一下,不是老周偷懶,而是因為暑假到了,老周的書法培訓班又要開工了。正忙於誤人子弟呢,所以博客更新頻率會慢一些。