在混合控制台和 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 住在 BackgroundWorker
或 Form
中。在我的真实项目中我有一个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();
}
}
}
这里发生了什么?而且,有没有一种方法可以让 Gui
和 Worker
分开,但没有这些 运行 时间异常?
线程部分在这里不是问题。您只需要以稍微不同的方式处理表单创建:
第一次设置:
- 窗体的句柄创建可以强制调用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 句柄未在正确的时间创建(因为方法调用是在不同的线程中生成的) ,考虑了假设的赛车条件)。
我有一个正在处理的项目,它有一个 Windows gui(可选)和一个可以写入 gui 或控制台(如果没有 gui 的话)的工作人员。 gui 是可选的,可以使这个项目向后兼容可能没有桌面环境的系统(我也可能最终用 C 或 C++ 重新制作这个项目,但由于时间限制,我现在需要一些东西来工作)。该程序将 运行 用于(目前)的大多数计算机都具有 Windows XP。 (我的目标是 .NET Framework 4.0.3)。
由于我希望 gui 是可选的,所以我不希望工人 class 住在 BackgroundWorker
或 Form
中。在我的真实项目中我有一个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();
}
}
}
这里发生了什么?而且,有没有一种方法可以让 Gui
和 Worker
分开,但没有这些 运行 时间异常?
线程部分在这里不是问题。您只需要以稍微不同的方式处理表单创建:
第一次设置:
- 窗体的句柄创建可以强制调用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
属性 returnsfalse
。
第二个设置:
- 由于(根据评论)此控制台应用程序应在创建 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 句柄未在正确的时间创建(因为方法调用是在不同的线程中生成的) ,考虑了假设的赛车条件)。