可以在后台线程上构造表单,然后在UI线程上显示

时间:2010-01-15 16:24:37

标签: c# winforms multithreading

更新:只是总结一下我的问题归结为:

我希望构建.NET表单和控件不会创建任何窗口句柄 - 希望该过程延迟到Form.Show/Form.ShowDialog

任何人都可以确认或否认这是否属实?


我有一个带有选项卡控件的大型WinForms表单,表单上有许多控件,在加载几秒钟后会暂停。我已经将它缩小到InitializeComponent中设计器生成的代码,而不是构造函数或OnLoad中的任何逻辑。

我很清楚我不能尝试在主UI线程以外的任何线程上与UI交互,但我想做的是让应用程序预先加载这个表单(运行后台的构造函数),一旦用户想要打开它,它就可以立即在UI线程上显示。但是,在后台线程中构建时,在设计器的这一行:

this.cmbComboBox.AutoCompleteMode = System.Windows.Forms.AutoCompleteMode.Suggest;

我收到了错误

  

当前线程必须设置为单个   OLE之前的线程单元(STA)模式   可以打电话。确保你的   主要功能有STAThreadAttribute   标记在上面。

现在这是设计器文件的一半,这让我希望一般这个策略可行。但是这条特殊的线似乎试图立即启动某种OLE调用。

有什么想法吗?

编辑:

我想我在这里并没有说清楚。延迟似乎发生在设计人员生成的代码中构建bazillion控件期间。

我希望所有这些初始化代码都是在没有实际尝试触摸任何真正的Win32窗口对象的情况下发生的,因为表单尚未实际显示。

我可以设置(例如)来自此后台线程的标签文本和位置的事实让我希望这是真的。然而,对于所有房产而言可能并非如此。

6 个答案:

答案 0 :(得分:16)

虽然无法在一个线程上创建表单,并使用另一个线程显示它,但 当然可以在非主GUI线程中创建表单。目前接受的答案似乎表明这是不可能的。

Windows Forms强制执行单线程单元模型。总之,这意味着每个线程只能有一个Window消息循环,反之亦然。另外,如果例如threadA想要与threadB的消息循环进行交互,它必须通过诸如BeginInvoke之类的机制来封送调用。

但是,如果您创建一个新线程并为其提供自己的消息循环,该线程将很乐意独立处理事件,直到它被告知结束消息循环。

因此,为了演示,下面是用于在非GUI线程上创建和显示表单的Windows窗体代码:

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        label1.Text = Thread.CurrentThread.ManagedThreadId.ToString();

    }

    private void button1_Click(object sender, EventArgs e)
    {
        ThreadStart ts = new ThreadStart(OpenForm);

        Thread t = new Thread(ts);
        t.IsBackground=false;

        t.Start(); 
    }

    private void OpenForm()
    {
        Form2 f2 = new Form2();

        f2.ShowDialog();
    }
}


public partial class Form2 : Form
{
    public Form2()
    {
        InitializeComponent();
    }

    private void Form2_Load(object sender, EventArgs e)
    {
        label1.Text = Thread.CurrentThread.ManagedThreadId.ToString() ;

    }
}

OpenForm方法在新线程中运行并创建Form2的实例。

Form2实际上通过调用ShowDialog()给它自己独立的消息循环。如果您要调用Show(),则不会提供任何消息循环,而Form2会立即关闭。

另外,如果您尝试在OpenForm()中访问Form1(例如使用“this”),则在尝试进行跨线程UI访问时会收到运行时错误。

t.IsBackground=false将线程设置为前台线程。我们需要一个前台线程,因为在没有首先调用FormClosing或FormClosed事件的情况下关闭主窗体时会立即杀死后台线程。

除了这些要点之外,Form2现在可以像任何其他形式一样使用。您会注意到Form1仍然像往常一样快乐地运行它自己的消息lopp。这意味着您可以单击按钮并创建Form2的多个实例,每个实例都有自己独立的消息循环和线程。

