在混合控制台和 WinForms 环境中,我可以保持工作程序和 GUI 分离而不会出现跨线程异常吗?

Can I keep worker and GUI separate without cross thread exceptions in mixed Console and WinForms environment?

我有一个正在处理的项目,它有一个 Windows gui(可选)和一个可以写入 gui 或控制台(如果没有 gui 的话)的工作人员。 gui 是可选的,可以使这个项目向后兼容可能没有桌面环境的系统(我也可能最终用 C 或 C++ 重新制作这个项目,但由于时间限制,我现在需要一些东西来工作)。该程序将 运行 用于(目前)的大多数计算机都具有 Windows XP。 (我的目标是 .NET Framework 4.0.3)。

由于我希望 gui 是可选的,所以我不希望工人 class 住在 BackgroundWorkerForm 中。在我的真实项目中我有一个UserInterface"interface"(c#接口)可以通过多种用户界面实现

在 Windows GUI 中,tfhere 是一个主要的 Form,带有一个用于打开对话框 Form 的按钮。该对话框有一个多行文本框,工作人员可以在其中附加行。

因为我没有使用 BackgroundWorker 或其他传统的做事方式,所以我 运行 遇到了与跨线程操作和在 [= 之前​​调用 BeginInvoke 相关的各种问题72=]句柄已经创建。我能够通过在 MainForm 的构造函数中调用 _ = MainForm.Handle 强制在 window 句柄创建之前创建 "solve" window 句柄问题=72=] 显示(以便工作人员可以将行附加到文本框,这可能发生在 gui 显示之前)。

这是我最小的、可重现的示例,它捕获了我在实际项目中遇到的问题。当 a) 从 MainForm 构造函数中删除 window 句柄的创建时会遇到问题,这会导致 BeginInvoke 抱怨在 window 句柄创建之前被调用,或者b) 现在,一旦对话框 window 关闭并重新打开,对 ShowDialog 的调用由于跨线程操作而失败。

Program.cs

using System;
using System.Windows.Forms;

namespace MinimalExample
{
    static class Program
    {
        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            Gui gui = new Gui();
            Worker worker = new Worker(gui);

            worker.start();
            gui.show();
        }
    }
}

Gui.cs

using System.Windows.Forms;

namespace MinimalExample
{
    class Gui
    {
        private readonly DialogForm _dialog_form;
        private readonly MainForm _main_form;

        public Gui()
        {
            _dialog_form = new DialogForm();
            _main_form = new MainForm(_dialog_form);
        }

        public void addLine(string line)
        {
            _dialog_form.addLine(line);
        }

        public void show()
        {
            Application.Run(_main_form);
        }
    }
}

MainForm.cs

using System;
using System.Windows.Forms;

namespace MinimalExample
{
    public partial class MainForm : Form
    {
        private readonly DialogForm _dialog_form;

        public MainForm(DialogForm form)
        {
            _dialog_form = form;

            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            _dialog_form.ShowDialog(this);
        }
    }
}

DialogForm.cs

using System;
using System.Windows.Forms;

namespace MinimalExample
{
    public partial class DialogForm : Form
    {
        public DialogForm()
        {
            InitializeComponent();

            _ = Handle;
            _ = textBox1.Handle;

            /* Visible = true;
               Visible = false; */
        }

        public void addLine(string line)
        {
            Action action = () =>
            {
                textBox1.AppendText(line);
                textBox1.AppendText(Environment.NewLine);
            };

            if (InvokeRequired)
                textBox1.BeginInvoke(action);
            else
                action();

        }
    }
}

Worker.cs

using System;
using System.Threading;

namespace MinimalExample
{
    internal class Worker
    {
        private readonly Gui _gui;
        private readonly Thread _thread;

        internal Worker(Gui gui)
        {
            _gui = gui;
            _thread = new Thread(new ThreadStart(working));
            _thread.IsBackground = true;
        }

        private void working()
        {
            while (true)
            {
                if (_gui != null)
                    _gui.addLine("Test");
                else
                    Console.WriteLine("Test");
                Thread.Sleep(1000);
            }
        }

        public void start()
        {
            _thread.Start();
        }
    }
}

这里发生了什么?而且,有没有一种方法可以让 GuiWorker 分开,但没有这些 运行 时间异常?

线程部分在这里不是问题。您只需要以稍微不同的方式处理表单创建:

