😸
BackgroundWorkerを使った進捗表示画面の実装
初めに
ファイルの読み込みや大量のループといった重い処理をする際は、進捗を表示してあげることがUI的にもよろしいです。
画面ごとにプログレスバーを実装して表示してもよいのですが、Backgroundworker
を使うことで「進捗を表示する画面」を処理と分離して実装できると分かったのでまとめました。
TL;DR
以下のように表示側、呼び出し側のコードを実装します。
VisualStudioの自動生成分は省略してます。
表示側
public partial class ProgressForm : Form
{
private BackgroundWorker backgroundWorker;
public ProgressForm(BackgroundWorker bw)
{
InitializeComponent();
this.backgroundWorker = bw;
if (!bw.WorkerSupportsCancellation)
{
//キャンセル
this.cancelBtn.Enabled = false;
this.cancelBtn.Visible = false;
this.ControlBox = false;
}
backgroundWorker.ProgressChanged += BackgroundWorker_ProgressChanged;
backgroundWorker.RunWorkerCompleted += BackgroundWorker_RunWorkerCompleted;
}
//完了時の挙動
private void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if((e.Error is null) == false)
{
//エラーの表示等の処理
Console.WriteLine("error");
DialogResult = DialogResult.Cancel;
}
else if (e.Cancelled)
{
//キャンセル時の処理
Console.WriteLine("canceled");
DialogResult = DialogResult.Cancel;
}
else
{
//完了時の処理
Console.WriteLine("completed");
DialogResult = DialogResult.OK;
}
//フォームを閉じる
this.Close();
}
//進捗の表示
private void BackgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
//プログレスバーの値を変更する
//設定されてる値の範囲外の値を渡すとエラーが発生することに注意
if (e.ProgressPercentage < this.progressBar1.Minimum)
{
this.progressBar1.Value = this.progressBar1.Minimum;
}
else if (this.progressBar1.Maximum < e.ProgressPercentage)
{
this.progressBar1.Value = this.progressBar1.Maximum;
}
else
{
this.progressBar1.Value = e.ProgressPercentage;
}
//UserStateの内容を表示する
progressLabel.Text = e.UserState.ToString();
}
private void ProgressForm_Shown(object sender, EventArgs e)
{
//画面表示を開始した際に動いてない場合はRunWorkerAsync()を呼ぶ
//二重に叩かないように判定を入れる
if (!backgroundWorker.IsBusy)
{
backgroundWorker.RunWorkerAsync();
}
}
private void cancelBtn_Click(object sender, EventArgs e)
{
//処理をキャンセルする
if (backgroundWorker.IsBusy)
{
this.backgroundWorker.CancelAsync();
}
}
private void ProgressForm_FormClosing(object sender, FormClosingEventArgs e)
{
//×ボタンで閉じられた際の対処
//backgroundWorkerが動いており
if (backgroundWorker.IsBusy && !backgroundWorker.CancellationPending)
{
//一度Closingをキャンセルしたうえで、改めてキャンセル処理をする。
e.Cancel = true;
backgroundWorker.CancelAsync();
}
}
private void ProgressForm_FormClosed(object sender, FormClosedEventArgs e)
{
//フォーム側でイベントハンドラーに加えた処理を外す
backgroundWorker.ProgressChanged -= BackgroundWorker_ProgressChanged;
backgroundWorker.RunWorkerCompleted -= BackgroundWorker_RunWorkerCompleted;
}
}
呼び出し側
public partial class Form1 : Form
{
BackgroundWorker bw;
public Form1()
{
InitializeComponent();
bw = new BackgroundWorker();
}
private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
for (int i = 0; i < 100; i++)
{
bw.ReportProgress(i, $"count is {i}");
if(bw.CancellationPending)
{
//処理がキャンセルされた場合
//※重要:競合を防ぐため、Cancel状態をDoWorkEventArgsにも伝える。
//この処理を省くと、 RunWorkerCompletedEventArgs.Canceledがfalseのまま伝わる。
e.Cancel = true;
return;
}
System.Threading.Thread.Sleep(100);
}
}
private void startBtn_Click(object sender, EventArgs e)
{
//キャンセルできる処理を呼び出す
bw.WorkerSupportsCancellation = true;
bw.WorkerReportsProgress = true;
bw.DoWork += this.backgroundWorker_DoWork;
using (var prgForm = new ProgressForm(bw))
{
//進捗表示フォームを呼び出す。
var res = prgForm.ShowDialog();
if(res == DialogResult.OK)
{
//正常終了時の処理
}
else{
//正常終了でない場合の処理
}
}
bw.DoWork -= this.backgroundWorker_DoWork;
}
private void NoCancelStartBtn_Click(object sender, EventArgs e)
{
//キャンセルできない処理を呼び出す。
bw.WorkerSupportsCancellation = false;
bw.WorkerReportsProgress = true;
bw.DoWork += this.backgroundWorker_DoWork;
using (var prgForm = new ProgressForm(bw))
{
//進捗表示フォームを呼び出す。
var res = prgForm.ShowDialog();
if (res == DialogResult.OK)
{
//正常終了時の処理
}
else
{
//正常終了でない場合の処理
}
}
bw.DoWork -= this.backgroundWorker_DoWork;
}
}
メソッドとイベントの関係
Backgroundworkerには以下の3つのMethodが実装されており、それぞれ叩かれた瞬間にイベントを発生させます。
-
RunWorkerDoAsync()
/RunWorkerDoAsync(object? argument)
- DoWorkイベントを発生させる
-
object? argument
で渡した値がDoWorkEventArgs.Argument
に格納されて引き渡される。
-
- DoWorkイベントを発生させる
-
ReportProgress(int percentProgress)
/ReportProgress(int percentProgress, object? userState)
- ProgressChangedイベントを発生させる
- 引数で渡した値が
ProgressChangedEventArgs
に格納されて引き渡される。-
int percentProgress
⇒ProgressChangedEventArgs.ProgressPercentage
-
object? userState
⇒ProgressChangedEventArgs.UserState
-
- 引数で渡した値が
- ProgressChangedイベントを発生させる
-
CancelAsync()
-
BackgroundWorker.CancellationPending
がtrueにされる。 - RunWorkerCompletedイベントを発生させる
-
これらのメソッドの挙動を元に、画面ごとにイベントハンドラへイベントを追加します。
進捗表示画面側の処理
進捗表示画面では以下の処理を行います。
- 進捗の表示
- ProgressChangedイベントハンドラーに表示の更新処理を追加する。
private void BackgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
//プログレスバーの値を変更する
//設定されてる値の範囲外の値を渡すとエラーが発生することに注意
if (e.ProgressPercentage < this.progressBar1.Minimum)
{
this.progressBar1.Value = this.progressBar1.Minimum;
}
else if (this.progressBar1.Maximum < e.ProgressPercentage)
{
this.progressBar1.Value = this.progressBar1.Maximum;
}
else
{
this.progressBar1.Value = e.ProgressPercentage;
}
//UserStateの内容を表示する
progressLabel.Text = e.UserState.ToString();
}
- キャンセルの受付
- キャンセルボタン/×ボタンを押した際、
CancelAsync()
を呼ぶ
- キャンセルボタン/×ボタンを押した際、
private void cancelBtn_Click(object sender, EventArgs e)
{
//処理をキャンセルする
if (backgroundWorker.IsBusy)
{
this.backgroundWorker.CancelAsync();
}
}
private void ProgressForm_FormClosing(object sender, FormClosingEventArgs e)
{
//×ボタンで閉じられた際の対処
//backgroundWorkerが動いており
if (backgroundWorker.IsBusy && !backgroundWorker.CancellationPending)
{
//一度Closingをキャンセルしたうえで、改めてキャンセル処理をする。
e.Cancel = true;
backgroundWorker.CancelAsync();
}
}
- 完了時の処理
- RunWorkerCompletedイベントハンドラに表示処理を足す
- エラーで終了する場合:エラー内容の表示等を行う
- キャンセルされた場合:DialogResultの値を変更する
- RunWorkerCompletedイベントハンドラに表示処理を足す
//完了時の挙動
private void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if((e.Error is null) == false)
{
//エラーの表示等の処理
Console.WriteLine("error");
DialogResult = DialogResult.Cancel;
}
else if (e.Cancelled)
{
//キャンセル時の処理
Console.WriteLine("canceled");
DialogResult = DialogResult.Cancel;
}
else
{
//完了時の処理
Console.WriteLine("completed");
DialogResult = DialogResult.OK;
}
//フォームを閉じる
this.Close();
}
以上を加えたフォームを呼び出すコードが舌のようになります(上記部分は省略)
public partial class ProgressForm : Form
{
private BackgroundWorker backgroundWorker;
public ProgressForm(BackgroundWorker bw)
{
InitializeComponent();
this.backgroundWorker = bw;
if (!bw.WorkerSupportsCancellation)
{
//キャンセル
this.cancelBtn.Enabled = false;
this.cancelBtn.Visible = false;
this.ControlBox = false;
}
backgroundWorker.ProgressChanged += BackgroundWorker_ProgressChanged;
backgroundWorker.RunWorkerCompleted += BackgroundWorker_RunWorkerCompleted;
}
private void ProgressForm_Shown(object sender, EventArgs e)
{
//画面表示を開始した際に動いてない場合はRunWorkerAsync()を呼ぶ
//二重に叩かないように判定を入れる
if (!backgroundWorker.IsBusy)
{
backgroundWorker.RunWorkerAsync();
}
}
private void ProgressForm_FormClosed(object sender, FormClosedEventArgs e)
{
//フォーム側でイベントハンドラーに加えた処理を外す
backgroundWorker.ProgressChanged -= BackgroundWorker_ProgressChanged;
backgroundWorker.RunWorkerCompleted -= BackgroundWorker_RunWorkerCompleted;
}
}
注意すべき点(筆者が作ってて躓いた点)は以下のようになります
- フォームが閉じた際に、
backgroundWorker
のイベントハンドラーに足した処理を外す- 渡された
backgroundWorker
を呼び出し側が再利用した際に副作用が起こる危険があるため
- 渡された
private void ProgressForm_FormClosed(object sender, FormClosedEventArgs e)
{
//フォーム側でイベントハンドラーに加えた処理を外す
backgroundWorker.ProgressChanged -= BackgroundWorker_ProgressChanged;
backgroundWorker.RunWorkerCompleted -= BackgroundWorker_RunWorkerCompleted;
}
- キャンセル時に
RunWorkerCompletedEventArgs.Result
を呼び出さない-
RunWorkerCompletedEventArgs.Cancelled
がtrueになった状態でこの値を呼ぶとInvalidOperationExceptionが発生する仕様になっているため
-
- キャンセルボタン以外に×ボタンを押して終了させる可能性を忘れない
- ×ボタンそのものを非表示にする/閉じる前にイベントを検知する、等の対策を組み込む
private void ProgressForm_FormClosing(object sender, FormClosingEventArgs e)
{
//×ボタンで閉じられた際の対処
//backgroundWorkerが動いており
if (backgroundWorker.IsBusy && !backgroundWorker.CancellationPending)
{
//一度Closingをキャンセルしたうえで、改めてキャンセル処理をする。
e.Cancel = true;
backgroundWorker.CancelAsync();
}
}
- プログレスバーに値を反映する際、プログレスバー側の最大値・最小値を超えた値を設定しないようにする。
- 範囲内に無い値を代入するとエラーが出るため。
呼び出し側の処理
呼び出し側では以下の処理を行います。
- 進捗の管理:DoWorkイベントハンドラーに以下の内容を含んだ処理を足す。
- 行いたい重たい処理
-
ReportProgress
メソッドによる進捗の報告 - キャンセル時の挙動
public partial class Form1 : Form
{
BackgroundWorker bw;
public Form1()
{
InitializeComponent();
bw = new BackgroundWorker();
}
private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
for (int i = 0; i < 100; i++)
{
bw.ReportProgress(i, $"count is {i}");
if(bw.CancellationPending)
{
//処理がキャンセルされた場合
e.Cancel = true;//※重要:競合を防ぐため、Cancel状態をDoWorkEventArgsにも伝える。
return;
}
System.Threading.Thread.Sleep(100);
}
}
private void startBtn_Click(object sender, EventArgs e)
{
//イベントハンドラーに処理を渡す。
bw.DoWork += this.backgroundWorker_DoWork;
using (var prgForm = new ProgressForm(bw))
{
//進捗表示フォームを呼び出す。
var res = prgForm.ShowDialog();
if(res == DialogResult.OK){
//正常終了時の処理
}
else{
//正常終了でない場合の処理
}
}
bw.DoWork -= this.backgroundWorker_DoWork;
}
注意すべき点(筆者が作ってて躓いた点)は以下のようになります。
-
BackgroundWorker.CancellationPending
の値とDoWorkEventArgs.Cancel
を必ず揃える。-
DoWorkEventArgs.Cancel
の値がRunWorkerCompletedEventArgs.Cancelled
の値になるため、この処理を挟まないと完了時のキャンセル処理が行えなくなる。
-
if(bw.CancellationPending)
{
//処理がキャンセルされた場合
e.Cancel = true;//※重要:競合を防ぐため、Cancel状態をDoWorkEventArgsにも伝える。
return;
}
参照
以下のサイトを参考にしました。
Discussion