1

【WPF】在新线程上打开窗口

 2 years ago
source link: https://www.cnblogs.com/tcjiaan/p/7105361.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

【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任务马上完成,按钮就可以及时恢复可用。

好了,好了,到这一步,咱们的预期效果就达到了。看看结果吧。

示例代码下载地址。

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

说一句题外话,最近老周的博客更新得较慢,特特说明一下,不是老周偷懒,而是因为暑假到了,老周的书法培训班又要开工了。正忙于误人子弟呢,所以博客更新频率会慢一些。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK