【问题标题】:Odd form closing behavior when using taskbar's Close All Windows使用任务栏的关闭所有窗口时出现奇怪的表单关闭行为
【发布时间】:2010-04-14 16:14:27
【问题描述】:

我有一个 Windows 窗体应用程序,该应用程序带有一个主窗口并打开了 0 个或多个其他窗口。其他打开的窗口不属于主窗口,也不是模式对话框或任何东西。但是,默认行为是如果主窗口关闭,则应用程序会由于 Application.Run 方法返回而关闭。这很好,但是因为用户可能在其他打开的窗口中有未保存的工作,所以我实现了一些表单关闭逻辑。

当另一个窗口关闭时,它会检查未保存的更改并使用标准的保存/不保存/取消 Microsoft Word 样式提示来提示用户。

当主窗口关闭时,它会首先尝试关闭所有其他打开的窗口。如果其中任何一个未能关闭(即用户单击取消),则它会停止关闭事件。

这个逻辑发生在 FormClosing 事件中并且工作得很好,除非用户使用任务栏的“关闭所有窗口”命令。当分组处于活动状态时,它会出现在 7 的新任务栏中以及 XP/Vista 中(尽管它被标记为“关闭组”)。

此命令似乎向所有窗口发送关闭消息。问题是每个其他窗口检查更改和提示,然后主窗口尝试关闭其他窗口。如果我使用标准 MessageBox.Show 命令提示用户,则关闭事件会在对话框等待用户响应时暂停。单击按钮后,它会正常处理,但所有其他窗口要么放弃或忽略窗口关闭命令。他们点击什么也没关系。显示提示的表单反应正确(如果他们点击取消它保持打开,如果没有,它正常关闭)。但是包括主窗口在内的所有其他窗口都像什么都没发生一样。他们的 FormClosing 事件永远不会引发。

如果我使用TaskDialog(通过调用非托管TaskDialogIndirect),那么在提示应该出现并暂停表单关闭事件时,其他表单会处理它们的表单关闭事件。这是在同一个线程上(主 UI 线程)。当主窗口出现时,它会尝试像平常一样关闭所有表单。任何试图提示的表单仍处于打开状态,其余的由于“关闭所有窗口”命令而自行关闭。主窗口会尝试关闭那些仍然存在的窗口,从而导致第二次 FormClosing 事件要处理和第二次尝试提示(毕竟,更改仍未保存!)所有这些都在主线程上,请注意。

最终的结果是,在通过调用堆栈展开后,提示会连续出现两次。我知道这一切都是通过 Visual Studio 的调用堆栈在同一个线程上发生的。我可以随时回顾第一次提示尝试,直到它即将再次调用它。只有第二个调用似乎实际处理它并显示提示。第一次通过它几乎就像在非托管代码中的某个地方它正在让步给其他消息。我应该先提一下,我自己不会在任何地方调用 Application.DoEvents。

TaskDialogIndirect 是某种半异步调用吗?但据我所知,我从未离开过所有这一切。然后为什么标准的 MessageBox 会立即提示(我认为 TaskDialog 也应该如此),但随后似乎删除了所有其他窗口关闭事件?其他窗口关闭消息是否只是超时?他们不应该只是在消息队列中等待模式对话框(消息框)返回吗?

我觉得这完全是由于 Windows 窗体的“Win32 API 的托管包装”性质——也许是leaky abstraction

