强制程序关闭MessageBox

26

让我先来介绍一下背景。

我们有一个中等规模的应用程序,它在各个地方(数百处)使用 MessageBox.Show(...)。

这些消息框是工作流程的一部分,用于通知、警告或从用户那里获取输入。如果没有活动,应用程序应自动注销。我们有一个要求,在注销应用程序时,只需清除会话数据、清除视图并隐藏它本身,以便在下次启动时,不必执行启动过程,该过程在时间成本方面很高。

一切运行良好,但是当屏幕上有某个消息框而用户没有响应该消息框,并且由于没有活动而导致应用程序注销时,问题就出现了。问题是消息框不会消失。

如何在隐藏应用程序的同时关闭打开的消息框(如果有的话)?


也许发送键盘的回车或ESC键? :) - Renatas M.
我认为 MessageBox.Show(...) 是模态的,那么程序怎么能发送一个键呢?你是使用线程/任务吗? - Fischermaen
谢谢回复。只是想澄清,使用自定义消息框不是一个选项,因为重新设计的工作量很大。发送 ESC 键也不正确,因为只有活动应用程序才会接收到命令。我正在使用 FindWindow 方法,在通过 id 和消息框标题传递参数后,获取 Msgbox 句柄。获取句柄后,我使用以下 win32 API 关闭它,例如 SendMessage(new HandleRef(null, msgbxcHandler), WM_CLOSE, IntPtr.Zero, IntPtr.Zero); SendMessage(new HandleRef(null, msgbxcHandler), WM_NCDESTROY, IntPtr.Zero, IntPtr.Zero); 到目前为止一切正常。 - NYK
12个回答

14

这是一段基于UIAutomation的代码(一个酷但仍未被广泛使用的API),它试图关闭当前进程中的所有模态窗口(包括使用MessageBox打开的窗口):

    /// <summary>
    /// Attempt to close modal windows if there are any.
    /// </summary>
    public static void CloseModalWindows()
    {
        // get the main window
        AutomationElement root = AutomationElement.FromHandle(Process.GetCurrentProcess().MainWindowHandle);
        if (root == null)
            return;

        // it should implement the Window pattern
        object pattern;
        if (!root.TryGetCurrentPattern(WindowPattern.Pattern, out pattern))
            return;

        WindowPattern window = (WindowPattern)pattern;
        if (window.Current.WindowInteractionState != WindowInteractionState.ReadyForUserInteraction)
        {
            // get sub windows
            foreach (AutomationElement element in root.FindAll(TreeScope.Children, new PropertyCondition(AutomationElement.ControlTypeProperty, ControlType.Window)))
            {
                // hmmm... is it really a window?
                if (element.TryGetCurrentPattern(WindowPattern.Pattern, out pattern))
                {
                    // if it's ready, try to close it
                    WindowPattern childWindow = (WindowPattern)pattern;
                    if (childWindow.Current.WindowInteractionState == WindowInteractionState.ReadyForUserInteraction)
                    {
                        childWindow.Close();
                    }
                }
            }
        }
    }

例如,如果您有一个WinForms应用程序,当您按下某个button1时会弹出一个MessageBox,您仍然可以使用Windows的“关闭窗口”菜单(在任务栏中右键单击)关闭该应用程序。
    private void button1_Click(object sender, EventArgs e)
    {
        MessageBox.Show("Don't click me. I want to be closed automatically!");
    }

    protected override void WndProc(ref System.Windows.Forms.Message m)
    {
        const int WM_SYSCOMMAND = 0x0112;
        const int SC_CLOSE = 0xF060;

        if (m.Msg == WM_SYSCOMMAND) // this is sent even if a modal MessageBox is shown
        {
            if ((int)m.WParam == SC_CLOSE)
            {
                CloseModalWindows();
                Close();
            }
        }
        base.WndProc(ref m);
    }

您可以在代码的其他位置使用CloseModalWindows,这只是一个示例。

