はじめに
C# ソフト開発時に、決まり事として実施していた内容を記載します。
「ワーキングダイアログ」は、「プログレスダイアログ」のことです。
本記事サンプルコードのクラス名から「ワーキングダイアログ」という呼称を利用しています。
参考情報
下記情報を参考にさせて頂きました。
素材と画像加工
サイクル GIF として下記サイトを利用させて頂きました。
GIF背景透過 加工サイトとして下記サイトを利用させて頂きました。
テスト環境
ここに記載した情報/ソースコードは、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
記載したソースコードは .NET 8 ベースとしています。
.NET Framework 4.8 の場合は、コメントで記載している null 許容参照型の明示 ?
を削除してください。
Visual Studio 2022 - .NET Framework 4.8 は、C# 7.3 が既定です。
このため、サンプルコードは、C# 7.3 機能範囲で記述しています。
ワーキングダイアログ
時間がかかる処理を行う際に、ワーキングダイアログを表示することがありました。
ワーキングダイアログは、サブ画面(Windows Form の場合は Form、WPF の場合は Window)、BackgroundWorker を利用した実装です。
上記図は基本的なメカニズムを表現したものです。
今回のサンプルでは、BackgroundWorker を内包した WorkingDialogManager を用意します。
BackgroundWorker.DoWork イベントハンドラ型 DoWorkEventHandler は下記の通りです。
public event System.ComponentModel.DoWorkEventHandler? DoWork;
public delegate void DoWorkEventHandler(object? sender, DoWorkEventArgs e);
キャンセル動作は下記流れとなります。
- サブ画面キャンセルボタンで BackgroundWorker.CancelAsync 実行
- メイン画面処理で定期的に BackgroundWorker.CancellationPending で確認
- キャンセル実施時には true となり、DoWorkEventArgs に値を設定して処理終了
ワーキングダイアログのレイアウトを決めます。
BackgroundWorker には、プログレスバーを考慮したメソッド/イベントがあります。
しかし、プログレスバーは更新タイミングに、モヤモヤすることがあるので、下記レイアウトで、STEP 実行内容を更新する形態を選択します。
前準備
【フリーアイコン】 くるくる回る で「処理中 丸2」を選択、「背景色 - 透明」にしてダウンロードします。
「背景色 - 透明」にしても、透過 GIF になってくれなかったので、Ezgif - free online animated GIF editor を利用して、「Upload Clip」で対象 GIF を選択、変換後「Download」でダウンロードして、Circle.gif にリネームします。
マネージャ
複数の画面、複数の処理で、ワーキングダイアログを手軽に利用できるようにしたいと思ます。
今回は、ワーキングダイアログ操作をまとめたクラス(マネージャという位置づけ)を用意します。
[プロジェクト][追加][新しい項目]で、[クラス]を選択して、WorkingDialogManager.cs を作成します。
Windows Forms と WPF では、軽微な差異があるので、まず、Windows Forms のサンプルコードを記載して、次に WPF の差分を記載します。
Windows Forms
public class WorkingDialogManager
{
public BackgroundWorker Worker { get; } = new BackgroundWorker();
public bool IsCancelling { get { return Worker.CancellationPending; } }
private WorkingDialog? Dialog = null; // .NET Framework 時 ? 不要
public WorkingDialogManager()
{
Worker.WorkerSupportsCancellation = true;
Worker.WorkerReportsProgress = false;
}
// ワーキングダイアログ表示して処理実行
public DialogResult ShowDialog(Form form, DoWorkEventHandler proc)
{
// ワーキングダイアログ表示中の処理登録
Worker.DoWork += proc;
// ワーキングダイアログ表示
Dialog = new WorkingDialog(this);
Dialog.Owner = form;
var result = Dialog.ShowDialog();
Dialog.Dispose();
Dialog = null;
// 再利用するので、登録したイベントハンドラ解除
Worker.DoWork -= proc;
// 必要に応じて下記処理実施
// Application.DoEvents(); // メッセージキュー滞留を全て処理
// form.Activate(); // アクティブ化
return result;
}
// ワーキングダイアログ表示更新
public void SetStepMessage(int count, int max, string msg)
{
SetStepMessage(string.Format($"STEP[{count}/{max}]\r\n{msg}"));
}
public void SetStepMessage(string msg)
{
Dialog?.SetStepMessage(msg);
}
}
ダイアログ表示メッセージ更新は、BackgroundWorker で用意されてるプログレスバーを考慮したメソッド/イベントを利用することもできますが、SetStepMessage を作成しています。
ワーキングダイアログ呼び出し元の操作については、複数の画面、複数の処理で手軽に利用という目的で、本クラスで全てラップしました。
ワーキングダイアログ側の固有操作は、とりあえず、本クラスの対象から除外しています。必要に応じて、本クラスに追加してください。
WPF
WPF では、下記差異があるので、ShowDialog を書き換えます。
- 画面は Form ではなく Window
- モーダル表示の戻り値が DialogResult ではなく bool?
public bool? ShowDialog(Window window, DoWorkEventHandler proc)
{
// ワーキングダイアログ表示中の処理登録
Worker.DoWork += proc;
// ワーキングダイアログ表示
Dialog = new WorkingDialog(this);
Dialog.Owner = window;
var result = Dialog.ShowDialog();
// 再利用するので、登録したイベントハンドラ解除
Worker.DoWork -= proc;
//// 必要に応じて下記処理実施 - DispatcherPriority は要確認
//Application.Current.Dispatcher.Invoke(
// System.Windows.Threading.DispatcherPriority.Background,
// new Action(delegate { })); // UIスレッドキューを全て処理
//window.Activate(); // アクティブ化
return result;
}
WPF でワーキングダイアログ利用は今までなく、今回 WPF 化をしました。
コメントアウトして記載している Dispatcher 処理は、Copilot に Application.DoEvents 代替えを確認した回答に基づいたコードで、マニュアル、および、コメントを外して動作させて問題ないことのみを確認しています。
このため、実際に利用する場合には、適切な処理に書き換えてください。
サブ画面
Windows Forms と WPF では、大幅な差異があるので、Windows Forms と WPF のサンプルそれぞれを記載します。
Windows Forms
[プロジェクト][追加][新しい項目]で、[Form(Windows フォーム)]を選択して、WorkingDialog.cs を作成します。
WorkingDialog.cs デザイナ画面で、下記レイアウトを作成してください。
デザイナ画面では、位置/サイズなどを設定してください。
画面横幅は、表示する文字列の最大幅を考慮して設定することを推奨します。
後述ソースコードで「AutoSizeMode.GrowOnly」「FormStartPosition.CenterParent」としていますが、途中で画面横幅が大きくなっても、画面左上位置はそのままで、自動的にセンタリングはされないためです。
PictureBox pbWorking には下記プロパティをセットしてください。
プロパティ | 設定値 |
---|---|
SizeMode | StretchImage |
Image | ローカルリソース インポートを選択、前準備で用意した Circle.gif を設定 |
サンプルコードを記載します。
public partial class WorkingDialog : Form
{
private WorkingDialogManager? wdManager = null; // .NET Framework 時 ? 不要
public WorkingDialog(WorkingDialogManager manager)
{
InitializeComponent();
// クラス変数に保持
wdManager = manager;
// Formプロパティとイベントハンドラ
AutoSize = true;
AutoSizeMode = AutoSizeMode.GrowOnly;
ControlBox = false;
FormBorderStyle = FormBorderStyle.FixedDialog;
MaximizeBox = false;
ShowIcon = false;
ShowInTaskbar = false;
StartPosition = FormStartPosition.CenterParent;
Text = "処理中";
Shown += WorkingDialog_Shown;
FormClosed += WorkingDialog_FormClosed;
// 配置コントロール設定
SetStepMessage("初期化中...");
if (!wdManager.Worker.WorkerSupportsCancellation)
{
btnCancel.Enabled = false;
}
else
{
btnCancel.Click += btnCancel_Click;
}
// BackgroundWorker イベントハンドラ登録
wdManager.Worker.RunWorkerCompleted += BackgroundWorker_RunWorkerCompleted;
}
// .NET Framework 時 object? の ? 不要
private void WorkingDialog_Shown(object? sender, EventArgs e)
{
// 処理中でないことを確認
if (wdManager?.Worker.IsBusy == false)
{
wdManager.Worker.RunWorkerAsync();
}
}
// .NET Framework 時 object? の ? 不要
private void WorkingDialog_FormClosed(object? sender, FormClosedEventArgs e)
{
// wdManager.Worker 再利用するので、登録したイベントハンドラ解除
if (wdManager?.Worker != null)
{
wdManager.Worker.RunWorkerCompleted -= BackgroundWorker_RunWorkerCompleted;
}
}
// .NET Framework 時 object? の ? 不要
private void btnCancel_Click(object? sender, EventArgs e)
{
btnCancel.Enabled = false;
// 処理中確認
if (wdManager?.Worker.IsBusy == true)
{
// 処理をキャンセル
wdManager.Worker.CancelAsync();
}
}
// .NET Framework 時 object? の ? 不要
private void BackgroundWorker_RunWorkerCompleted(object? sender,
RunWorkerCompletedEventArgs e)
{
if ((e.Error is null) == false)
{
// エラー時の処理 - TODO
DialogResult = DialogResult.Abort;
}
else if (e.Cancelled)
{
// キャンセル時の処理
DialogResult = DialogResult.Cancel;
}
else
{
// 完了時の処理
DialogResult = DialogResult.OK;
}
// ダイアログを閉じる
this.Close();
}
// ワーキングダイアログ表示更新
public void SetStepMessage(string msg)
{
MethodInvoker method = () => { lblStepMessage.Text = msg; };
// メインスレッド以外で、コントロール更新は Invoke 利用
if (this.InvokeRequired)
{
Invoke(method);
}
else
{
method();
}
}
}
SetStepMessage での Invoke 処理、スレッド セーフなコントロールの呼び出し | C# プログラミング解説 を参考にさせて頂きました。
このようにシンプルな記述にできることは知りませんでした。
WPF
WPF - Image コントロールの標準機能では、アニメーション GIF がサポートされていないため、NuGet Gallery | WpfAnimatedGif を導入します。
PM> Install-Package WpfAnimatedGif
プロジェクトに Images フォルダを作成して、前準備で用意した Circle.gif を Images フォルダ下にコピーします。(.NET Framework 4.8 の場合、自動的にプロジェクトに追加されないようなので、プロジェクトに追加してください)
ソリューションエクスプローラで、コピーした Circle.gif を「ビルドアクション - リソース」とします。(.NET Framework 4.8 の場合、Resource と英文表記となっています)
[プロジェクト][追加][新しい項目]で、[ウィンドウ(WPF)]を選択して、WorkingDialog.xaml を作成します。
xaml サンプルコードを記載します。
<Window x:Class="YourAppName.WorkingDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:gif="http://wpfanimatedgif.codeplex.com"
xmlns:local="clr-namespace:YourAppName"
mc:Ignorable="d"
Title="WorkingDialog" Height="140" Width="300" >
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="48"/>
<RowDefinition Height="30"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="48"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="80"/>
</Grid.ColumnDefinitions>
<Image Name="imgCircle" Grid.Row="0" Grid.Column="0"
gif:ImageBehavior.AnimatedSource="pack://application:,,,/Images/Circle.gif"
Stretch="Uniform"/>
<Label Name="lblStepMessage" Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2"
Content="初期化中..." VerticalAlignment="Center" Margin="10,0,0,0" />
<Button Name="btnCancel" Grid.Row="1" Grid.Column="3"
Content="キャンセル" Margin="0,10,0,0" />
</Grid>
</Window>
WpfAnimatedGif 用に xmlns:gif="http://wpfanimatedgif.codeplex.com"
を追記しています。
x:Class="YourAppName.WorkingDialog"
および xmlns:local="clr-namespace:YourAppName"
に記述されている YourAppName
は作成したプロジェクトの namespece に変更してください。
かなり雑なレイアウトで申し訳ありません。
Window の Height/Width、および、各コントールのレイアウトは適宜変更してください。
C# サンプルコードを記載します。
public partial class WorkingDialog : Window
{
private WorkingDialogManager? wdManager = null; // .NET Framework 時 ? 不要
public WorkingDialog(WorkingDialogManager manager)
{
InitializeComponent();
// クラス変数に保持
wdManager = manager;
// GIF イメージ設定
var image = new BitmapImage(new Uri("pack://application:,,,/Images/Circle.gif"));
ImageBehavior.SetAnimatedSource(imgCircle, image);
// Windowプロパティとイベントハンドラ
ResizeMode = ResizeMode.NoResize;
ShowInTaskbar = false;
SizeToContent = SizeToContent.Manual;
Title = "処理中";
WindowStartupLocation = WindowStartupLocation.CenterOwner;
ContentRendered += Window_ContentRendered;
Closed += Window_Closed;
// 配置コントロール設定
SetStepMessage("初期化中...");
if (!wdManager.Worker.WorkerSupportsCancellation)
{
btnCancel.IsEnabled = false;
}
else
{
btnCancel.Click += btnCancel_Click;
}
// BackgroundWorker イベントハンドラ登録
wdManager.Worker.RunWorkerCompleted += BackgroundWorker_RunWorkerCompleted;
}
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
// Windows Fomrs での ControlBox=false 相当 - 最小化、最大化、クローズボタン非表示
IntPtr handle = new WindowInteropHelper(this).Handle;
long style = (long)NativeMethods.GetWindowLong(handle, NativeMethods.GWL_STYLE);
NativeMethods.SetWindowLong(handle, NativeMethods.GWL_STYLE,
style & ~NativeMethods.WS_SYSMENU);
}
private static class NativeMethods
{
[DllImport("user32.dll")]
public static extern IntPtr SetWindowLong(IntPtr hWnd, int nIndex, long dwLong);
[DllImport("user32.dll")]
public static extern IntPtr GetWindowLong(IntPtr hWnd, int nIndex);
public const int GWL_STYLE = (-16);
public const int WS_SYSMENU = 0x00080000;
}
// .NET Framework 時 object? の ? 不要
private void Window_ContentRendered(object? sender, EventArgs e)
{
// 処理中でないことを確認
if (wdManager?.Worker.IsBusy == false)
{
wdManager.Worker.RunWorkerAsync();
}
}
// .NET Framework 時 object? の ? 不要
private void Window_Closed(object? sender, EventArgs e)
{
// wdManager.Worker 再利用するので、登録したイベントハンドラ解除
if (wdManager?.Worker != null)
{
wdManager.Worker.RunWorkerCompleted -= BackgroundWorker_RunWorkerCompleted;
}
}
// .NET Framework 時 object? の ? 不要
private void btnCancel_Click(object? sender, RoutedEventArgs e)
{
btnCancel.IsEnabled = false;
// 処理中確認
if (wdManager?.Worker.IsBusy == true)
{
// 処理をキャンセル
wdManager.Worker.CancelAsync();
}
}
// .NET Framework 時 object? の ? 不要
private void BackgroundWorker_RunWorkerCompleted(object? sender,
RunWorkerCompletedEventArgs e)
{
if ((e.Error is null) == false)
{
// エラー時の処理 - TODO
DialogResult = false;
}
else if (e.Cancelled)
{
// キャンセル時の処理
DialogResult = false;
}
else
{
// 完了時の処理
DialogResult = true;
}
// ダイアログを閉じる
this.Close();
}
// ワーキングダイアログ表示更新
public void SetStepMessage(string msg)
{
// 本来は MVVM とすべきですが、サンプルなので簡易的に下記としています
Dispatcher.Invoke((Action)(() => { lblStepMessage.Content = msg; }));
}
}
呼び出し元
Windows Forms と WPF では、軽微な差異があるので、まず、Windows Forms のサンプルコードを記載して、次に WPF の差分を記載します。
Windows Forms
WorkingDialogManager をクラス変数として作成します。
// WorkingDialog 操作クラス
private readonly WorkingDialogManager wdManager = new WorkingDialogManager();
WorkingDialog 利用は、WorkingDialogManager 経由で下記手順となります。
// ワーキングダイアログ表示して処理実行
DialogResult result = wdManager.ShowDialog(this, DoWorkSample10);
//// wdManager 再利用テストの待機
// Thread.Sleep(500);
//
//// wdManager 再利用テスト
// result = wdManager.ShowDialog(this, DoWorkSample15);
// .NET Framework 時 object? の ? 不要
private void DoWorkSample10(object? sender, DoWorkEventArgs e)
{
DoWorkSample(10, "〇〇〇処理中...", e);
}
// .NET Framework 時 object? の ? 不要
private void DoWorkSample15(object? sender, DoWorkEventArgs e)
{
DoWorkSample(15, "△△△処理中...", e);
}
private void DoWorkSample(int max, string msg, DoWorkEventArgs e)
{
for (int count = 1; count <= max; count++)
{
// ワーキングダイアログ表示更新
wdManager.SetStepMessage(count, max, msg);
// ワーキングダイアログでのキャンセル確認
if (wdManager.IsCancelling)
{
e.Cancel = true;
return;
}
// 実際の処理代替で待機
Thread.Sleep(500);
}
}
WorkingDialogManager.ShowDialog 第2引数で指定した処理では、クラス変数の参照/更新、コントロール参照が可能です。
コントロール更新は、WorkingDialog.SetStepMessage と同様な形態で、Invoke が必要となります。
WPF
WPF では、下記差異があるので、WorkingDialogManager利用方法
を書き換えます。
- モーダル表示の戻り値が DialogResult ではなく bool?
// ワーキングダイアログ表示して処理実行
bool? result = wdManager.ShowDialog(this, DoWorkSample10);
//// wdManager 再利用テストの待機
// Thread.Sleep(500);
//
//// wdManager 再利用テスト
// result = wdManager.ShowDialog(this, DoWorkSample15);
補足
本実装では、WorkingDialogManager から、WorkingDialog を呼び出す際に、引数として WorkingDialogManager を渡しています。
エラー情報などは、WorkingDialogManager にプロパティを追加することで、相互に情報伝達させることも可能です。
public int ErrorCode { get; set; } = 0;
public string ErrorMessage { get; set; } = string.Empty;
WorkingDialogManager を再利用するケースを考慮して、初期化のタイミングを忘れずに入れてください。
必要であれば、エラー情報初期化のメソッドを追加してください。