第一次设置:

  • 窗体的句柄创建可以强制调用CreateHandle() right after InitializeComponent() (the .Net Source code related to the method call is more interesting, also note the call to UpdateHandleWithOwner())。
  • 显示工作线程输出的下一个条件是窗体也是可见的,因为在调用它时文本框句柄也需要存在。
  • Gui class 需要 BeginInvoke() Form 的 AddLine() 方法,因为方法调用是在不同的线程中生成的(Gui 对象被Worker 对象线程)。
  • 我在 Gui class 中添加了一个 public 属性: public bool CanWrite: 工人 class 可以检查此 属性 以确定应将输出写入何处。
    public属性returns:dialogForm != null && dialogForm.Visible;,这是因为↓:
  • DialogForm 显示调用 ShowDialog():这意味着当 DialogForm 关闭时,不会释放 Form。此外,该对象在 Gui class 中仍有引用。当它关闭时,它的 Visible 属性 returns false

第二个设置:

  • 由于(根据评论)此控制台应用程序应在创建 Gui 对象时输出到 DialogForm 窗体,并且不必立即显示对话框,因此 DialogForm 中的 TextBox 控件应缓存由线程发布的文本行工人 class.
    这需要一个简单的编辑:将 dialogForm != null && dialogForm.IsVisible 更改为 dialogForm != null,然后在 Gui.AddLine() 之前验证句柄状态] 如果此时句柄不可用,则调用并缓存文本行。
  • 所有者窗体 MainForm 指示 DialogForm 在 DialogForm.ShowDialog() returns 时重新创建其句柄。由于ShowDialog()用于显示DialogForm,因此不处理该表单。重新创建句柄不会导致子控件丢失其内容。

实施两个选项:

IsVisible 属性 检查可以成为 Gui 的 属性,类似于 bool UpdateOnDialogVisible,以在 CanWrite, 上进行测试,因此文本将被写入根据此 属性 的状态添加到 TextBox。


测试时间:

- Windows 7(我没有可用的 WinXP 机器)
- .Net Framework 4.0
- C# 5

在Program.cs中:

class Program
{
    private static Worker worker = null;
    private static Gui gui = null;

    // [...]

    gui = new Gui();
    worker = new Worker(gui);

    worker.start();
    gui.Show();
}

在Gui.cs

public class Gui
{
    private StringBuilder sb = null;
    // [...]

    public Gui() {
        sb = new StringBuilder();
        dialogForm = new DialogForm();
        mainForm = new MainForm(dialogForm);
    }

    public bool CanWrite {
        get { return dialogForm != null }
        // Or, with the condition that the Dialog is already visible:  
        // get { return dialogForm != null && dialogForm.Visible; }
    }

    public void AddLine(string line) {
        sb.AppendLine(line);
        // Safety measure: cache if the handle is not available at this time
        if (this.CanWrite && dialogForm.IsHandleCreated) {
            dialogForm.BeginInvoke(new MethodInvoker(() => {
                dialogForm.AddLine(sb.ToString());
                sb.Clear();
            }));
        }
    }
    // [...]
}

在Worker.cs中:

internal class Worker
{
    // [...]
    private void working() {
        while (true) {
            if (gui != null && gui.CanWrite) {
                gui.AddLine("Test");
            }
            else {
                Console.WriteLine("Test");
            }
            Thread.Sleep(1000);
        }
    }
    // [...]
}

在DialogForm.cs中:

public partial class DialogForm : Form
{
    private TextBox textBox1;

    public DialogForm() {
        InitializeComponent();
        this.CreateHandle();
    }

    public void AddLine(string line) {
        if (this.IsDisposed || !this.IsHandleCreated) return;
        this.textBox1.AppendText(line);
        this.textBox1.ScrollToCaret();
    }

    public void RecreateWindow() {
        this.CreateHandle();
    }

    private void InitializeComponent() {
        // [...]
    }
}

在MainForm.cs中:

public partial class MainForm : Form
{
    private Button button1;
    internal readonly DialogForm dialogForm = null;

    public MainForm() : this(null) { }
    public MainForm(DialogForm form) {
        dialogForm = form;
        InitializeComponent();
    }

    private void button1_Click(object sender, EventArgs e)
    {
        button1.Enabled = false;
        if (dialogForm != null) dialogForm.ShowDialog(this);
        button1.Enabled = true;
        // As describe in the notes, if a, e.g., UpdateOnDialogVisible () property is 
        // created, call this method when this property is true, to show text on this 
        // Window only when is Visible.
        dialogForm.RecreateWindow();
    }

    private void InitializeComponent() {
        // [...]
    }
}

这是它的工作原理:

第一个选项

  • 仅当指定表单在生成输出时可见时,控制台输出才会重定向到表单:

第二个选项
创建 Gui class 时,控制台输出始终定向到指定的表单。

  • 每次关闭时都会重新创建窗体句柄。由于使用 ShowDialog() 来显示它,因此不会处理 Form。
  • 用于显示控制台输出的 TextBox 也可以在关闭窗体时缓存输出。
  • StringBuilder 对象充当安全二级缓存,以防 Window 句柄未在正确的时间创建(因为方法调用是在不同的线程中生成的) ,考虑了假设的赛车条件)。