はじめに
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 にして表示させます。
- タスクバー上、対象アプリのアイコンをマウスオーバーするとプレビュー画面が表示されて、プレビュー画面右端の X で対象フォームのクローズができてしまいます。
Form1 → Form2 → Form3 と多階層にサブフォームを Form.ShowDialog するケースまで考えると、上記に加えて、下記対処も必要となります。
- サブフォームの VisibleChanged イベントハンドラ
- 前述 SizeChanged の対処をして、Form3 を最小化すると、Form3.SizeChanged で Form2 は Visible = false となりますが、Form2.SizeChanged イベントは発生せず、 Form1 はそのままとなります。
Form1 まで玉突き非表示とするには、Form2.VisibleChanged イベントハンドラで、Form1.Visible = Form2.Visible とする必要があります。
- 前述 SizeChanged の対処をして、Form3 を最小化すると、Form3.SizeChanged で Form2 は Visible = false となりますが、Form2.SizeChanged イベントは発生せず、 Form1 はそのままとなります。
上記、対応方法は、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 で同一コードとなります。
private void DoAction()
{
// サブフォームをモーダルダイアログ表示
var subForm = new Form2();
subForm.Owner = this;
DialogResult result = subForm.ShowDialog();
subForm.Dispose();
}
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;
}
// 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 するサンプルコードです。
private void DoAction()
{
// サブフォームをモーダルダイアログ表示
var subForm = new Window1();
subForm.Owner = this;
bool? dialogResult = subForm.ShowDialog();
}
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;
}
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?
に変更するだけです。
// .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
}
// .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
}