1
@ErikFunkenbusch - 不知道你所指的“针对WPF”。UIAutomation支持任何类型的应用程序。 - Simon Mourier
1
也许是这样,但UIAutomation并不仅限于WPF,无论是作为客户端还是服务器。同一篇文章还说:“为除WPF以外的框架开发UI Automation提供程序的开发人员。” - Simon Mourier
1
谢谢,帮了我很多。 - digitguy
1
这符合我的需求。 - Albert Alberto
1
我在一个使用计时器的WPF应用程序中进行了测试,当计时器滴答作响时,调用了方法CloseModalWindows(),效果非常好,谢谢! - Paul Efford
显示剩余2条评论

7

这个链接在MSDN论坛上展示了如何使用FindWindow和发送WM_CLOSE消息来关闭消息框。虽然该问题是针对.NET/WindowsCE提出的,但它可能解决您的问题,值得一看。


4
我认为加入至少一点额外的信息来解决问题是一个很好的做法(而不仅仅是发布裸链接)。我希望你不介意我加了一个简短的描述。无论如何,这可能是一个有用的资源。 - MartinStettner
马丁,你说得完全正确。由于时间复杂度的原因,我无法添加这些信息。不过感谢你的编辑。 :) - Bravo

6

请参考"在若干秒后关闭MessageBox"的DmitryG帖子

在超时到达后自动关闭MessageBox

using System.Runtime.InteropServices;
using System.Threading;
using System.Windows.Forms;

    public class AutoClosingMessageBox
    {
        System.Threading.Timer _timeoutTimer;
        string _caption;
        AutoClosingMessageBox(string text, string caption, int timeout)
        {
            _caption = caption;
            _timeoutTimer = new System.Threading.Timer(OnTimerElapsed,
                null, timeout, System.Threading.Timeout.Infinite);
            MessageBox.Show(text, caption);
        }
        public static void Show(string text, string caption, int timeout)
        {
            new AutoClosingMessageBox(text, caption, timeout);
        }
        void OnTimerElapsed(object state)
        {
            IntPtr mbWnd = FindWindow(null, _caption);
            if (mbWnd != IntPtr.Zero)
                SendMessage(mbWnd, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
            _timeoutTimer.Dispose();
        }
        const int WM_CLOSE = 0x0010;
        [System.Runtime.InteropServices.DllImport("user32.dll", SetLastError = true)]
        static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
        [System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Auto)]
        static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);
    }

并通过

调用它

AutoClosingMessageBox.Show("Content", "Title", TimeOut);

似乎这个解决方案无法访问MessageBox的返回选项? - Paul Efford

3
这是我使用SendKeys的示例-经过测试可以正常工作:
假设我们有一个后台工作者和一个表单中的按钮。在单击按钮后,启动工作者并显示消息框。在工作者的DoWork事件中,睡眠5秒,然后发送回车键-消息框关闭。
private void button1_Click(object sender, EventArgs e)
{
    backgroundWorker1.RunWorkerAsync();
    MessageBox.Show("Close this message!");
}

private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
    Thread.Sleep(5000);
    SendKeys.SendWait("{Enter}");//or Esc
}

4
我想指出,对于解决方案来说,检查是否真的有消息框打开非常重要:发送给用户界面的任何其他部分的回车键可能会产生不良后果... - MartinStettner
是的,你说得很好。这是一个快速解决方法,所以回车键显然不是一个好的键,最好使用esc键 - 在用户界面中它应该代表取消。 - Renatas M.
2
这个解决方案只在用户没有切换焦点到其他应用程序时有效。你不能确定应用程序仍然拥有焦点。而且Windows禁止应用程序通过编程方式从另一个应用程序获取焦点。 - Alex
2
此外,如果MessageBox有Yes和No按钮,“Esc”键无法使用。 - Sandy

