【问题标题】:Possible to construct form on background thread, then display on UI thread可以在后台线程上构造表单,然后在 UI 线程上显示
【发布时间】:2011-01-05 14:18:15
【问题描述】:

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

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

任何人都可以确认或否认这是真的吗?


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

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

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

我收到了错误

当前线程必须设置为单线程 OLE 之前的线程单元 (STA) 模式 可以拨打电话。确保您的 主函数具有 STAThreadAttribute 标记在上面。

现在这是设计器文件的一半,这让我希望总的来说这个策略会奏效。但这条特殊的线路似乎试图立即启动某种 OLE 调用。

有什么想法吗?

编辑:

我想我在这里没有说清楚。延迟似乎发生在设计器生成代码期间构建大量控件的过程中。

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

我可以从这个后台线程设置(例如)标签文本和位置的事实让我希望这是真的。但是,它可能不适用于所有属性。

【问题讨论】:

  • 是仅为 cmbox 或所有控件引发的异常。因为如果是这样,您可以将属性设置为最后一件事
  • 仅用于在组合框上设置 AutoCompleteMode。在设计器的上面是大量的代码设置文本/名称/位置/大小/等。控件的属性。
  • 在“预加载”表单时,您的应用程序会做什么?可能显示“请稍候”消息?
  • 他们在预加载时为所欲为——这就是将其置于后台线程中的全部意义所在。
  • @Clyde:如果您只想显示可以处理用户输入的“请稍候”消息,您可以使用另一个线程来显示,然后让主线程启动带有许多控件的表单。

标签: c# winforms multithreading


【解决方案1】:

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

即使你让它发挥作用,它能给你带来什么?总体而言,它会慢一些,而不是更快。

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

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

【讨论】:

  • 这对我来说最有意义。查看 MethodInvoker 委托以获取有关如何执行此操作的示例。 bg 线程正在做腿部工作,而 GUI 线程仍在独立运行并接受用户输入。
【解决方案2】:

我觉得你的理解有点不对劲。必须从创建它们的线程而不是主 UI 线程中触摸控件。一个应用程序中可以有许多 UI 线程,每个线程都有自己的一组控件。因此,在不同线程上创建控件将不允许您在不使用 Invoke 或 BeginInvoke 编组所有调用的情况下从主线程使用它。

编辑 多个 UI 线程的一些参考:

MSDN on Message Loops MSDN social discussion Multiple threads in WPF

【讨论】:

  • 您能否提供此声明的参考资料?我的理解是处理消息循环的同一个线程需要更新控件。
  • 对....我想说的是,我不打算更新任何屏幕上的元素。这都是初始化代码,表单还没有显示。我只是想尽早摆脱 .NET 对象的构造。
  • Eric - 您可以有 N 个消息循环,每个线程一个,控件具有线程亲和性 Clyde - 创建 .Net 对象的行为会创建绑定到线程的底层 Windows 对象在其上创建它。
  • @dsolimano:您的一篇参考文献指出 winforms 只能有一个 UI 线程。问题是关于winforms。在 Windows.Forms 下,只有一个 GUI 线程(调用 Application.Run 的那个)。 “所有控件都应该在那个线程上创建。在 WPF 下,可以有多个 GUI 线程,但每个控件只属于那个线程。”您是否有示例说明在 WinForms 下创建第二个 UI 线程?
  • 你不能在 Windows 窗体中做的是有一个包含 2 个控件的窗体,每个控件都有一个单独的线程处理事件。我认为令人困惑的是 dsolimano (正确地)将表单包含为控件。照这个意思,每个线程都可以有自己的Form(Control),但是所有放置在Form上的Button等都必须属于同一个线程。
【解决方案3】:

答案是否定的。

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

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

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

【讨论】:

  • 您能帮我确认一下.NET 控件的构造函数确实创建了窗口句柄吗?有这方面的文件吗?
  • 没关系......我亲眼目睹了 Handle 是在构建后填充的。
  • @Jan,这是不正确的。完全可以创建窗体和控件并将它们显示在主 GUI 线程之外的线程中。当然,如果你这样做,你只能从创建它的线程访问多线程 GUI,但这是可能的。
  • @Ash 我的意思是'不,你永远不能展示它'不是说你不能更新这个对象,这是可能的,但学术上的,而不是克莱德所问的精神。
  • Ash 是对的。我刚刚将我的应用程序切换为每个表单使用一个线程,它能够将它们全部显示出来。 “UI 线程”实际上是指“您在其上创建感兴趣的 UI 实体的线程”。
【解决方案4】:

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

Windows 窗体强制执行单线程单元模型。总之,这意味着每个线程只能有一个 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 实例,每个实例都有自己独立的消息循环和线程。

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

【讨论】:

  • 您对消息循环的理解是令人生畏的,但真正构建第二个消息循环(无论您如何做)通常不是一个好主意,因为很多事情都会让人感到困惑。像焦点、标签、键盘和鼠标捕获等。
  • Jan,是的,如果你想为视频、动画等创建一个单独的渲染窗口,拥有一个单独的消息循环可能更有用。
【解决方案5】:

我相信可以将在非 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,您需要创建一个委托并使用 Invoke 返回主线程。

【讨论】:

    【解决方案6】:

    可以在后台线程中创建控件,但只能在 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;
    }
    

    【讨论】:

      猜你喜欢
      • 2015-03-20
      • 1970-01-01
      • 1970-01-01
      • 2012-12-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-06-22
      • 1970-01-01
      相关资源
      最近更新 更多