如何要求 GUI 线程创建对象

本文关键字:线程 创建对象 GUI | 更新日期: 2023-09-27 17:56:51

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

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

在这一点上,我陷入了困境:我知道我需要让 GUI 线程为我创建这些组件,但没有Control我可以调用InvokeRequired。执行 MainWindow.InvokeRequired 也不起作用。

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

如何要求 GUI 线程创建对象

使用对我问题的评论,特别是关于继续方法的说明,这使我找到了这个非常有用的问题,我实现了以下结果:

  • 初始化的第一部分是异步执行的(无更改)。
  • 初始化的第二部分(创建 UI 元素)之后在 UI 线程的上下文中作为延续任务执行。
  • 除了相当短的 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();
}

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

我建议您尝试以下方法:

  • 当应用程序启动时,您创建初始屏幕窗体,因此您的程序.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; .这是关于此事的链接。

    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 构造函数。

希望这有帮助。