如何让GUI线程创建对象?

时间:2015-06-18 12:57:27

标签: c# multithreading winforms async-await

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

  1. GUI Thread创建了一个闪屏和一个非常空的主窗口,都继承了Form
  2. 显示启动画面并将其提供给Application.Run()
  3. 启动屏幕将发送一个事件,触发async事件处理程序执行初始化,使用IProgress界面将进度报告回GUI。 (这完美无瑕。)
  4. 在初始化期间的某个时刻,我需要根据某些插件提供的信息动态创建GUI组件,并将它们添加到主窗口。
  5. 此时我陷入困境:我知道我需要让GUI线程为我创建这些组件,但没有Control我可以调用InvokeRequired。做MainWindow.InvokeRequired既不起作用。

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

2 个答案:

答案 0 :(得分:0)

使用我的问题的评论,特别是有关使我找到this very useful question的延续方法的说明,我实现了以下目标:

  • 初始化的第一部分是异步执行的(无变化)。
  • 初始化的第二部分(创建UI元素)随后在UI线程的 context 中作为Continuation Task执行。
  • 除了相当短的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();
}

答案 1 :(得分:0)

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

我建议您尝试以下方法:

  • 启动应用程序后,您可以创建启动画面,以便您的Program.cs就像这样

    static void Main()
    {
        Application.EnableVisualStyles();
        Application.SetCompatibleTextRenderingDefault(false);
        Application.Run(new SplashForm());
    }
    
  • 在splash表单构造函数中,创建一个新线程(我将使用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构造函数。

希望这有帮助。