有没有非阻塞版本的MessageBox.Show(或类似的东西)?

35

长期延迟的更新

我接受 MUG4N 对这个问题的回答,也想回应一些对它提出的批评。

ChrisF 说:

...你不能直接从后台线程进行 UI 调用。

这是一个笼统的说法,并不完全正确。让我指出一些事实:

  1. 如果设置 Control.CheckForIllegalCrossThreadCalls = false,实际上你可以随心所欲地进行 UI 调用。我听到你在说“啊!”、“永远不要那样做!”是的,是的——但为什么?答案是:因为有时这会破坏内存。

    System.Windows.Forms 中的控件类不是线程安全的,因此有时从后台线程更新它们可能会破坏内存。但如果这只是有时发生而不是总是,那么这告诉我们:它并不是调用 UI 代码本身,而是潜在的不安全的 UI 代码交错导致异常

  2. 为了加强第1点,考虑这个:从后台线程调用 UI 代码的“安全”方式是使用 Control.InvokeControl.BeginInvoke,对吧?但这也是一个 UI 调用;这只是我们应该从非 GUI 线程更新 GUI 时要进行的 UI 调用。我的意思是,显然,仅仅是从外部线程调用Control对象上的“任何”方法并不会导致混乱(如果是这样的话,我们甚至无法调用Invoke,完全被卡住了)。再次强调,不安全的分离 UI 调用同时发生可能导致破坏。

  3. 记住以上两点,问问自己:为什么从非 GUI 线程调用MessageBox.Show 是不安全的?会创建并显示一个完全独立的Form;它的属性不以任何方式与任何其他现有的 GUI 对象交互;实际上,除了从调用线程访问其DialogResult属性(仅通过Show方法的返回值),它不能以任何方式在任何地方被访问。

接下来,Conrad Albrecht 说:

...鉴于 Dan 引用的主题声称 Show() 设置了自己的消息泵,(尽管没有被证实,但我无法反驳)...

这是一个完全公正的观点(虽然我个人认为 Jared Par 的声誉足以让我不太倾向于怀疑他说的话)。无论如何,通过Reflector查看MessageBox.Show方法,可以看到以下代码片段:

Application.BeginModalMessageLoop();
try
{
    result = Win32ToDialogResult(SafeNativeMethods.MessageBox(new HandleRef(owner, zero), text, caption, type));
}
finally
{
    Application.EndModalMessageLoop();
    UnsafeNativeMethods.ThemingScope.Deactivate(userCookie);
}

进一步查看Application.BeginModalMessageLoop方法会发现以下内容:

ThreadContext.FromCurrent().BeginModalMessageLoop(null);

而这个ThreadContext.FromCurrent,反过来:

// [Reflector shows that currentThreadContext is a ThreadStatic member. -Dan]
if (currentThreadContext == null)
{
    currentThreadContext = new Application.ThreadContext();
}
return currentThreadContext;

我对这些较低级别的Windows结构不太了解,所以无法完全理解这段代码,但是这似乎证明了Jared在我早期评论中提到的答案中所说的正是事实(对于好奇的读者:MessageBox.Show()自动将线程传递给UI线程吗?)。

所以,是的。我完全同意MUG4N的观点。

(如果有人能够令人信服地证明我在这里还是错误的,请说出来。尽管我感觉我已经为为什么我相信MUG4N正确提供了充分的理由,但我显然不是100%确定的。)


原始问题

通常,您只想通知用户发生了某些事情,但实际上并不需要他们的任何输入。在这种常见情况下,我有时会看到像这样的代码:

MessageBox.Show("Something has occurred", "Something", MessageBoxButtons.OK);

众所周知,这段代码会导致一个小弹窗出现,只有一个确定按钮。问题是:此代码会阻塞(UI线程)。但在绝大多数情况下,如果你只有一个确定按钮,似乎很少需要阻塞。(阻塞的目的通常不是从用户那里接收一些输入吗?而在这种典型情况下,如果用户的唯一选择是“确定”,那么阻塞就显得相当无意义了,是吗?)

显然,我可以编写自己的小表单,实现与MessageBox.Show基本相同的功能,只是不返回任何内容(没有DialogResult),也不阻塞。但我想知道是否已经存在类似于此的东西,我不知道。


消息框的意图是在用户继续之前,让用户决定(如果有取消等)或确认某件事情(如果只是确定)。- 如果您认为需要非模态消息框,请重新考虑您的UI设计。什么是使用案例? - Patrick Klug
3
@Patrick:使用案例是通知用户某个事件已经发生。假设用户在应用程序中启动了某个过程,然后离开电脑去洗手间。当他们回来时,他们想知道该过程是否已完成。对我来说,消息框是向用户发出这种信号的最合适方式。同时,可能希望此消息框不会阻止应用程序继续运行其他进程。 - Dan Tao
4个回答

13

您需要使用多线程来执行此任务,其中一个线程(主线程)将进行处理,另一个线程将用于显示消息框。


1
@MUG4N:那是一种方法。但是MessageBox.Show仍然会阻塞调用它的线程,对吧?我猜你的意思是我可以编写一个新方法,从后台线程调用MessageBox.Show,是这样吗? - Dan Tao
5
@Dan - 你不能直接在后台线程中进行UI调用。 - ChrisF
根据我的经验,你可以从后台线程调用MessageBox.Show。我意识到这可能不被鼓励,但我没有找到任何表明绝对不应该这样做的文档。在测试过后,我实际上为这个答案点了赞。它可以正常工作--无需Control.CheckForIllegalCrossThreadCalls设置为true - Dan Tao
1
@Conrad:认真吗?你有试过MUG4N建议的方法吗?它完美地解决了问题。此外,根据我的研究,从后台线程调用MessageBox.Show实际上是完全没有问题的。例如,可以参考Jared Par对这个问题的回答:https://dev59.com/g3RB5IYBdhLWcg3wpotm - Dan Tao
“它能工作”并不意味着线程错误不会出现,这些错误是无法预测的。但是,鉴于在丹的参考主题中声称Show()设置了自己的消息泵(尽管没有得到证实,但我无法反驳),我将撤回我的投票反对此观点[如果StacO允许的话]。 - Conrad Albrecht
例如,这对我来说运行良好: new Thread(() => MessageBox.Show("message")).Start(); - Neil

7
添加NotifyIcon到您的应用程序,并显示气球提示,如何?唯一的缺点是通知会在短时间内消失,但如果用户不需要采取行动,那可能对他们最好。

此问题上还有更多建议。


在我看来,这更多是一个UI隐喻的问题,而不是特定小部件类的线程行为。如果你想要一个非模态行为的警报,那么除了MessageBox之外的其他东西似乎更合适。 - Marc Bollinger

2

我建议尝试直接从Win32 API调用MessageBox函数,例如:

using System.Runtime.InteropServices;

[DllImport("User32.dll")]
public static extern int MessageBox(int h, string m, string c, int type);

尝试使用空句柄和APPLMODAL类型。这可能有效。

1
这就是最阻塞的情况了。"消息框返回一个整数值,指示用户单击了哪个按钮。" 如果在对话框关闭之前返回,它显然无法执行此操作。 - Ben Voigt

0

这是一个老问题,但尽管如此...

1)导入IWshShell(“Windows脚本宿主对象模型”)

Dim wsh As New IWshRuntimeLibrary.WshShell wsh.Popup(String.Concat(Me.GetType.FullName, vbCrLf, _ Application.ExecutablePath), 0.75, "Title", MessageBoxButtons.OKCancel Or MessageBoxIcon.Question)

Jens...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接