3
首先有一个问题:如果消息框作为工作流的一部分使用,那么程序关闭消息框是否会导致流程改变/继续?
我认为你有三个选择:
  1. 创建你自己的消息框类版本,它打开一个对话框窗口,看起来像一个带有附加功能的消息框,所以在一段时间后自动关闭。
  2. 在C#中实现类似于此的东西,以便可以编程关闭消息框。 http://www.codeproject.com/KB/dialog/AutoCloseMessageBox.aspx
  3. 消除中断工作流的消息框。这可能是最好的解决方案,因为从声音的角度来看,编程关闭消息框将导致工作流程继续/改变,并且甚至可能导致另一个消息框显示,这可能不是理想的。但显然修复根本问题可能是最好的,但并不总是最容易的。
1和2需要从单独的线程中完成,因此您需要考虑到这一点,因为显示消息框将会阻塞。

+1 点赞第三点!我也不会有好的感觉,如果在不知道用户是否注意到的情况下,仅仅“确认”任何打开的消息框。 - MartinStettner

3

这个主题在其他SO问题中已经充分涵盖,但由于这个特定的问题有几个关于使用UI自动化/窗口查找技术(我不是特别喜欢)和创建自己的对话框而没有提供代码的通用建议,因此我决定发布自己的解决方案。可以创建一个可实例化的MessageBox类,如下所示:

using System;
using System.Drawing;
using System.Linq;
using System.Windows.Forms;
using System.Text.RegularExpressions;

namespace Common
{
    // Loosely based on: https://www.codeproject.com/Articles/17253/A-Custom-Message-Box
    class MsgBox : Form
    {
        private Panel _plHeader = new Panel();
        private Panel _plFooter = new Panel();
        private Panel _plIcon = new Panel();
        private PictureBox _picIcon = new PictureBox();
        private FlowLayoutPanel _flpButtons = new FlowLayoutPanel();
        private Label _lblMessage;

        private MsgBox()
        {
            FormBorderStyle = FormBorderStyle.FixedDialog;
            BackColor = Color.White;
            StartPosition = FormStartPosition.CenterScreen;
            MinimizeBox = false;
            MaximizeBox = false;
            ShowIcon = false;
            Width = 400;

            _lblMessage = new Label();
            _lblMessage.Font = new Font("Segoe UI", 10);
            _lblMessage.Dock = DockStyle.Fill;
            _lblMessage.TextAlign = ContentAlignment.MiddleLeft;

            _flpButtons.FlowDirection = FlowDirection.RightToLeft;
            _flpButtons.Dock = DockStyle.Fill;

            //_plHeader.FlowDirection = FlowDirection.TopDown;
            _plHeader.Dock = DockStyle.Fill;
            _plHeader.Padding = new Padding(20);
            _plHeader.Controls.Add(_lblMessage);

            _plFooter.Dock = DockStyle.Bottom;
            _plFooter.BackColor = Color.FromArgb(240, 240, 240);
            _plFooter.Padding = new Padding(10);
            _plFooter.Height = 60;
            _plFooter.Controls.Add(_flpButtons);

            _picIcon.Location = new Point(30, 50);

            _plIcon.Dock = DockStyle.Left;
            _plIcon.Padding = new Padding(20);
            _plIcon.Width = 70;
            _plIcon.Controls.Add(_picIcon);

            Controls.Add(_plHeader);
            Controls.Add(_plIcon);
            Controls.Add(_plFooter);
        }

        public static DialogResult Show(IWin32Window owner, string message, string title = null, MessageBoxButtons? buttons = MessageBoxButtons.OK, MessageBoxIcon icon = MessageBoxIcon.Information)
        {
            var msgBox = Create(message, title, buttons, icon);
            return msgBox.ShowDialog(owner);
        }

        public static DialogResult Show(string message, string title = null, MessageBoxButtons? buttons = MessageBoxButtons.OK, MessageBoxIcon icon = MessageBoxIcon.Information)
        {
            var msgBox = Create(message, title, buttons, icon);
            return msgBox.ShowDialog();
        }

        public static MsgBox Create(string message, string title = null, MessageBoxButtons? buttons = MessageBoxButtons.OK, MessageBoxIcon icon = MessageBoxIcon.Information)
        {
            var msgBox = new MsgBox();
            msgBox.Init(message, title, buttons, icon);
            return msgBox;
        }

