DOBON.NET プログラミング道: .NET Framework, VB.NET, C#, Visual Basic, Visual Studio, インストーラ, ...

フォームが閉じられる時その原因を知る
QueryUnloadイベントのUnloadModeに代わるものは?

VB6のQueryUnloadイベントにおけるUnloadModeのように、フォームが閉じられる時にどうしてフォームが閉じられようとしているのか(ウィンドウの「閉じる」ボタンのクリックにより閉じられようとしているのか、コードのCloseメソッドにより閉じられようとしているのか等)知るにはどのようにすればよいのかについて説明します。

.NET Framework 2.0以降で、FormClosingイベントなどを使用する方法

.NET Framework 2.0からはFormClosingイベントが追加され、フォームが閉じられようとしているときに、その理由を簡単に知ることができるようになりました。FormClosingイベントハンドラで取得できるFormClosingEventArgsオブジェクトのCloseReasonプロパティで閉じられる理由を知ることができます。

まったく同じように、FormClosedイベントハンドラでフォームが閉じられた理由を知ることができます。

以下にCloseReasonの値と、そのときの理由を表に示します。

CloseReasonのメンバ フォームが閉じられる原因
ApplicationExitCall Application.Exitが呼び出された。
FormOwnerClosing このフォームを所有しているフォームが閉じられようとしている。
MdiFormClosing このフォームのMDI親フォームが閉じられようとしている。
TaskManagerClosing タスクマネージャーがアプリケーションを終了させようとしている。
UserClosing ユーザーが、ウィンドウの「閉じる」ボタン(ウィンドウの右上のXボタン)を押した、コントロールメニューの「閉じる」を選択した、Alt + F4キーを押したなど。フォームのCloseメソッドを呼び出したときも。
WindowsShutDown OSが終了するために、アプリケーションを終了させようとしている。
None 不明

フォームが閉じられるときにその理由を表示するサンプルを以下に示します。

VB.NET
コードを隠すコードを選択
'フォームのFormClosingイベントハンドラ
Private Sub Form1_FormClosing(ByVal sender As System.Object, _
        ByVal e As System.Windows.Forms.FormClosingEventArgs) _
        Handles MyBase.FormClosing
    Select Case e.CloseReason
        Case CloseReason.ApplicationExitCall
            Console.WriteLine("Application.Exitによる")
        Case CloseReason.FormOwnerClosing
            Console.WriteLine("所有側のフォームが閉じられようとしている")
        Case CloseReason.MdiFormClosing
            Console.WriteLine("MDIの親フォームが閉じられようとしている")
        Case CloseReason.TaskManagerClosing
            Console.WriteLine("タスクマネージャによる")
        Case CloseReason.UserClosing
            Console.WriteLine("ユーザーインターフェイスによる")
        Case CloseReason.WindowsShutDown
            Console.WriteLine("OSのシャットダウンによる")
        Case CloseReason.None
            Console.WriteLine("未知の理由")
        Case Else
            Console.WriteLine("それ以外")
    End Select
End Sub
C#
コードを隠すコードを選択
//フォームのFormClosingイベントハンドラ
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
    switch (e.CloseReason)
    {
        case CloseReason.ApplicationExitCall:
            Console.WriteLine("Application.Exitによる");
            break;
        case CloseReason.FormOwnerClosing:
            Console.WriteLine("所有側のフォームが閉じられようとしている");
            break;
        case CloseReason.MdiFormClosing:
            Console.WriteLine("MDIの親フォームが閉じられようとしている");
            break;
        case CloseReason.TaskManagerClosing:
            Console.WriteLine("タスクマネージャによる");
            break;
        case CloseReason.UserClosing:
            Console.WriteLine("ユーザーインターフェイスによる");
            break;
        case CloseReason.WindowsShutDown:
            Console.WriteLine("OSのシャットダウンによる");
            break;
        case CloseReason.None:
        default:
            Console.WriteLine("未知の理由");
            break;
    }
}

