👨‍🔧

C#定石 - モーダルダイアログに対する最小化の辻褄あわせ

に公開

はじめに

C# ソフト開発時に、決まり事として実施していた内容を記載します。

テスト環境

ここに記載した情報/ソースコードは、Visual Studio Community 2022 を利用した下記プロジェクトで生成したモジュールを Windows 11 24H2 で動作確認しています。

  • Windows Forms - .NET Framework 4.8
  • Windows Forms - .NET 8
  • WPF - .NET Framework 4.8
  • WPF - .NET 8

モーダルダイアログの最小化

Windows Forms において、メインフォームから Form.ShowDialog で、モーダルダイアログ表示したサブフォームを最小化した場合、サブフォームのみ最小化して、メインフォームは何の操作もできないのに、表示されたままという残念な挙動になっています。

対応方法

この残念な挙動の改善策として、いくつかの手法が考えられると思いますが、下記アプローチで考えることとします。

  • サブフォームの SizeChanged イベントハンドラ
    • サブフォームが最小化されたら、メインフォームを Visible = false にして非表示とします。
    • サブフォームが最小化から復帰したら、メインフォームを Visible = ture にして表示します。
  • サブフォームの FormClosed イベントハンドラ
    • タスクバー上、対象アプリのアイコンをマウスオーバーするとプレビュー画面が表示されて、プレビュー画面右端の X で対象フォームのクローズができてしまいます。
      前述 SizeChanged でサブフォームの最小化に連動して、メインフォームが Visible = false となった状態で、サブフォームをクローズするとメインフォームは非表示のままです。
      そこで FromClosed 時に、メインフォームを Visible = ture にして表示させます。

Form1 → Form2 → Form3 と多階層にサブフォームを Form.ShowDialog するケースまで考えると、上記に加えて、下記対処も必要となります。

  • サブフォームの VisibleChanged イベントハンドラ
    • 前述 SizeChanged の対処をして、Form3 を最小化すると、Form3.SizeChanged で Form2 は Visible = false となりますが、Form2.SizeChanged イベントは発生せず、 Form1 はそのままとなります。
      Form1 まで玉突き非表示とするには、Form2.VisibleChanged イベントハンドラで、Form1.Visible = Form2.Visible とする必要があります。

実装上のポイント1

Form1 から Form2 を Form.ShowDialog した場合、現時点では Form2 プロパティの Owner, Parent は null となっています。
Form2 から Form1 を操作させるために、呼び出し元の Form1 で Form2.Owner に Form1 をセットします。

var subForm = new Form2();
subForm.Owner = this;     // これを忘れずに !!
DialogResult result = subForm.ShowDialog();
subForm.Dispose();

実装上のポイント2

FormClosed イベント後に発生する VisibleChanged イベントでは、今回の対応を動作させたくないので、FormClosed イベントハンドラで CurrentFormClosed = true をセットします。

private bool CurrentFormClosed = false;  // FormClosed イベント後かの判断値

// VisibleChanged イベントハンドラ
private void Form2_VisibleChanged(object sender, EventArgs e)
{
  // this.Owner が Form でないならば以降の処理はしない
  if (this.Owner is Form owner)
  {
    // FormClosed イベント後に発生する VisibleChanged イベントでは処理しない
    if (!CurrentFormClosed)
    {
      owner.Visible = this.Visible;
    }
  }
}

// FormClosed イベントハンドラ
private void Form2_FormClosed(object sender, FormClosedEventArgs e)
{
  // this.Owner が Form でないならば以降の処理はしない
  if (this.Owner is Form owner)
  {
    owner.Visible = true;
  }
  // FormClosed イベント後に発生する VisibleChanged イベントで
  // 親フォームに対する処理を抑止するため CurrentFormClosed 更新
  CurrentFormClosed = true;
}

サンプルコード

Windows Forms - .NET Framework 4.8 / .NET 8

Form1 → Form2 → Form3 と Form.ShowDialog するサンプルコードです。
.NET Framework 4.8 と .NET8 で同一コードとなります。

https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.forms.form.showdialog