【问题讨论】:

    标签: winforms


    【解决方案1】:

    关闭所有窗口(自 XP 起)实现有点 hacky。在您的FormClosing 实现中,检查表单是否被禁用,因为TaskDialog 或任何其他提示显示为表单是提示的所有者。

    当您使用它时,请检查您的关闭方案在 WM_QUERYENDSESSION 上的执行情况,即用户注销并等待更改。

    【讨论】:

    • 我有适当的检查,如果发生关机或任务管理器强制退出,它会跳过这些检查更改并丢弃它们。 Close-all-windows 命令显示为标准的 UserClosing 原因,就像他们单击红色 X 一样。我无法检查打开的 TaskDialogs,因为它们尚未出现。我第一次尝试展示一个时,它似乎在实际出现之前屈服于其他形式的关闭事件。
    • 在显示保存更改提示之前检查您的表单是否有 WS_DISABLED。启动记事本并输入任何内容,然后尝试注销并得到确认。
    【解决方案2】:

    Close all windowsWM_CLOSE 发送到任务栏组中的所有窗口,通常(总是?)包括主窗口。许多应用程序在主窗口上都有确认对话框提示,但在子窗口上没有。一些子窗口可能会在主窗口之前收到WM_CLOSE 消息,因此即使用户决定取消关闭请求也会关闭。

    这里有一些代码拦截WM_CLOSE 消息,然后拦截postsWM_CLOSE 到主窗口(如果它是发送消息的窗口之一)。这可以防止子窗口关闭,如果用户决定取消关闭请求,这很好。

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Drawing;
    using System.Linq;
    using System.Runtime.InteropServices;
    using System.Text;
    using System.Threading;
    using System.Windows.Forms;
    
    namespace WindowsFormsApplication1 {
    
    public static class CloseAllWindowsHandler {
    
        private const int WM_CLOSE = 0x10;
        private const int WM_DESTROY = 0x2;
    
        static List<NW> closing = new List<NW>();
        static List<NW> nws = new List<NW>();
        static Thread thread = null;
        static IntPtr hwndMainWindow = IntPtr.Zero;
    
        private class NW : NativeWindow {
    
            // determine to allow or deny the WM_CLOSE messages
            bool intercept = true;
    
            public NW() {}
    
            protected override void WndProc(ref System.Windows.Forms.Message m) {
                if (m.Msg == WM_CLOSE) {
                    if (!intercept) {
                        intercept = true;
                        base.WndProc(ref m);
                        return;
                    }
    
                    closing.Add(this);
    
                    Thread t = null;
                    t = new Thread(() => {
                        try {
                            Thread.Sleep(100);
                        } catch {}
    
                        if (thread == t) {
                            // no more close requests received in the last 100 ms
                            // if a close request was sent to the main window, then only post a message to it
                            // otherwise send a close request to each root node at the top of the owner chain
                            NW nwMain = null;
                            foreach (NW nw in closing) {
                                if (nw.Handle == hwndMainWindow) {
                                    nwMain = nw;
                                    break;
                                }
                            }
    
                            BackgroundWorker bgw = new BackgroundWorker();
                            var closing2 = closing;
                            closing = new List<NW>();
                            bgw.RunWorkerCompleted += (o, e) => {
                                try {
                                    if (nwMain != null) {
                                        // if the 'Close all windows' taskbar menu item is clicked, then closing2.Count
                                        // will contain all the window handles
                                        nwMain.intercept = false;
                                        PostMessage(hwndMainWindow, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
                                    }
                                    else {
                                        // doesn't seem to ever happen, closing2.Count always equals 1
                                        // so nothing really has to be done
                                        // if (closing2.Count > 1)
    
                                        foreach (NW nw in closing2) {
                                            nw.intercept = false;
                                            PostMessage(nw.Handle, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
                                        }
                                    }
                                    bgw.Dispose();
                                } catch {}
                            };
                            bgw.RunWorkerAsync();
                        }
                    });
                    thread = t;
                    t.IsBackground = true;
                    t.Priority = ThreadPriority.Highest;
                    t.Start();
                    return;
                }
                else if (m.Msg == WM_DESTROY) {
                    ReleaseHandle();
                    nws.Remove(this);
                }
    
                base.WndProc(ref m);
            }
        }
    
        [DllImport("user32.dll")]
        private static extern bool PostMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
    
        [DllImport("user32.dll")]
        public static extern IntPtr GetParent(IntPtr hWnd);
    
        private static void RegisterWindow(IntPtr hwnd) {
            NW nw = new NW();
            nws.Add(nw); // prevent garbage collection
            nw.AssignHandle(hwnd);
        }
    
        private const int WINEVENT_OUTOFCONTEXT = 0;
        private const int EVENT_OBJECT_CREATE = 0x8000;
    
        public static void AssignHook(IntPtr mainWindowHandle) {
            hwndMainWindow = mainWindowHandle;
            uint pid = 0;
            uint tid = GetWindowThreadProcessId(mainWindowHandle, out pid);
            CallWinEventProc = new WinEventProc(EventCallback);
            hHook = SetWinEventHook(EVENT_OBJECT_CREATE, EVENT_OBJECT_CREATE, IntPtr.Zero, CallWinEventProc, pid, tid, WINEVENT_OUTOFCONTEXT);      
        }
    
        [DllImport("user32.dll")]
        private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
    
        [DllImport("user32.dll")]
        private static extern IntPtr SetWinEventHook(uint eventMin, uint eventMax, IntPtr hmodWinEventProc, WinEventProc lpfnWinEventProc, uint idProcess, uint idThread, uint dwFlags);
    
        [DllImport("user32.dll")]
        private static extern int UnhookWinEvent(IntPtr hWinEventHook);
    
        private static IntPtr hHook = IntPtr.Zero;
        private static WinEventProc CallWinEventProc;
        private delegate void WinEventProc(IntPtr hWinEventHook, int iEvent, IntPtr hWnd, int idObject, int idChild, int dwEventThread, int dwmsEventTime);
        private static void EventCallback(IntPtr hWinEventHook, int iEvent, IntPtr hWnd, int idObject, int idChild, int dwEventThread, int dwmsEventTime) {
            if (iEvent == EVENT_OBJECT_CREATE) {    
                IntPtr pWnd = GetParent(hWnd);
                if (pWnd == IntPtr.Zero) { // top level window
                    RegisterWindow(hWnd);
                }
            }
        }
    }
    
    public class Form2 : Form {
    
        public Button btnOpen = new Button { Text = "Open" };
        public CheckBox cbConfirmClose = new CheckBox { Text = "Confirm Close" };
        private static int counter = 0;
        public Form2() {
            Text = "Form" + counter++;
            FlowLayoutPanel panel = new FlowLayoutPanel { Dock = DockStyle.Top };
            panel.Controls.AddRange(new Control [] { btnOpen, cbConfirmClose });
            Controls.Add(panel);
    
            btnOpen.Click += btnOpen_Click;
        }
    
        void btnOpen_Click(object sender, EventArgs e) {
            Form2 f = new Form2();
            f.Owner = this;
            f.Size = new Size(300,300);
            f.Show();
        }
    
        protected override void OnFormClosing(FormClosingEventArgs e) {
            if (cbConfirmClose.Checked) {
                var dr = MessageBox.Show(this, "Confirm close?", "Close " + Text, MessageBoxButtons.OKCancel);
                if (dr != System.Windows.Forms.DialogResult.OK)
                    e.Cancel = true;
            }
    
            base.OnFormClosing(e);
        }
    }
    
    public class Program2 {
    
        [STAThread]
        static void Main() {
    
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Form2 f2 = new Form2();
            f2.HandleCreated += delegate {
                CloseAllWindowsHandler.AssignHook(f2.Handle);
            };
            Application.Run(f2);
        }
    }
    
    }
    

    【讨论】:

      猜你喜欢
      • 2021-05-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多