WndProcメソッドをオーバーライドする方法

.NET Framework 1.1以前で最も一般的なのは、フォームのWndProcメソッドをオーバーライドし、送られてくるメッセージを調べるという方法です。次の例では、WM_ENDSESSION、WM_SYSCOMMAND、WM_CLOSEが送られてきたか調べ、フォームが閉じられる原因がOSのシャットダウンによるか、Xボタン(「閉じる」ボタン)やコントロールメニューによるか、コードによるか、判断しています。

VB.NET
コードを隠すコードを選択
'Imports System.Security.Permissions

<SecurityPermission(SecurityAction.Demand, _
    Flags:=SecurityPermissionFlag.UnmanagedCode)> _
Protected Overrides Sub WndProc(ByRef m As Message)
    Const WM_CLOSE As Integer = &H10
    Const WM_ENDSESSION As Integer = &H16
    Const WM_SYSCOMMAND As Integer = &H112
    Const SC_CLOSE As Long = &HF060L

    Select Case m.Msg
        Case WM_ENDSESSION
            'OSのシャットダウンで閉じられようとしている
            Console.WriteLine("WM_ENDSESSION")
            Exit Select
        Case WM_SYSCOMMAND
            If (m.WParam.ToInt64() And &HFFF0L) = SC_CLOSE Then
                'Xボタン、コントロールメニューの「閉じる」、
                'コントロールボックスのダブルクリック、
                'Atl+F4などにより閉じられようとしている
                Console.WriteLine("SC_CLOSE")
            End If
            Exit Select
        Case WM_CLOSE
            'Application.Exit以外で閉じられようとしている
            Console.WriteLine("WM_CLOSE")
            Exit Select
    End Select

    MyBase.WndProc(m)
End Sub
C#
コードを隠すコードを選択
//using System.Security.Permissions;

[SecurityPermission(SecurityAction.Demand,
    Flags = SecurityPermissionFlag.UnmanagedCode)]
protected override void WndProc(ref Message m)
{
    const int WM_CLOSE = 0x0010;
    const int WM_ENDSESSION = 0x16;
    const int WM_SYSCOMMAND = 0x112;
    const long SC_CLOSE = 0xF060L;

    switch (m.Msg)
    {
        case WM_ENDSESSION:
            //OSのシャットダウンで閉じられようとしている
            Console.WriteLine("WM_ENDSESSION");
            break;
        case WM_SYSCOMMAND:
            if ((m.WParam.ToInt64() & 0xFFF0L) == SC_CLOSE)
                //Xボタン、コントロールメニューの「閉じる」、
                //コントロールボックスのダブルクリック、
                //Atl+F4などにより閉じられようとしている
                Console.WriteLine("SC_CLOSE");
            break;
        case WM_CLOSE:
            //Application.Exit以外で閉じられようとしている
            Console.WriteLine("WM_CLOSE");
            break;
    }

    base.WndProc(ref m);
}

StackFrameを使った方法

この方法はStackFrameを使うというちょっと変わった方法で、「GotDotNet Message Boards - Form.Closing...」(リンク切れ)で紹介されています。

ここで紹介されているYeahIGotDotNetさん、MikeWill34さんの書いたコード及び、「The Code Project - Find out what's closing your application」で紹介されているEvilDoctorSmithさんのコードを参考にさせていただき、次のようなコードを書いてみました。詳しくは、これらのリンク先をご覧ください。