Form1.cs
private void DoAction()
{
  // サブフォームをモーダルダイアログ表示
  var subForm = new Form2();
  subForm.Owner = this;
  DialogResult result = subForm.ShowDialog();
  subForm.Dispose();
}
Form2.cs
private void DoAction()
{
  // サブフォームをモーダルダイアログ表示
  var subForm = new Form3();
  subForm.Owner = this;
  DialogResult result = subForm.ShowDialog();
  subForm.Dispose();
}

// SizeChanged イベントハンドラ
private void Form2_SizeChanged(object sender, EventArgs e)
{
  // this.Owner が Form でないならば以降の処理はしない
  if (this.Owner is Form owner)
  {
    if (this.WindowState == FormWindowState.Minimized)
    {
      // 最小化 → 呼び出し元を非表示
      owner.Visible = false;
    }
    else
    {
      owner.Visible = true;
    }
  }
}

// VisibleChanged イベントハンドラ
private void Form2_VisibleChanged(object sender, EventArgs e)
{
  // this.Owner が Form でないならば以降の処理はしない
  if (this.Owner is Form owner)
  {
    // FormClosed イベント後に発生する VisibleChanged イベントでは処理しない
    if (!CurrentFormClosed)
    {
      owner.Visible = this.Visible;
    }
  }
}

// FormClosed イベントハンドラ
private void Form2_FormClosed(object sender, FormClosedEventArgs e)
{
  // this.Owner が Form でないならば以降の処理はしない
  if (this.Owner is Form owner)
  {
    owner.Visible = true;
  }
  // FormClosed イベント後に発生する VisibleChanged イベントで
  // 親フォームに対する処理を抑止するため CurrentFormClosed 更新
  CurrentFormClosed = true;
}
Form3.cs
// SizeChanged イベントハンドラ
private void Form3_SizeChanged(object sender, EventArgs e)
{
  // this.Owner が Form でないならば以降の処理はしない
  if (this.Owner is Form owner)
  {
    if (this.WindowState == FormWindowState.Minimized)
    {
      // 最小化 → 呼び出し元を非表示
      owner.Visible = false;
    }
    else
    {
      owner.Visible = true;
    }
  }
}

// VisibleChanged イベントハンドラ
private void Form3_VisibleChanged(object sender, EventArgs e)
{
// Form3 から ShowDialog していないので処理不要
// // this.Owner が Form でないならば以降の処理はしない
// if (this.Owner is Form owner)
// {
//   // FormClosed イベント後に発生する VisibleChanged イベントでは処理しない
//   if (!CurrentFormClosed)
//   {
//     owner.Visible = this.Visible;
//   }
// }
} 

// FormClosed イベントハンドラ
private void Form3_FormClosed(object sender, FormClosedEventArgs e)
{
  // this.Owner が Form でないならば以降の処理はしない
  if (this.Owner is Form owner)
  {
    owner.Visible = true;
  }
  // FormClosed イベント後に発生する VisibleChanged イベントで
  // 親フォームに対する処理を抑止するため CurrentFormClosed 更新
  CurrentFormClosed = true;
}

WPF - .NET Framework 4.8

MainWindow → Window1 → Window2 と Window.ShowDialog するサンプルコードです。

https://learn.microsoft.com/ja-jp/dotnet/api/system.windows.window.showdialog

MainWindow.xaml.cs
private void DoAction()
{
  // サブフォームをモーダルダイアログ表示
  var subForm = new Window1();
  subForm.Owner = this;
  bool? dialogResult = subForm.ShowDialog();
}
Window1.xaml.cs
public Window1()
{
  InitializeComponent();

  StateChanged += Window1_StateChanged;
  Closed += Window1_Closed;
  IsVisibleChanged += Window1_IsVisibleChanged;
}

private void DoAction()
{
  // サブフォームをモーダルダイアログ表示
  var subForm = new Window2();
  subForm.Owner = this;
  bool? dialogResult = subForm.ShowDialog();
}

private bool CurrentFormClosed = false;

// .NET Framework - SizeChanged イベントハンドラの代替
private void Window1_StateChanged(object sender, System.EventArgs e)
{
  // this.Owner が Window でないならば以降の処理はしない
  if (this.Owner is Window owner)
  {
    if (this.WindowState == WindowState.Minimized)
    {
      // 最小化 → 呼び出し元を非表示
      owner.Visibility = Visibility.Collapsed;
    }
    else if (this.Owner.Visibility == Visibility.Collapsed)
    {
      owner.Visibility = Visibility.Visible;
    }
  }
}

