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

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

    示例代码下载地址。

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

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

  • 相关阅读:
    (翻译) CAP 理论 FAQ
    KVM 虚机镜像操作, 扩容和压缩
    LibOpenCM3(四) VSCode IDE 环境配置
    Ubuntu20.04 PostgreSQL 14 安装配置记录
    PlatformIO 创建 libopencm3 + FreeRTOS 项目
    GRC: 个人信息保护法, 个人隐私, 企业风险合规治理
    EmuELEC 4.3 安装和乐视手柄 LeWGP201 evremap问题解决
    LibOpenCM3(一) Linux下命令行开发环境配置
    LibOpenCM3(五) 基础功能: 系统时钟, GPIO, 定时器
    Spring的接口集合注入功能
  • 原文地址:https://www.cnblogs.com/tcjiaan/p/7105361.html
Copyright © 2020-2023  润新知