【问题标题】:How to ask the GUI thread to create objects?如何让 GUI 线程创建对象?
【发布时间】:2015-09-04 03:06:58
【问题描述】:

我的 Windows 窗体应用程序中有以下程序流程(不幸的是,WPF 不是一个可行的选择):

  1. GUI 线程创建一个启动屏幕和一个相当空的主窗口,两者都继承 Form
  2. 初始屏幕显示并提供给Application.Run()
  3. 初始屏幕将发送一个触发async 事件处理程序的事件,该事件处理程序执行初始化,使用IProgress 接口向GUI 报告进度。 (这完美无缺。)​​
  4. 在初始化过程中的某个时刻,我需要根据某些插件提供的信息动态创建 GUI 组件并将它们添加到主窗口。

此时我被卡住了:我知道我需要让 GUI 线程为我创建这些组件,但是没有 Control 我可以调用 InvokeRequired。执行 MainWindow.InvokeRequired 均无效。

我能想到的唯一想法是触发一个连接到 GUI 线程中的工厂的事件,然后等待该工厂触发另一个提供所创建控件的事件。但是我很确定有一个更强大的解决方案。有谁知道如何做到这一点?

【问题讨论】:

  • 好吧,您需要在 GUI 线程上完成所有这些工作 - 那么为什么不准备所有信息以在单独的线程上实际构建 GUI,然后将所有信息传递回启动画面作为成功价值?无论如何,GUI 将在那个时候被阻止。
  • 例如,只需在面板中创建所有控件,完成后传递单个 GUI 元素并将其添加到表单中。
  • 但是,您实际上有一个 GUI。使用闪屏的 Begin/Invoke() 方法同样有效。通过复制 SynchronizationContext.Current 并在线程中使用其 Post 方法,您将获得一些优雅点。或者按预期使用异步,延续应该在 GUI 线程上运行。
  • @HansPassant 感谢您评论的最后一部分,它导致了(我认为)一个相当优雅的解决方案,它将启动屏幕与任何初始化任务完全分离。如果您对更多详细信息感兴趣,请查看我的回答。

标签: c# multithreading winforms async-await


【解决方案1】:

在我的问题上使用 cmets,尤其是关于让我找到 this very useful question 的延续方法的注释,我实现了以下目标:

  • 初始化的第一部分是异步执行的(没有变化)。
  • 初始化的第二部分(创建 UI 元素)随后在 UI 线程的 context 中作为延续任务执行。
  • 除了相当短的 GUI 初始化部分外,启动画面是响应式的(即鼠标光标在悬停在启动画面时不会变为“等待”)。
  • 两个初始化例程都不知道启动画面(即我可以轻松地更换它)。
  • 核心控制器只知道SplashScreen界面,甚至不知道它是Control
  • 目前没有异常处理。这是我的下一个任务,但不影响这个问题。

TL;DR:代码看起来有点像这样:

public void Start(ISplashScreen splashScreen, ...)
{
    InitializationResult initializationResult = null;
    var progress = new Progress<int>((steps) => splashScreen.IncrementProgress(steps));
    splashScreen.Started += async (sender, args) => await Task.Factory.StartNew(

             // Perform non-GUI initialization - The GUI thread will be responsive in the meantime.
             () => Initialize(..., progress, out initializationResult)

        ).ContinueWith(

            // Perform GUI initialization afterwards in the UI context
            (task) =>
                {
                    InitializeGUI(initializationResult, progress);
                    splashScreen.CloseSplash();
                },
            TaskScheduler.FromCurrentSynchronizationContext()

        );

    splashScreen.Finished += (sender, args) => RunApplication(initializationResult);

    splashScreen.SetProgressRange(0, initializationSteps);        
    splashScreen.ShowSplash();

    Application.Run();
}

【讨论】:

    【解决方案2】:

    管理多个表单并在另一个正在工作或正在构建时显示一个要容易得多。

    我建议您尝试以下方法:

    • 当应用程序启动时,您创建启动屏幕表单,因此您的 Program.cs 是这样的

      static void Main()
      {
          Application.EnableVisualStyles();
          Application.SetCompatibleTextRenderingDefault(false);
          Application.Run(new SplashForm());
      }
      
    • 在启动表单构造函数中,创建一个新线程(我将使用BackgroundWorker,但还有其他选项,例如任务)来构建您的主表单。

      public SplashForm()
      {
          InitializeComponent();
          backgroundWorker1.WorkerSupportsCancellation = true;
          backgroundWorker1.WorkerReportsProgress = true;
          backgroundWorker1.DoWork += new DoWorkEventHandler(backgroundWorker1_DoWork);
          backgroundWorker1.ProgressChanged += new ProgressChangedEventHandler(backgroundWorker1_ProgressChanged);
          backgroundWorker1.RunWorkerAsync();
      }
      
    • 现在我们需要编写 SplashForm 成员函数来告诉后台工作人员该做什么

      private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
      {
          BackgroundWorker worker = sender as BackgroundWorker;
      
          // Perform non-GUI initialization - The GUI thread will be responsive in the meantime
      
          // My time consuming operation is just this loop.
          //make sure you use worker.ReportProgress() here
          for (int i = 1; (i <= 10); i++)
          {
              if ((worker.CancellationPending == true))
              {
                  e.Cancel = true;
                  break;
              }
              else
              {
                  System.Threading.Thread.Sleep(500);
                  worker.ReportProgress((i * 10));
              }
          }
      
          SetVisible(false);
          MainForm mainForm = new MainForm();
          mainForm.ShowDialog();
      
          //instead of
          //this.Visible = false;
      }
      
      private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
      {
          this.progressBar1.Value = e.ProgressPercentage;
      }
      
    • 您现在可能已经注意到,我正在使用另一个成员函数来隐藏启动画面。这是因为你现在在另一个线程中,你不能只使用this.visible = false;。这是关于此事的link

      delegate void SetTextCallback(bool visible);
      private void SetVisible(bool visible)
      {
          // InvokeRequired required compares the thread ID of the
          // calling thread to the thread ID of the creating thread.
          // If these threads are different, it returns true.
          if (this.InvokeRequired)
          {
              SetTextCallback d = new SetTextCallback(SetVisible);
              this.Invoke(d, new object[] { visible });
          }
          else
          {
              this.Visible = visible;
          }
      }
      

    当我运行这个示例项目时,它会显示进度条,然后在隐藏 SplashForm 后加载 MainForm 窗口窗体。

    通过这种方式,您可以将可能需要的任何控件放入 MainForm 构造函数中。您缩短为 // Perform GUI initialization afterwards in the UI context 的部分应该进入 MainForm 构造函数。

    希望这会有所帮助。

    【讨论】:

    • 感谢您的建议,但是在您的解决方案中,闪屏类实现了线程管理和应用程序初始化。启动画面不应该是responsible,因为这会将启动画面与特定项目紧密结合。在我的解决方案中,我可以在多个项目中重复使用闪屏(界面),甚至可以通过实现 ISplashScreen 将其与 WPF 闪屏进行交换,而无需再次实现线程管理和初始化。
    • 您的 ISplashScreen 是可重复使用的,您也可以将我的 ISplashScreen 转换为可重复使用的东西。您可以创建一个表单构建后台工作类并将其作为参数传递给启动屏幕构造函数。它不必知道“dowork”函数实际上做了什么。 ProgressChanged 需要保持不变,因为进度条是 SplashForm 的成员,通用代码可能无法更新它。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-01-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-05-28
    相关资源
    最近更新 更多