2010-01-15 48 views
12

更新:只是总结一下我的问题归结为:可以构建在后台线程的形式,然后在UI线程显示

我希望构建.NET窗体和控件没有创建任何窗口句柄 - - 希望该过程被推迟到Form.Show/Form.ShowDialog

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


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

我很清楚,我不能尝试与除主UI线程以外的任何线程上的UI进行交互,但我想要做的是让应用程序预加载此表单(运行构造函数),所以只要用户想打开它,就可以立即显示在UI线程上。然而,当构建在后台线程,在这条线的设计:

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

,我发现了错误

当前线程必须设置为单 线程单元(STA)模式之前可以进行OLE 调用。确保你的主函数有标记的STAThreadAttribute 。

现在,这是一半,设计师的文件,这给了我希望,一般这个策略会奏效。但是这条线似乎试图立即启动某种OLE呼叫。

任何想法?

编辑:

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

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

事实上,我可以设置(例如)从这个后台线程标签文本和位置给了我希望这是真实的。但是,对于所有房产来说可能并非如此。

+0

是仅针对cmbox或所有控件引发的异常。 因为如果是这样,您可以将属性设置为最后一件事 – 2010-01-15 16:27:28

+0

只是为了在组合框上设置AutoCompleteMode。在设计师的这个上面有很多代码设置文本/名称/位置/大小/等等。控制属性。 – Clyde 2010-01-15 16:29:14

+0

虽然它是“预加载”表单,你的应用程序会做什么?可能显示“请稍候”的消息? – Codesleuth 2010-01-15 16:29:28

回答

3

答案是否定的。

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

编辑:完全可以创建窗体和控件,并将其显示在主GUI线程以外的线程中。当然,如果你使用 ,你只能从创建它的线程 访问多线程的GUI,但这是可能的。 - 阿什利亨德森

你需要在一个BG线程执行任何繁重的工作,然后将数据加载到您的GUI控件

+0

您能否确认一下.NET控件的构造函数实际上是否创建窗口句柄?有没有关于这方面的任何文件? – Clyde 2010-01-15 17:41:46

+0

没关系......我亲眼看到手柄是在施工后填充的。 – Clyde 2010-01-15 18:58:44

+4

@Jan,这是不正确的。完全可以创建窗体并控制并在主GUI线程以外的线程中显示它们。当然,如果你这样做,你只能从创建它的线程访问多线程的GUI,但它是可能的。 – Ash 2010-01-16 03:49:12

1

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

即使你得到这个工作,它会给你买什么?它会稍微慢一点,总体来说不会更快。

也许只是显示一个启动画面,而这种形式加载?

或者,查看为什么你的表单需要这么长时间来构建。这是不常见的,需要几秒钟。

+0

这对我最有意义。查看MethodInvoker委托,了解如何执行此操作的示例。 bg线程正在做腿部工作,而GUI线程仍在独立运行并接受用户输入。 – spoulson 2010-01-15 18:59:48

4

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

编辑 多线程UI一些文献:

MSDN on Message Loops MSDN social discussion Multiple threads in WPF

+0

你能否提供这个说法的参考?我的理解是处理消息循环的相同线程需要更新控件。 – 2010-01-15 16:38:59

+0

正确....我想说的是我不打算更新任何屏幕上的元素。这是所有初始化代码,表单尚未显示。我只是试图尽早让.NET对象结构不受影响。 – Clyde 2010-01-15 16:46:47

+0

Eric - 您可以拥有N个消息循环,每个线程一个,控件具有线程亲和力 Clyde - 创建.Net对象的动作创建基础Windows对象,该对象绑定到创建它的线程。 – dsolimano 2010-01-15 16:56:55

14

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

Windows窗体强制执行单线程公寓模型。总之,这意味着每个线程只能有一个Window消息循环,反之亦然。另外,例如,如果threadA想要与threadB的消息循环交互,则它必须通过诸如BeginInvoke之类的机制来封送该调用。然而,如果你创建一个新的线程并为它提供它自己的消息循环,那么这个线程将会愉快地独立处理事件,直到它被告知结束消息循环为止。

所以证明,下面是Windows窗体的代码,用于创建和非GUI线程显示表单:

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方法在一个新的线程中运行,并创建窗体2的实例。

Form2实际上是通过调用ShowDialog()给它自己单独的消息循环。如果您要调用Show(),则不会提供消息循环,并且Form2会立即关闭。另外,如果您尝试访问OpenForm()内的Form1(例如使用'this'),您将在尝试执行跨线程UI访问时收到运行时错误。

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

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

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

+0

你对消息循环的理解是强大的,但真正旋转了第二个消息循环(你怎么做)通常不是一个好主意,因为许多事情会变得混乱。像焦点,标签,键盘和鼠标捕获等。 – 2010-01-18 09:21:09

+0

Jan,是的,如果你想为视频,动画等创建一个单独的渲染窗口,那么单独的消息循环可能更有用。 – Ash 2010-01-18 09:28:39

1

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

所以有2个线程'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,而你需要创建一个委托并使用调用找回了MainThread。

0

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

我以与异步使用该创建的扩展方法/等待模式

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; 
}