使用任务栏的“关闭所有Windows”时出现奇怪的表单关闭行为

时间:2010-04-14 16:14:27

标签: winforms

我有一个带有主窗口的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

2 个答案:

答案 0 :(得分:0)

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

当你在它时,检查你的结算方案在WM_QUERYENDSESSION上的执行情况,即用户注销未决的更改。

答案 1 :(得分:0)

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);
    }
}

}