        void Init(string message, string title, MessageBoxButtons? buttons, MessageBoxIcon icon)
        {
            _lblMessage.Text = message;
            Text = title;
            InitButtons(buttons);
            InitIcon(icon);
            Size = MessageSize(message);
        }

        void InitButtons(MessageBoxButtons? buttons)
        {
            if (!buttons.HasValue)
                return;

            switch (buttons)
            {
                case MessageBoxButtons.AbortRetryIgnore:
                    AddButton("Ignore");
                    AddButton("Retry");
                    AddButton("Abort");
                    break;

                case MessageBoxButtons.OK:
                    AddButton("OK");
                    break;

                case MessageBoxButtons.OKCancel:
                    AddButton("Cancel");
                    AddButton("OK");
                    break;

                case MessageBoxButtons.RetryCancel:
                    AddButton("Cancel");
                    AddButton("Retry");
                    break;

                case MessageBoxButtons.YesNo:
                    AddButton("No");
                    AddButton("Yes");
                    break;

                case MessageBoxButtons.YesNoCancel:
                    AddButton("Cancel");
                    AddButton("No");
                    AddButton("Yes");
                    break;
            }
        }

        void InitIcon(MessageBoxIcon icon)
        {
            switch (icon)
            {
                case MessageBoxIcon.None:
                    _picIcon.Hide();
                    break;
                case MessageBoxIcon.Exclamation:
                    _picIcon.Image = SystemIcons.Exclamation.ToBitmap();
                    break;

                case MessageBoxIcon.Error:
                    _picIcon.Image = SystemIcons.Error.ToBitmap();
                    break;

                case MessageBoxIcon.Information:
                    _picIcon.Image = SystemIcons.Information.ToBitmap();
                    break;

                case MessageBoxIcon.Question:
                    _picIcon.Image = SystemIcons.Question.ToBitmap();
                    break;
            }

            _picIcon.Width = _picIcon.Image.Width;
            _picIcon.Height = _picIcon.Image.Height;
        }

        private void ButtonClick(object sender, EventArgs e)
        {
            Button btn = (Button)sender;

            switch (btn.Text)
            {
                case "Abort":
                    DialogResult = DialogResult.Abort;
                    break;

                case "Retry":
                    DialogResult = DialogResult.Retry;
                    break;

                case "Ignore":
                    DialogResult = DialogResult.Ignore;
                    break;

                case "OK":
                    DialogResult = DialogResult.OK;
                    break;

                case "Cancel":
                    DialogResult = DialogResult.Cancel;
                    break;

                case "Yes":
                    DialogResult = DialogResult.Yes;
                    break;

                case "No":
                    DialogResult = DialogResult.No;
                    break;
            }

            Close();
        }

        private static Size MessageSize(string message)
        {
            int width=350;
            int height = 230;

            SizeF size = TextRenderer.MeasureText(message, new Font("Segoe UI", 10));

            if (message.Length < 150)
            {
                if ((int)size.Width > 350)
                {
                    width = (int)size.Width;
                }
            }
            else
            {
                string[] groups = (from Match m in Regex.Matches(message, ".{1,180}") select m.Value).ToArray();
                int lines = groups.Length+1;
                width = 700;
                height += (int)(size.Height+10) * lines;
            }
            return new Size(width, height);
        }

        private void AddButton(string caption)
        {
            var btn = new Button();
            btn.Text = caption;
            btn.Font = new Font("Segoe UI", 8);
            btn.BackColor = Color.FromArgb(225, 225, 225);
            btn.Padding = new Padding(3);
            btn.Height = 30;
            btn.Click += ButtonClick;
            _flpButtons.Controls.Add(btn);
        }
    }
}

可以将对话框的引用保存在类作用域中,显示对话框并获取结果,或者在应用程序退出事件处理程序中关闭对话框。

MsgBox _msgBox;