注意:このコードは、.NET Framework 1.1以前を考慮して書かれています。.NET Framework 2.0以降では、FormクラスのClosing、Closedイベントの代わりに、FormClosing、FormClosedイベントを使用してください。
VB.NET
コードを隠すコードを選択
Private Sub Form1_Closing(ByVal sender As Object, _
        ByVal e As System.ComponentModel.CancelEventArgs) _
        Handles MyBase.Closing
    Dim stack As New System.Diagnostics.StackTrace(True)
    Dim frame7 As System.Diagnostics.StackFrame = stack.GetFrame(7)
    Select Case frame7.GetMethod().Name
        Case "DispatchMessageW"
            Console.WriteLine("タスクマネージャーによる")
        Case "SendMessage"
            Console.WriteLine("コードによる")
        Case "CallWindowProc"
            If stack.FrameCount > 14 Then
                Dim frame14 As System.Diagnostics.StackFrame = _
                    stack.GetFrame(14)
                If frame14.GetMethod().Name = "WmSysCommand" Then
                    Console.WriteLine( _
                        "Xボタンまたはコントロールメニューによる")
                Else
                    If frame14.GetMethod().Name = "WndProc" Then
                        Console.WriteLine("OSのシャットダウンによる")
                    End If
                End If
            End If
        Case "DefMDIChildProc"
            Console.WriteLine( _
                "MDI子フォームのXボタンまたはコントロールメニューによる")
        Case "DefFrameProc"
            Console.WriteLine("MDI親フォームが閉じられたことによる")
        Case "ShowDialog"
            Console.WriteLine("モーダルダイアログが閉じられたことによる")
        Case Else
            Console.WriteLine("原因不明")
    End Select
End Sub
C#
コードを隠すコードを選択
private void Form1_Closing(object sender,
    System.ComponentModel.CancelEventArgs e)
{
    System.Diagnostics.StackTrace stack =
        new System.Diagnostics.StackTrace(true);
    System.Diagnostics.StackFrame frame7 = stack.GetFrame(7);
    switch (frame7.GetMethod().Name)
    {
        case "DispatchMessageW":
            Console.WriteLine("タスクマネージャーによる");
            break;
        case "SendMessage":
            Console.WriteLine("コードによる");
            break;
        case "CallWindowProc":
            if (stack.FrameCount > 14)
            {
                System.Diagnostics.StackFrame frame14 =
                    stack.GetFrame(14);
                if (frame14.GetMethod().Name == "WmSysCommand")
                    Console.WriteLine(
                        "Xボタンまたはコントロールメニューによる");
                else if (frame14.GetMethod().Name == "WndProc")
                    Console.WriteLine("OSのシャットダウンによる");
            }
            break;
        case "DefMDIChildProc":
            Console.WriteLine(
                "MDI子フォームのXボタンまたはコントロールメニューによる");
            break;
        case "DefFrameProc":
            Console.WriteLine("MDI親フォームが閉じられたことによる");
            break;
        case "ShowDialog":
            Console.WriteLine("モーダルダイアログが閉じられたことによる");
            break;
        default:
            Console.WriteLine("原因不明");
            break;
    }
}

隠れたフォームを使う方法

最後に紹介するのは、hidden windowを使った方法です。これは、「Visual Studio Magazine - Determine a Form's UnloadMode」(リンク切れのため、Internet Archiveへのリンク)で紹介されているものです。先に紹介したWndProcメソッドによる方法では判断できない原因を調べるために、隠れたフォームを使うという方法のようです。詳しくは、リンク先をご覧ください。

  • 履歴:
  • 2007/1/15 「.NET Framework 2.0以降で、FormClosingイベントなどを使用する方法」を追加。
  • 2010/6/30 WndProcにSecurityPermissionAttributeを付けた。
  • 2012/4/9 「WndProcメソッドをオーバーライドする方法」のコードが64ビット環境でうまくいかない不具合を修正。
  • 2013/12/9 SecurityAction.LinkDemandの代わりにSecurityAction.Demandを使うようにした。

注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。

  • イベントハンドラの意味が分からない、C#のコードをそのまま書いても動かないという方は、こちらをご覧ください。
  • コードの先頭に記述されている「Imports ??? がソースファイルの一番上に書かれているものとする」(C#では、「using ???; がソースファイルの一番上に書かれているものとする」)の意味が分からないという方は、こちらをご覧ください。
  • .NET Tipsをご利用いただく際は、注意事項をお守りください。