使用任务栏关闭所有窗口时的奇怪表单关闭行为 [英] Odd form closing behavior when using taskbar's Close All Windows

查看:25
本文介绍了使用任务栏关闭所有窗口时的奇怪表单关闭行为的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I have a Windows Forms app with a main window and 0 or more other windows open. The other open windows are not owned by the main window nor are they modal dialogs or anything. However, the default behavior is if the main window closes then the application closes due to the Application.Run method returning. That's fine, but because the user may have unsaved work in the other opened windows I implemented some form closing logic.

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

When an other window is closed, it checks for unsaved changes and prompts the user with a standard Save/Don't save/Cancel Microsoft Word style prompt.

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

When the main window is closed, it attempts to first close all other open windows. If any of them fail to close (i.e. user clicked Cancel), then it stops the closing event.

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

This logic occurs in the FormClosing events and works great except if the user uses the taskbar's "Close all windows" command. This appears in 7's new taskbar as well as XP/Vista when grouping is active (though then it is labeled "Close Group").

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

This command seems to send a close message to all windows. The problem is that each other window checks for changes and prompts and then the main window attempts to close other windows. If I prompt the user using a standard MessageBox.Show command, then the closing event pauses while the dialog is waiting for a user response. Once a button is clicked, it is processed as normal, but all other windows either discard or disregard the window close command. It doesn't matter what they clicked, either. The form showing the prompt reacts correctly (if they hit Cancel it remains open, if not, it closes normally). But all other windows including the main act like nothing happened. Their FormClosing event is never raised.

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

If I use a TaskDialog (via calling unmanaged TaskDialogIndirect), then at the point when the prompt should appear and pause the form closing event, instead the other forms process their form closing events. This is on the same thread no less (the main UI thread). When the main window's turn comes around, it attempts to close all forms like normal. Any form that attempted to prompt is still open, the rest have since closed on their own due to the "Close all windows" command. The main window attempts to close those still remaining, resulting in a second FormClosing event to process and a second attempt to prompt (after all, the changes are still unsaved!) all on the main thread mind you.

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

The end result is that the prompt then appears twice in succession after unwinding through the call stack. I know this is all happening on the same thread via Visual Studio's call stack. I can look back all the way at any point to the first prompt attempt up to the time when it is about to call it again. Only the second call seems to actually process it and show the prompt. The first time through it is almost like somewhere in the unmanaged code it is yielding to other messages. I should mention up front that I do not call Application.DoEvents anywhere myself.

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

Is TaskDialogIndirect some kind of semi-asynchronous call? But I never leave the main thread through all of this so far as I can tell. And then why does the standard MessageBox prompt immediately (as I would think TaskDialog should too), but then appear to drop all the other window close events? Are the other window close messages just timing out maybe? Shouldn't they just be pending in the message queue until the modal dialog (the message box) returns?

我有一种感觉,这完全是由于 Windows 窗体的Win32 API 的托管包装器"的性质 —可能是 抽象抽象.

I have a feeling this is all due to the "managed wrapper for Win32 API" nature of Windows Forms — a leaky abstraction perhaps.

推荐答案

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

The Close all windows sends the WM_CLOSE to all windows in the taskbar group, which usually(always?) includes the main window. Many applications have a confirm dialog prompt on the main window, but not on child windows. Some child windows may receive the WM_CLOSE message before the main window and thus will be closed even if the user decides to cancel the close request.

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

Here is some code that intercepts the WM_CLOSE messages and then posts the WM_CLOSE to the main window if it was one of the windows that was sent the message. This prevents the child windows from closing, which is nice if the user decides to cancel the close request.

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

}

这篇关于使用任务栏关闭所有窗口时的奇怪表单关闭行为的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