void eventHandler1(object sender, EventArgs e)
{
    _msgBox = MsgBox.Create("Do you want to continue", "Inquiry", MessageBoxButtons.YesNo);
    var result = _msgBox.ShowDialog();
    // do something with result
}

void applicationExitHandler(object sender, EventArgs e)
{
    if (_msgBox != null)
        _msgBox.Close();
}

1
好的,但是这个解决方案是在模仿MessageBox,因此它缺少原始功能。 - SQL Police
1
@SQLPolice 这仍然是一个强大的起点,可以添加任何缺失的内容或自定义方面。有时候人们需要原始的 MessageBox 没有的功能,因此制作自定义的 MessageBox 类非常有用。在其他 SO 回答中,我还发现了一些关于使用 MessageBox.Show()IWin32Window 所有者参数来插入低级别不可见窗口的很酷的解决方案。我也喜欢那种方法,但我无法在短时间内使其足够强大。 - ceztko

2

我认为最干净的方法是实现自己的消息框表单,就像这样:

class MyMessageBox : Form {
  private MyMessageBox currentForm; // The currently active message box

  public static Show(....) { // same as MessageBox.Show
    // ...
  }

  public static Show(...) { // define additional overloads
  }

  public static CloseCurrent() {
    if (currentForm != null)
      currentForm.Close();
  }

  // ...
}

在我一些较大的项目中,我发现这种方法也可用于其他目的(例如自动记录错误消息等)。
我提出的第二个想法是使用GetTopWindow()(或者可能是其他WIN32函数)获取您应用程序的当前顶级窗口,并向其发送WM_CLOSE消息。

如果用户按下 alt+tab 键或者其他消息框显示在期望的消息框之上,那么会发生什么?在这种情况下,我猜它会关闭不需要的那个。 - Vivekh

1
我使用了 .net 2 和两种相同的方法来实现这个技巧。
通过在 stub-Form 中使用 MessageBox.Show(this,"message") 打开 MessageBox。
当表单不可见或没有真正的 UI 时。
  1. 保留表单处理程序并使用以下代码关闭它:

    static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);
    

    或者

  2. 将表单作为类参数保持,并使用 FormX.Close()

由于表单是MessageBox的所有者,关闭它将关闭MessageBox。

1

假设您可以编辑调用MessageBox.Show()方法的代码,我建议不要使用MessageBox。相反,只需使用自己的自定义表单,在其上调用ShowDialog()以执行与MessageBox类基本相同的操作。然后,您就拥有了表单实例本身,并且可以在该实例上调用Close()来关闭它。

一个很好的例子是这里


哎呀。我们看到很多人重新实现消息框时出了多糟糕的问题。 msg 表明您可以拥有一个真正的消息框,并在延迟后关闭它... - Joey
@Joey:是的,你说得对...那只是一个想法,可能不是最好的。你说“_msg显示你可以有一个真正的消息框,并在延迟后关闭它_”...什么或谁是“msg”?抱歉,我不明白,请原谅。 - Marco

1
最简单的解决方案是创建一个在计时器触发时关闭的表单。
private int interval = 0;
private string message = "";

public msgBox(string msg = "", int i = 0)
{
    InitializeComponent();
    interval = i;
    message = msg;
}

private void MsgBox_Load(object sender, EventArgs e)
{
    if (interval > 0)
        timer1.Interval = interval;

    lblMessage.Text = message;
    lblMessage.Width = panel1.Width - 20;
    lblMessage.Left = 10;
}

private void Timer1_Tick(object sender, EventArgs e)
{
    this.Close();
}

private void Panel1_Paint(object sender, PaintEventArgs e)
{
    ControlPaint.DrawBorder(e.Graphics, this.panel1.ClientRectangle, Color.DarkBlue, ButtonBorderStyle.Solid);
}

在主表单中使用的方法

   private void showMessage(string msg, int interval = 0)
    {
        msgBox mb = new msgBox(msg, interval);
        mb.ShowDialog(this);
    }

叫它

  showMessage("File saved");

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