你需要注意跨表单访问,现在实际上是跨线程的。您还需要确保处理主窗体的关闭,以确保正确关闭任何非主线程形式。

答案 1 :(得分:4)

我认为你的理解有点偏差。必须从创建它们的线程触摸控件,而不是主UI线程。您可以在应用程序中拥有大量UI线程,每个线程都有自己的控件集。因此,在不同的线程上创建控件将不允许您从主线程使用它,而不使用Invoke或BeginInvoke对所有调用进行编组。

EDIT 多个UI线程的一些引用:

MSDN on Message Loops MSDN social discussion Multiple threads in WPF

答案 2 :(得分:3)

答案是否定的。

如果在GUI线程以外的任何线程上创建窗口句柄,则无法显示它。

  

编辑:完全可以创建表单和控件   将它们显示在主GUI线程以外的线程中。当然,如果   如果这样做,您只能从线程访问多线程GUI   它创造了它,但它是可能的。 - 阿什利亨德森

您需要在bg线程上执行任何繁重的工作,然后将数据加载到GUI小部件

答案 3 :(得分:1)

通常,需要从运行消息循环的同一线程访问表单的属性。这意味着,为了在另一个线程上构造表单,您需要使用BeginInvoke封送任何实际设置属性的调用。对于构造函数中的属性集也是如此,如果它们最终生成需要处理的消息(现在发生在你身上)。

即使你开始工作,它还能为你带来什么?总体而言,它会慢一点,而不是更快。

也许只是在加载此表单时显示启动画面?

或者,回顾一下为什么您的表单需要很长时间才能构建。这需要几秒钟才是常见的。

答案 4 :(得分:1)

我相信可以将在非UI线程上创建的组件添加到主UI中,我已经完成了。

所以有两个主题,'NewCompThread'和'MainThread'。

你旋转NewCompThread并为你创建组件 - 所有这些都准备好显示在MainUI上(在MainThread上创建)。

但是......如果你在NewCompThread上尝试这样的话,你会得到一个例外:     ComponentCreatedOnNewCompTHread.parent = ComponentCreatedOnMainThread;

但你可以加上这个:

if (ComponentCreatedOnMainThread.InvokeRequired) {
  ComponentCreatedOnMainThread.Invoke(appropriate delegate...);
} else {
  ComponentCreatedOnNewCompTHread.parent = ComponentCreatedOnMainThread; 
}

它会起作用。我做到了。
对我来说奇怪的是,然后ComponentCreatedOnNewCompTHread'认为'它是在MainThread上创建的。

如果您从NewCompThread执行以下操作:     ComponentCreatedOnNewCompTHread.InvokeRequired 它将返回TRUE,您需要创建一个委托并使用Invoke返回MainThread。

答案 5 :(得分:0)

可以在后台线程中创建控件,但只能在STA线程上创建。

我创建了一个扩展方法,以便将其与async / await模式一起使用

private async void treeview1_AfterSelect(object sender, TreeViewEventArgs e)
{
    var control = await CreateControlAsync(e.Node);
    if (e.Node.Equals(treeview1.SelectedNode)
    {
        panel1.Controls.Clear();
        panel1.Controls.Add(control);
    }
    else
    {
        control.Dispose();
    }
}

private async Control CreateControlAsync(TreeNode node)
{
    return await Task.Factory.StartNew(() => CreateControl(node), ApartmentState.STA);
}

private Control CreateControl(TreeNode node)
{
    // return some control which takes some time to create
}

这是扩展方法。任务不允许设置公寓,所以我在内部使用一个线程。

public static Task<T> StartNew<T>(this TaskFactory t, Func<T> func, ApartmentState state)
{
    var tcs = new TaskCompletionSource<T>();
    var thread = new Thread(() =>
    {
        try
        {
            tcs.SetResult(func());
        }
        catch (Exception e)
        {
            tcs.SetException(e);
        }
    });
    thread.IsBackground = true;
    thread.SetApartmentState(state);
    thread.Start();
    return tcs.Task;
}