Zenn
🤹

C#定石 - ワーキングダイアログ(プログレスダイアログ)

に公開2

はじめに

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

ワーキングダイアログ

時間がかかる処理を行う際に、ワーキングダイアログを表示することがありました。
ワーキングダイアログは、サブ画面(Windows Form の場合は Form、WPF の場合は Window)、BackgroundWorker を利用した実装です。

キャンセル動作は下記流れとなります。

  • サブ画面キャンセルボタンで 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

WorkingDialogManager.cs
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);
  }
}

WPF

WPF では、下記差異があるので、ShowDialog を書き換えます。

  • 画面は Form ではなく Window
  • モーダル表示の戻り値が DialogResult ではなく bool?
WorkingDialogManager.cs
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;
}

サブ画面

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();
    }
  }
}

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 サンプルコードを記載します。

WorkingDialog.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:WpfAppNet"
        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>

C# サンプルコードを記載します。

WorkingDialog.xaml.cs
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 経由で下記手順となります。

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);
  }
}

WPF

WPF では、下記差異があるので、WorkingDialogManager利用方法 を書き換えます。

  • モーダル表示の戻り値が DialogResult ではなく bool?
WorkingDialogManager利用方法
// ワーキングダイアログ表示して処理実行
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;

出典

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

C#定石 - ワーキングダイアログ(プログレスダイアログ)

Discussion

junerjuner

これなら普通に 代入して using した方がいいのではないでしょうか……?

Dialog = new WorkingDialog(this);
var result = Dialog.ShowDialog();
Dialog.Dispose();
Dialog = null;
Dialog = new WorkingDialog(this);
DialogResult result;
using (Dialog) {
    result = Dialog.ShowDialog();
}
Dialog = null;
@chai0917(渡辺 久彦)@chai0917(渡辺 久彦)

ご意見ありがとうございます。
using にすれば Dispose 不要で Dispose 忘れが抑止できるということは認識しており、ご指摘の記述とするか、現在の記述とするかは悩みました。

今回については、現在の記述のほうが簡潔と考えて、現在の記述とさせて頂いています。
私感に基づく記述ということでご了承ください。

ログインするとコメントできます