// .NET Framework - VisibleChanged イベントハンドラ相当
private void Window1_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
  // this.Owner が Window でないならば以降の処理はしない
  if (this.Owner is Window owner)
  {
    if (!CurrentFormClosed)
    {
      if (this.Visibility == Visibility.Visible)
      {
        if (owner.Visibility == Visibility.Collapsed)
        {
          owner.Visibility = Visibility.Visible;
        }
      }
      else if (this.Visibility == Visibility.Collapsed)
      {
        owner.Visibility = Visibility.Collapsed;
      }
    }
  }
}

// .NET Framework - FormClosed イベントハンドラ相当
private void Window1_Closed(object sender, System.EventArgs e)
{
  // this.Owner が Window でないならば以降の処理はしない
  if (this.Owner is Window owner)
  {
    if (this.Owner.Visibility == Visibility.Collapsed)
    {
      owner.Visibility = Visibility.Visible;
    }
  }
  CurrentFormClosed = true;
}
Window2.xaml.cs
public Window2()
{
  InitializeComponent();

  StateChanged += Window2_StateChanged;
  Closed += Window2_Closed;
  IsVisibleChanged += Window2_IsVisibleChanged;
}

private void DoAction()
{
  // サブフォームをモーダルダイアログ表示
  var subForm = new Window2();
  subForm.Owner = this;
  bool? dialogResult = subForm.ShowDialog();
}

private bool CurrentFormClosed = false;

// .NET Framework - SizeChanged イベントハンドラの代替
private void Window2_StateChanged(object sender, System.EventArgs e)
{
  // this.Owner が Window でないならば以降の処理はしない
  if (this.Owner is Window owner)
  {
    if (this.WindowState == WindowState.Minimized)
    {
      // 最小化 → 呼び出し元を非表示
      owner.Visibility = Visibility.Collapsed;
    }
    else if (owner.Visibility == Visibility.Collapsed)
    {
      owner.Visibility = Visibility.Visible;
    }
  }
}

// .NET Framework - VisibleChanged イベントハンドラ相当
private void Window2_IsVisibleChanged(object sender, DependencyPropertyChangedEventArgs e)
{
// Window2 から ShowDialog していないので処理不要
// // this.Owner が Window でないならば以降の処理はしない
// if (this.Owner is Window owner)
// {
//   if (!CurrentFormClosed)
//   {
//     if (this.Visibility == Visibility.Visible)
//     {
//       if (owner.Visibility == Visibility.Collapsed)
//       {
//         owner.Visibility = Visibility.Visible;
//       }
//     }
//     else if (this.Visibility == Visibility.Collapsed)
//     {
//       owner.Visibility = Visibility.Collapsed;
//     }
//   }
// }
}

// .NET Framework - FormClosed イベントハンドラ相当
private void Window2_Closed(object sender, System.EventArgs e)
{
  // this.Owner が Window でないならば以降の処理はしない
  if (this.Owner is Window owner)
  {
    if (this.Owner.Visibility == Visibility.Collapsed)
    {
      owner.Visibility = Visibility.Visible;
    }
  }
  CurrentFormClosed = true;
}

WPF - .NET 8

MainWindow → Window1 → Window2 と Window.ShowDialog するサンプルコードです。

WPF - .NET Framework 4.8 とほぼ同一コードです。
イベントハンドラの第一引数を下記のように object? に変更するだけです。

Window1.xaml.cs
// .NET Framework - SizeChanged イベントハンドラの代替
private void Window1_StateChanged(object? sender, System.EventArgs e)
{
  // TODO
}

// .NET Framework - FormClosed イベントハンドラ相当
private void Window1_Closed(object? sender, System.EventArgs e)
{
  // TODO
}
Window2.xaml.cs
// .NET Framework - SizeChanged イベントハンドラの代替
private void Window2_StateChanged(object? sender, System.EventArgs e)
{
  // TODO
}

// .NET Framework - FormClosed イベントハンドラ相当
private void Window2_Closed(object? sender, System.EventArgs e)
{
  // TODO
}

出典

本記事は、2025/01/08 Qiita 投稿記事の転載です。

C#定石 - モーダルダイアログに対する最小化の辻褄あわせ

Discussion