与程序同时运行表单

Run Form simultaneously with Program

提问人:Brendan Lynn 提问时间:11/11/2023 最后编辑:NimanthaBrendan Lynn 更新时间:11/12/2023 访问量:121

问:

我有一个班级,试图创造一个更好的班级替代品。它的开始如下:System.Console

public class SuperConsole
{
    private readonly Form _Form;
    public SuperConsole()
    {
        _Form = new()
        {
            Text = DefaultTitle,
            BackColor = DefaultBackColor,
        };
        Application.Run(_Form);
    }
    public string Title
    {
        get => _Form.Invoke(() => _Form.Text);
        set => _Form.Invoke(() => _Form.Text = value);
    }
    //more stuff
}

显然,如果我运行构造函数,它会冻结在 .当然,窗体会运行,但构造函数的调用方必须稍等片刻。因此,我更改了构造函数以在新线程上运行:Application.RunApplication.Run

public SuperConsole()
{
    _Form = new()
    {
        Text = DefaultTitle,
        BackColor = DefaultBackColor,
    };
    Thread t = new(() => Application.Run(_Form));
    t.Start();
}

但这会抛出一个.System.InvalidOperationException

System.InvalidOperationException:“在创建窗口句柄之前,无法对控件调用 Invoke 或 BeginInvoke。”

我不知道如何进行。我尝试了几种变体,但没有效果。

我希望表单和调用函数能够同时运行,并且仍然能够正常使用。我还希望让类的用户不必担心任何这些。我该怎么做?Form.Invoke

C# 多线程处理 WinForms 线程安全 InvalidOperationException

评论

1赞 Nick Abbot 11/11/2023
我认为您需要在线程中实例化整个类,而不仅仅是表单。
0赞 Brendan Lynn 11/11/2023
@NickAbbot我考虑过。字段已声明,因此除了构造函数之外,没有方法可以更改它。即使我删除了 ,我仍然必须调用相同的函数。这将阻止它返回,所以我不能等待它用 .因此,这很快就会变得非常丑陋。readonlyreadonlyApplication.RunThread.Join
0赞 Brendan Lynn 11/11/2023
但是,这可能是必要的。我只是希望有一个更干净的替代品。
0赞 Ben Voigt 11/11/2023
Win32 窗口具有线程相关性(其消息通过线程的消息队列传递,如果创建它们的线程退出,则立即销毁它们)。因此,“阻止它返回,所以我不能等待它用 Thread.Join 初始化”确实是真的。
0赞 Enigmativity 11/11/2023
不能创建、访问或修改不在 UI 线程上的任何 UI 元素。

答:

4赞 Theodor Zoulias 11/11/2023 #1

我认为您必须在专用线程上创建,而不是在当前线程上创建,因为 UI 组件是线程仿射的。您还必须将线程声明为 STA,并等待表单创建完成。像这样的东西应该可以工作:FormSuperConsoleSuperConsole

public class SuperConsole
{
    private Form _form;

    public SuperConsole()
    {
        ManualResetEventSlim mres = new();
        Thread t = new(() =>
        {
            _form = new()
            {
                Text = DefaultTitle,
                BackColor = DefaultBackColor,
            };
            mres.Set();
            Application.Run(_form);
        });
        t.Name = "SuperConsole";
        t.SetApartmentState(ApartmentState.STA);
        t.Start();
        mres.Wait();
    }

    public string Title
    {
        get => _form.Invoke(() => _Form.Text);
        set => _form.Invoke(() => _Form.Text = value);
    }
}

ManualResetEventSlim 用于指示实例已创建并分配给字段。否则,当前线程可以观察到 to be .Form_form_formnull

我还没有测试上面的代码。在消息循环开始之前,表单可能尚未准备好调用。在这种情况下,您可能需要在窗体的 HandleCreated 或 or 事件中移动。若要了解如何仅订阅一个通知的事件,请参阅此答案Invokemres.Set();LoadShown

-1赞 Brendan Lynn 11/11/2023 #2

我找到了以下解决方案,类似于用户@TheodorZoulias的假设。

public SuperConsole()
{
    _Form = new()
    {
        Text = DefaultTitle,
        BackColor = DefaultBackColor,
    };
    ManualResetEventSlim mres = new();
    _Form.HandleCreated += HandleCreated;
    Thread t = new(() => Application.Run(_Form));
    t.Start();
    mres.Wait();
    void HandleCreated(object? Sender, EventArgs E)
    {
        mres.Set();
        _Form.HandleCreated -= HandleCreated;
    }
}

特别感谢用户@TheodorZoulias指出为什么我的最后一次尝试不是最佳的。我借用了他使用班级和活动的想法。ManualResetEventSlimForm.HandleCreated

但是,窗体不需要在与调用函数的同一线程上构造。Application.Run

评论

0赞 Theodor Zoulias 11/11/2023
如果将静态 Control.CheckForIllegalCrossThreadCalls 属性设置为 ,它是否仍能正常工作?true
1赞 Brendan Lynn 11/11/2023
@TheodorZoulias确实如此。我检查了。
0赞 Enigmativity 11/12/2023
不要在其他线程上创建表单。西奥的回答是正确的。事实并非如此。如果它有效,那么你是幸运的,你会一直很幸运,直到你不成功。
0赞 Theodor Zoulias 11/12/2023
@Enigmativity我的猜测是,Brendan 希望将字段保留为 ,这是一个合理的愿望,但我的答案没有做到。_Formreadonly
0赞 Enigmativity 11/13/2023
@TheodorZoulias - 私人二传手会这样做。