2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2025-01-08

はじめに

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

本情報について、Windows Forms は Form クラス、WPF は Window クラスなどの違いがあり、双方を併記すると訳が分からなくなりそうなので、以降は Windows Forms ベースでの記載とします。

WPF に関しては、Windows Forms サンプルコードと同等の対応をしたサンプルコードのみを記載します。

テスト環境

ここに記載した情報/ソースコードは、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 で、モーダルダイアログ表示したサブフォームを最小化した場合、サブフォームのみ最小化して、メインフォームは何の操作もできないのに、表示されたままという残念な挙動になっています。

Form.Show を用いた場合は、複数サブフォームを同時表示できるので、ひとつのサブフォームを最小化しても、メインフォームの表示/非表示に影響を与えないのは当然の動作だと思います。

状況は異なりますが、Form.Show した場合、メインフォームの最小化で、サブフォーム(複数でも)を連動して非表示にしてくれてます。
このようなフォーム同士の関係性に基づいた挙動を 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 とする必要があります。

上記、対応方法は、Form.ShowDialog のみ利用で、Form.Show が対象ソース上にないという前提のものです。

実装上のポイント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();

「現時点では Form2 プロパティの Owner, Parent は null となっています」と、スッキリしない文面としています。
このようにした理由は、ひと昔前まで、 Form1 から Form2 を Form.ShowDialog した場合、Form2.Owner = Form1 が自動的にセットされていた気がするからです。
どこかの時点からセットされなくなったと思うのですが、今となっては確かめようがありません、、、

実装上のポイント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)
  {
    // FormClosed イベント後に発生する VisibleChanged イベントでは処理しない
    if (!CurrentFormClosed)
    {
      this.Owner.Visible = this.Visible;
    }
  }
}

// FormClosed イベントハンドラ
private void Form2_FormClosed(object sender, FormClosedEventArgs e)
{
  // this.Owner が Form でないならば以降の処理はしない
  if (this.Owner is Form)
  {
    this.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 で同一コードとなります。

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)
  {
    if (this.WindowState == FormWindowState.Minimized)
    {
      // 最小化 → 呼び出し元を非表示
      this.Owner.Visible = false;
    }
    else
    {
      this.Owner.Visible = true;
    }
  }
}

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

// FormClosed イベントハンドラ
private void Form2_FormClosed(object sender, FormClosedEventArgs e)
{
  // this.Owner が Form でないならば以降の処理はしない
  if (this.Owner is Form)
  {
    this.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)
  {
    if (this.WindowState == FormWindowState.Minimized)
    {
      // 最小化 → 呼び出し元を非表示
      this.Owner.Visible = false;
    }
    else
    {
      this.Owner.Visible = true;
    }
  }
}

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

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

WPF - .NET Framework 4.8

MainWindow → Window1 → Window2 と 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)
  {
    if (this.WindowState == WindowState.Minimized)
    {
      // 最小化 → 呼び出し元を非表示
      this.Owner.Visibility = Visibility.Collapsed;
    }
    else if (this.Owner.Visibility == Visibility.Collapsed)
    {
      this.Owner.Visibility = Visibility.Visible;
    }
  }
}

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

// .NET Framework - FormClosed イベントハンドラ相当
private void Window1_Closed(object sender, System.EventArgs e)
{
  // this.Owner が Window でないならば以降の処理はしない
  if (this.Owner is Window)
  {
    if (this.Owner.Visibility == Visibility.Collapsed)
    {
      this.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)
  {
    if (this.WindowState == WindowState.Minimized)
    {
      // 最小化 → 呼び出し元を非表示
      this.Owner.Visibility = Visibility.Collapsed;
    }
    else if (this.Owner.Visibility == Visibility.Collapsed)
    {
      this.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)
// {
//   if (!CurrentFormClosed)
//   {
//     if (this.Visibility == Visibility.Visible)
//     {
//       if (this.Owner.Visibility == Visibility.Collapsed)
//       {
//         this.Owner.Visibility = Visibility.Visible;
//       }
//     }
//     else if (this.Visibility == Visibility.Collapsed)
//     {
//       this.Owner.Visibility = Visibility.Collapsed;
//     }
//   }
// }
}

// .NET Framework - FormClosed イベントハンドラ相当
private void Window2_Closed(object sender, System.EventArgs e)
{
  // this.Owner が Window でないならば以降の処理はしない
  if (this.Owner is Window)
  {
    if (this.Owner.Visibility == Visibility.Collapsed)
    {
      this.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
}
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?