【WPF】在新線程上打開窗口


當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任務馬上完成,按鈕就可以及時恢復可用。

 

好了,好了,到這一步,咱們的預期效果就達到了。看看結果吧。

 

示例代碼下載地址。

 

===================================================================

說一句題外話,最近老周的博客更新得較慢,特特說明一下,不是老周偷懶,而是因為暑假到了,老周的書法培訓班又要開工了。正忙於誤人子弟呢,所以博客更新頻率會慢一些。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM