0
0

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-02-24

はじめに

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 を利用した実装です。

Chart.png

上記図は基本的なメカニズムを表現したものです。
今回のサンプルでは、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 実行内容を更新する形態を選択します。

layout-original.png

前準備

【フリーアイコン】 くるくる回る で「処理中 丸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);
  }
}

ダイアログ表示メッセージ更新は、BackgroundWorker で用意されてるプログレスバーを考慮したメソッド/イベントを利用することもできますが、SetStepMessage を作成しています。

ワーキングダイアログ呼び出し元の操作については、複数の画面、複数の処理で手軽に利用という目的で、本クラスで全てラップしました。
ワーキングダイアログ側の固有操作は、とりあえず、本クラスの対象から除外しています。必要に応じて、本クラスに追加してください。

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

WPF でワーキングダイアログ利用は今までなく、今回 WPF 化をしました。
コメントアウトして記載している Dispatcher 処理は、Copilot に Application.DoEvents 代替えを確認した回答に基づいたコードで、マニュアル、および、コメントを外して動作させて問題ないことのみを確認しています。
このため、実際に利用する場合には、適切な処理に書き換えてください。

サブ画面

Windows Forms と WPF では、大幅な差異があるので、Windows Forms と WPF のサンプルそれぞれを記載します。

Windows Forms

[プロジェクト][追加][新しい項目]で、[Form(Windows フォーム)]を選択して、WorkingDialog.cs を作成します。
WorkingDialog.cs デザイナ画面で、下記レイアウトを作成してください。

layout-WinForm.png

デザイナ画面では、位置/サイズなどを設定してください。
画面横幅は、表示する文字列の最大幅を考慮して設定することを推奨します。
後述ソースコードで「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 サンプルコードを記載します。

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: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>

layout-WPF.png

WpfAnimatedGif 用に xmlns:gif="http://wpfanimatedgif.codeplex.com" を追記しています。

x:Class="YourAppName.WorkingDialog" および xmlns:local="clr-namespace:YourAppName" に記述されている YourAppName は作成したプロジェクトの namespece に変更してください。

かなり雑なレイアウトで申し訳ありません。
Window の Height/Width、および、各コントールのレイアウトは適宜変更してください。

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

WorkingDialogManager.ShowDialog 第2引数で指定した処理では、クラス変数の参照/更新、コントロール参照が可能です。
コントロール更新は、WorkingDialog.SetStepMessage と同様な形態で、Invoke が必要となります。

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;

WorkingDialogManager を再利用するケースを考慮して、初期化のタイミングを忘れずに入れてください。
必要であれば、エラー情報初期化のメソッドを追加してください。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?