C# BackgroundWorkerの使い方を初心者向けに解説|UIが固まらない非同期処理・進捗表示・キャンセル対応まで
はじめに
C#でWindows Formsアプリを作っていると、「ボタンを押した瞬間に画面が固まる」「ProgressBarが更新されない」「キャンセルボタンを押しても反応しない」といった問題に出会うことがあります。
この原因の多くは、時間のかかる処理をUIスレッドで直接実行していることです。UIスレッドは、ボタンのクリック、画面の再描画、ラベルの更新などを担当しています。そのため、同じスレッドで重い処理を実行すると、画面操作や再描画が止まったように見えてしまいます。
そこで使えるのがBackgroundWorkerです。BackgroundWorkerを使うと、時間のかかる処理をUIスレッドとは別のスレッドで実行し、処理中の進捗表示や完了後のUI更新、キャンセル処理を比較的わかりやすく実装できます。Microsoft Learnでも、BackgroundWorkerは時間のかかる処理を別スレッドで実行し、UIの応答性を保つためのクラスとして説明されています。
この記事では、C#のBackgroundWorkerの基本から、非同期処理、進捗表示、キャンセル、引数と戻り値、例外処理、よくあるエラー、実践的なサンプルコードまで初心者向けに解説します。
1. C#のBackgroundWorkerとは?UIが固まる原因と解決できること
1-1. BackgroundWorkerの役割を初心者向けにわかりやすく解説
BackgroundWorkerは、C#でバックグラウンド処理を実行するためのクラスです。主にWindows Formsアプリで使われ、時間のかかる処理を画面とは別のスレッドで動かすために利用されます。
たとえば、次のような処理は時間がかかることがあります。
大量データの読み込み
ファイルのコピー
画像変換
CSVやExcelファイルの解析
データベース処理
外部APIとの通信
重い計算処理
これらをボタンクリックイベントの中で直接実行すると、処理が終わるまで画面が反応しなくなる場合があります。BackgroundWorkerを使うと、重い処理をDoWorkイベントで実行し、画面更新はProgressChangedやRunWorkerCompletedで行う、という役割分担ができます。
1-2. UIスレッドで重い処理を実行すると画面が固まる理由
Windows Formsの画面は、基本的にUIスレッドと呼ばれる1つのスレッドで操作されます。このUIスレッドは、次のような処理を担当しています。
| UIスレッドの役割 | 具体例 |
|---|---|
| ユーザー操作の受付 | ボタンクリック、テキスト入力 |
| 画面の描画 | ラベル更新、ProgressBar更新 |
| イベント処理 | Click、Load、TextChangedなど |
| フォームの応答 | 移動、リサイズ、閉じる操作 |
このUIスレッド上でforループやThread.Sleep、ファイル処理などの重い処理を実行すると、その間は画面の再描画やクリックイベントを処理できません。その結果、ユーザーから見ると「アプリが固まった」「応答なしになった」と感じます。
悪い例は次のようなコードです。
C#private void buttonStart_Click(object sender, EventArgs e)
{
labelStatus.Text = "処理中...";
for (int i = 0; i < 10; i++)
{
Thread.Sleep(1000); // 重い処理の代わり
}
labelStatus.Text = "完了しました";
}
このコードでは、buttonStart_ClickがUIスレッド上で実行されます。Thread.Sleepで止まっている間、画面は更新されず、ボタンも反応しにくくなります。
1-3. BackgroundWorkerでできること:非同期処理・進捗表示・完了通知・キャンセル
BackgroundWorkerでは、主に次のことができます。
| 機能 | 内容 |
|---|---|
| 非同期処理 | 重い処理をUIスレッドとは別に実行する |
| 進捗表示 | ReportProgressで進捗率を通知する |
| UI更新 | ProgressChangedやRunWorkerCompletedで安全に画面を更新する |
| 完了通知 | 処理が終わったタイミングで結果を受け取る |
| キャンセル | CancelAsyncでキャンセル要求を出す |
| エラー処理 | RunWorkerCompletedで例外を確認する |
BackgroundWorkerでは、バックグラウンド処理を開始するためにRunWorkerAsyncを呼び出し、処理完了時にはRunWorkerCompletedイベントが発生します。公式ドキュメントでも、この流れが基本的な使い方として説明されています。
1-4. WinFormsアプリでBackgroundWorkerが使われる代表的な場面
BackgroundWorkerは、特にWindows Formsアプリで次のような場面に向いています。
| 場面 | 使用例 |
|---|---|
| ファイル処理 | CSV読み込み、ログ解析、ファイルコピー |
| データ取得 | DB検索、API通信、外部ファイル読み込み |
| 画像処理 | リサイズ、変換、サムネイル生成 |
| 一括処理 | 複数ファイルの変換、一括登録 |
| レガシーアプリ保守 | 既存WinFormsアプリの改善 |
| 進捗表示 | ProgressBarで処理状況を表示 |
| キャンセル処理 | 長時間処理を途中で止める |
初心者にとっては、DoWork、ProgressChanged、RunWorkerCompletedという3つのイベントに処理を分けられるため、非同期処理の流れを理解しやすい点がメリットです。
1-5. Task/async-awaitとの違いと、今でもBackgroundWorkerを使うケース
現在のC#では、非同期処理にはTaskやasync-awaitを使うことが一般的です。特に、Web API呼び出し、ファイル読み込み、データベースアクセスなどのI/O処理ではasync-awaitの方が自然に書けます。
一方、BackgroundWorkerはイベントベースで非同期処理を扱う古くからある仕組みです。新規開発ではTaskやasync-awaitを検討することが多いですが、次のようなケースでは今でもBackgroundWorkerが使われます。
| BackgroundWorkerを使うケース | 理由 |
|---|---|
| 既存のWinFormsアプリを保守している | すでに実装されていることが多い |
| デザイナーから追加したい | Visual Studioのコンポーネントとして扱いやすい |
| 進捗・完了・キャンセルをイベントで分けたい | 初心者にも構造が見えやすい |
| 簡単なバックグラウンド処理を追加したい | 小規模な処理なら実装しやすい |
| .NET Framework時代のアプリを扱う | レガシー環境で使いやすい |
Microsoft Learnでは、BackgroundWorkerは.NET 10.0向けAPIとしても掲載されており、Windows Formsのバックグラウンド処理の説明にも使われています。ただし、現代的なC#の非同期処理ではasync-awaitも重要な選択肢です。
2. BackgroundWorkerを使う前に押さえる基本構成
2-1. BackgroundWorkerの主要プロパティ一覧
BackgroundWorkerを使う前に、まず主要なプロパティを押さえておきましょう。
| プロパティ | 型 | 内容 |
|---|---|---|
WorkerReportsProgress | bool | 進捗報告を使うかどうか |
WorkerSupportsCancellation | bool | キャンセルを使うかどうか |
CancellationPending | bool | キャンセル要求が出ているか |
IsBusy | bool | 現在実行中かどうか |
特に重要なのは、WorkerReportsProgressとWorkerSupportsCancellationです。進捗表示を使うならWorkerReportsProgress = true、キャンセルを使うならWorkerSupportsCancellation = trueを設定します。
C#backgroundWorker1.WorkerReportsProgress = true;
backgroundWorker1.WorkerSupportsCancellation = true;
WorkerReportsProgressがfalseのままReportProgressを呼ぶと例外が発生します。公式ドキュメントでも、ReportProgressを使うにはWorkerReportsProgressをtrueにする必要があると説明されています。
2-2. DoWork・ProgressChanged・RunWorkerCompletedの役割
BackgroundWorkerで特に重要なのは、次の3つのイベントです。
| イベント | 実行されるタイミング | 主な役割 |
|---|---|---|
DoWork | バックグラウンド処理開始時 | 重い処理を書く |
ProgressChanged | 進捗通知時 | ProgressBarやLabelを更新する |
RunWorkerCompleted | 処理完了時 | 完了後のUI更新、結果表示、エラー処理 |
役割を簡単に分けると、次のようになります。
DoWork
→ 重い処理を書く場所
ProgressChanged
→ 処理中の進捗を画面に表示する場所
RunWorkerCompleted
→ 処理完了後に画面を元に戻す場所
重要なのは、DoWorkではUIコントロールを直接操作しないことです。DoWorkはUIスレッドとは別のスレッドで実行されるため、labelStatus.Text = "処理中";のような画面操作を直接書くとエラーの原因になります。
2-3. RunWorkerAsyncでバックグラウンド処理を開始する流れ
BackgroundWorkerの処理は、RunWorkerAsyncを呼び出すことで開始します。
C#backgroundWorker1.RunWorkerAsync();
基本的な流れは次のとおりです。
1. ボタンをクリックする
2. RunWorkerAsyncを呼ぶ
3. DoWorkイベントが実行される
4. 必要に応じてReportProgressで進捗を通知する
5. ProgressChangedで画面を更新する
6. 処理が終わる
7. RunWorkerCompletedで完了後の画面を更新する
引数を渡したい場合は、RunWorkerAsyncに値を指定します。
C#backgroundWorker1.RunWorkerAsync(100);
渡した引数は、DoWorkイベント内でe.Argumentから取得できます。
C#private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
int count = (int)e.Argument;
}
2-4. WorkerReportsProgressとWorkerSupportsCancellationの設定
進捗表示とキャンセル処理を使う場合は、初期設定で次のように書きます。
C#backgroundWorker1.WorkerReportsProgress = true;
backgroundWorker1.WorkerSupportsCancellation = true;
それぞれの意味は次のとおりです。
| 設定 | trueにするとできること |
|---|---|
WorkerReportsProgress | ReportProgressで進捗を通知できる |
WorkerSupportsCancellation | CancelAsyncでキャンセル要求を出せる |
設定し忘れると、進捗表示が呼ばれなかったり、キャンセルできなかったりします。
2-5. UIを直接更新してはいけない理由
Windows Formsのコントロールは、基本的に作成されたUIスレッドから操作する必要があります。DoWorkは別スレッドで実行されるため、次のようなコードは避けます。
C#private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
// NG:DoWork内でUIを直接更新している
labelStatus.Text = "処理中...";
}
正しくは、ReportProgressでUIスレッド側に通知し、ProgressChangedで画面を更新します。
C#private void backgroundWorker1_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker worker = (BackgroundWorker)sender;
worker.ReportProgress(50, "半分完了しました");
}
private void backgroundWorker1_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
labelStatus.Text = e.UserState?.ToString();
}
ReportProgressによって発生するProgressChangedイベントは、BackgroundWorkerを作成した側のスレッドで実行されるため、UI更新に使いやすい仕組みになっています。
3. C# BackgroundWorkerの基本的な使い方
3-1. BackgroundWorkerをフォームに追加する方法
Visual StudioのWindows Formsデザイナーを使っている場合、BackgroundWorkerはツールボックスからフォームに追加できます。
手順は次のとおりです。
Windows Formsのフォームを開く
ツールボックスを表示する
「コンポーネント」から
BackgroundWorkerを探すフォームにドラッグ&ドロップする
画面下部のコンポーネントトレイに
backgroundWorker1が表示されるプロパティから
WorkerReportsProgressやWorkerSupportsCancellationを設定するイベント一覧から
DoWorkなどを追加する
デザイナーで追加すると、自動的にbackgroundWorker1という名前のコンポーネントが作成されます。
3-2. コードでBackgroundWorkerを生成する方法
デザイナーを使わず、コードだけで作成することもできます。
C#using System.ComponentModel;
private BackgroundWorker worker = new BackgroundWorker();
public Form1()
{
InitializeComponent();
worker.WorkerReportsProgress = true;
worker.WorkerSupportsCancellation = true;
worker.DoWork += Worker_DoWork;
worker.ProgressChanged += Worker_ProgressChanged;
worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
}
イベントハンドラーは次のように用意します。
C#private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
}
private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
}
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
}
3-3. DoWorkイベントに重い処理を書く
DoWorkには、時間のかかる処理を書きます。
C#private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
for (int i = 0; i < 10; i++)
{
Thread.Sleep(500); // 重い処理の代わり
}
}
実際のアプリでは、Thread.Sleepの部分がファイル処理、データベース処理、計算処理などになります。
3-4. ボタンクリックでRunWorkerAsyncを呼び出す
開始ボタンを押したときに、RunWorkerAsyncを呼び出します。
C#private void buttonStart_Click(object sender, EventArgs e)
{
worker.RunWorkerAsync();
}
ただし、すでに実行中の状態で再度RunWorkerAsyncを呼ぶとエラーになります。そのため、通常はIsBusyを確認します。
C#private void buttonStart_Click(object sender, EventArgs e)
{
if (!worker.IsBusy)
{
worker.RunWorkerAsync();
}
}
3-5. RunWorkerCompletedで処理完了後のUIを更新する
処理が終わった後の画面更新は、RunWorkerCompletedに書きます。
C#private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
labelStatus.Text = "完了しました";
buttonStart.Enabled = true;
}
RunWorkerCompletedでは、成功、キャンセル、エラーを分けて処理できます。
C#private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
}
else if (e.Error != null)
{
labelStatus.Text = "エラーが発生しました";
}
else
{
labelStatus.Text = "完了しました";
}
buttonStart.Enabled = true;
}
3-6. 最小構成のサンプルコード
最小限のBackgroundWorkerサンプルは次のとおりです。
C#using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;
public partial class Form1 : Form
{
private BackgroundWorker worker = new BackgroundWorker();
public Form1()
{
InitializeComponent();
worker.DoWork += Worker_DoWork;
worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
}
private void buttonStart_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
return;
}
labelStatus.Text = "処理中...";
buttonStart.Enabled = false;
worker.RunWorkerAsync();
}
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
// 重い処理の代わり
Thread.Sleep(3000);
}
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
buttonStart.Enabled = true;
labelStatus.Text = "完了しました";
}
}
このコードでは、ボタンを押すと3秒間の処理がバックグラウンドで実行されます。その間もUIスレッドは止まらないため、画面が固まりにくくなります。
4. UIが固まらない非同期処理の実装例
4-1. 悪い例:ボタンクリック内で重い処理を直接実行するコード
まず、UIが固まる悪い例を見てみましょう。
C#private void buttonStart_Click(object sender, EventArgs e)
{
labelStatus.Text = "処理中...";
progressBar1.Value = 0;
for (int i = 1; i <= 10; i++)
{
Thread.Sleep(500);
progressBar1.Value = i * 10;
}
labelStatus.Text = "完了しました";
}
このコードは一見問題なさそうに見えます。しかし、buttonStart_Clickの中で重い処理を直接実行しているため、画面が固まる原因になります。
特に次のような問題が起こります。
| 問題 | 内容 |
|---|---|
| 画面が固まる | 処理中にフォームを移動できない |
| ProgressBarが更新されない | 処理後に一気に100%になる |
| キャンセルできない | ボタン操作を受け付けにくい |
| 応答なしになる | Windowsから停止したように見える |
4-2. 良い例:BackgroundWorkerで重い処理を別スレッドに分離するコード
BackgroundWorkerを使うと、重い処理をDoWorkに移せます。
C#private BackgroundWorker worker = new BackgroundWorker();
public Form1()
{
InitializeComponent();
worker.DoWork += Worker_DoWork;
worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
}
private void buttonStart_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
return;
}
labelStatus.Text = "処理中...";
buttonStart.Enabled = false;
worker.RunWorkerAsync();
}
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
for (int i = 1; i <= 10; i++)
{
Thread.Sleep(500);
}
}
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
labelStatus.Text = "完了しました";
buttonStart.Enabled = true;
}
このようにすると、重い処理はバックグラウンドで動き、UIスレッドは画面操作を受け付けられます。
4-3. 処理中にボタンを無効化して二重実行を防ぐ
バックグラウンド処理中に開始ボタンを何度も押されると、二重実行の原因になります。そのため、処理開始時にボタンを無効化します。
C#private void buttonStart_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
MessageBox.Show("すでに処理中です。");
return;
}
buttonStart.Enabled = false;
labelStatus.Text = "処理中...";
worker.RunWorkerAsync();
}
処理完了後にボタンを有効化します。
C#private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
buttonStart.Enabled = true;
labelStatus.Text = "完了しました";
}
4-4. IsBusyで実行中かどうかを判定する
IsBusyは、BackgroundWorkerが実行中かどうかを示します。
C#if (worker.IsBusy)
{
return;
}
開始処理では、基本的にIsBusyを確認してからRunWorkerAsyncを呼び出します。
C#private void buttonStart_Click(object sender, EventArgs e)
{
if (!worker.IsBusy)
{
worker.RunWorkerAsync();
}
}
これにより、実行中のBackgroundWorkerに対して再度RunWorkerAsyncを呼んでしまうミスを防げます。
4-5. 処理完了後にボタンやラベルを元に戻す
処理中に変更したUIは、完了時に必ず元に戻します。
C#private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
buttonStart.Enabled = true;
buttonCancel.Enabled = false;
if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
}
else if (e.Error != null)
{
labelStatus.Text = "エラーが発生しました";
}
else
{
labelStatus.Text = "完了しました";
}
}
完了時にUIを復旧する処理をまとめておくと、通常完了、キャンセル、エラーのいずれの場合でも画面が中途半端な状態になりにくくなります。
5. ProgressBarで進捗表示を行う方法
5-1. ReportProgressで進捗率を通知する
進捗表示を行うには、まずWorkerReportsProgressをtrueにします。
C#worker.WorkerReportsProgress = true;
次に、DoWork内でReportProgressを呼び出します。
C#private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker bw = (BackgroundWorker)sender;
for (int i = 1; i <= 100; i++)
{
Thread.Sleep(50);
bw.ReportProgress(i);
}
}
ReportProgressを呼ぶと、ProgressChangedイベントが発生します。進捗率はe.ProgressPercentageで受け取れます。
5-2. ProgressChangedでProgressBarとLabelを更新する
ProgressBarやLabelの更新はProgressChangedで行います。
C#private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
labelStatus.Text = $"{e.ProgressPercentage}% 完了";
}
ProgressChangedはUI更新用のイベントとして使えるため、ProgressBarやLabelの更新を書く場所として適しています。
5-3. 0〜100%の進捗率を計算する考え方
ReportProgressに渡す値は、通常0〜100の整数で表します。
たとえば、100件のデータを処理する場合は次のように計算します。
C#int percent = (i + 1) * 100 / totalCount;
worker.ReportProgress(percent);
全体件数がtotalCount、現在処理した件数がi + 1なら、進捗率は次の式で求められます。
進捗率 = 処理済み件数 × 100 / 全体件数
コード例は次のとおりです。
C#int totalCount = 50;
for (int i = 0; i < totalCount; i++)
{
Thread.Sleep(100);
int percent = (i + 1) * 100 / totalCount;
worker.ReportProgress(percent);
}
5-4. UserStateを使ってメッセージや処理状況を渡す
ReportProgressには、進捗率だけでなく任意のオブジェクトも渡せます。
C#worker.ReportProgress(percent, "ファイルを読み込み中...");
この第2引数は、ProgressChanged側でe.UserStateとして受け取れます。
C#private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
labelStatus.Text = e.UserState?.ToString();
}
UserStateを使うと、次のような情報をUIに渡せます。
| 渡す情報 | 例 |
|---|---|
| メッセージ | "読み込み中..." |
| ファイル名 | "data.csv" |
| 現在件数 | "10 / 100 件" |
| 独自クラス | 処理状況DTO |
ReportProgress(Int32, Object)では、進捗率に加えて任意の状態情報を渡せます。公式ドキュメントでも、userStateはProgressChangedEventArgs.UserStateとして返される値として説明されています。
5-5. 進捗表示付きのサンプルコード
進捗表示付きの基本サンプルです。
C#using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;
public partial class Form1 : Form
{
private BackgroundWorker worker = new BackgroundWorker();
public Form1()
{
InitializeComponent();
worker.WorkerReportsProgress = true;
worker.DoWork += Worker_DoWork;
worker.ProgressChanged += Worker_ProgressChanged;
worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
}
private void buttonStart_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
return;
}
progressBar1.Value = 0;
labelStatus.Text = "開始しました";
buttonStart.Enabled = false;
worker.RunWorkerAsync();
}
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker bw = (BackgroundWorker)sender;
int total = 20;
for (int i = 0; i < total; i++)
{
Thread.Sleep(200);
int percent = (i + 1) * 100 / total;
bw.ReportProgress(percent, $"{i + 1} / {total} 件を処理しました");
}
}
private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
labelStatus.Text = e.UserState?.ToString();
}
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
buttonStart.Enabled = true;
labelStatus.Text = "完了しました";
}
}
このコードでは、処理件数に応じてProgressBarとLabelが更新されます。
5-6. 進捗が不明な処理でマーキースタイルを使う方法
処理の全体件数がわからない場合、0〜100%の進捗率を計算できません。たとえば、外部APIの応答待ちや、終了時間が予測できない処理では進捗率が不明なことがあります。
その場合は、ProgressBarのStyleをMarqueeにします。
C#progressBar1.Style = ProgressBarStyle.Marquee;
progressBar1.MarqueeAnimationSpeed = 30;
処理完了後は通常表示に戻します。
C#progressBar1.Style = ProgressBarStyle.Blocks;
progressBar1.MarqueeAnimationSpeed = 0;
progressBar1.Value = 0;
使用例です。
C#private void buttonStart_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
return;
}
progressBar1.Style = ProgressBarStyle.Marquee;
progressBar1.MarqueeAnimationSpeed = 30;
labelStatus.Text = "処理中...";
worker.RunWorkerAsync();
}
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
progressBar1.Style = ProgressBarStyle.Blocks;
progressBar1.MarqueeAnimationSpeed = 0;
progressBar1.Value = 0;
labelStatus.Text = "完了しました";
}
6. キャンセル処理を実装する方法
6-1. CancelAsyncでキャンセル要求を出す
BackgroundWorkerでキャンセル処理を使うには、まずWorkerSupportsCancellationをtrueにします。
C#worker.WorkerSupportsCancellation = true;
キャンセルボタンではCancelAsyncを呼び出します。
C#private void buttonCancel_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
worker.CancelAsync();
labelStatus.Text = "キャンセル要求中...";
}
}
ここで注意したいのは、CancelAsyncを呼んだからといって即座に処理が強制停止されるわけではないことです。CancelAsyncは「キャンセルしてほしい」という要求を出すだけです。
6-2. CancellationPendingでキャンセル要求を確認する
実際に処理を止めるには、DoWork内でCancellationPendingを定期的に確認します。
C#private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker bw = (BackgroundWorker)sender;
for (int i = 0; i < 100; i++)
{
if (bw.CancellationPending)
{
e.Cancel = true;
return;
}
Thread.Sleep(100);
}
}
長いループ処理では、毎回または一定間隔でCancellationPendingを確認するのが基本です。
6-3. e.Cancelをtrueにしてキャンセル完了を通知する
キャンセル要求を検知したら、e.Cancel = true;を設定してDoWorkを抜けます。
C#if (bw.CancellationPending)
{
e.Cancel = true;
return;
}
これにより、RunWorkerCompletedでe.Cancelledがtrueになります。
C#private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
}
}
6-4. RunWorkerCompletedで通常完了・キャンセル・エラーを分岐する
RunWorkerCompletedでは、次の順番で確認するのが安全です。
C#private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
}
else if (e.Error != null)
{
labelStatus.Text = "エラーが発生しました";
MessageBox.Show(e.Error.Message);
}
else
{
labelStatus.Text = "正常に完了しました";
}
buttonStart.Enabled = true;
buttonCancel.Enabled = false;
}
Resultを使う場合は、ErrorやCancelledを確認してからアクセスする必要があります。公式ドキュメントでも、Errorがある場合やキャンセル済みの場合にResultへアクセスすると例外につながるため、先にErrorとCancelledを確認するよう説明されています。
6-5. キャンセルボタン付きのサンプルコード
C#using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;
public partial class Form1 : Form
{
private BackgroundWorker worker = new BackgroundWorker();
public Form1()
{
InitializeComponent();
worker.WorkerReportsProgress = true;
worker.WorkerSupportsCancellation = true;
worker.DoWork += Worker_DoWork;
worker.ProgressChanged += Worker_ProgressChanged;
worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
buttonCancel.Enabled = false;
}
private void buttonStart_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
return;
}
progressBar1.Value = 0;
labelStatus.Text = "処理中...";
buttonStart.Enabled = false;
buttonCancel.Enabled = true;
worker.RunWorkerAsync();
}
private void buttonCancel_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
worker.CancelAsync();
labelStatus.Text = "キャンセル要求中...";
buttonCancel.Enabled = false;
}
}
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker bw = (BackgroundWorker)sender;
int total = 50;
for (int i = 0; i < total; i++)
{
if (bw.CancellationPending)
{
e.Cancel = true;
return;
}
Thread.Sleep(100);
int percent = (i + 1) * 100 / total;
bw.ReportProgress(percent, $"{i + 1} / {total} 件を処理中");
}
}
private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
labelStatus.Text = e.UserState?.ToString();
}
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
buttonStart.Enabled = true;
buttonCancel.Enabled = false;
if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
}
else if (e.Error != null)
{
labelStatus.Text = "エラーが発生しました";
MessageBox.Show(e.Error.Message);
}
else
{
progressBar1.Value = 100;
labelStatus.Text = "完了しました";
}
}
}
6-6. キャンセルできない実装になってしまう原因
キャンセルボタンを押しても止まらない場合、よくある原因は次のとおりです。
| 原因 | 解決策 |
|---|---|
WorkerSupportsCancellationがfalse | trueに設定する |
CancelAsyncを呼んでいない | キャンセルボタンで呼び出す |
CancellationPendingを確認していない | DoWork内で定期的に確認する |
e.Cancel = trueを設定していない | キャンセル検知時に設定する |
| 1回の処理が長すぎる | 処理を分割し、途中で確認できるようにする |
| 外部処理がブロックしている | タイムアウトや別のキャンセル機構を検討する |
BackgroundWorkerのキャンセルは強制終了ではありません。処理側がキャンセル要求を見に行き、自分で終了する必要があります。
7. BackgroundWorkerで引数や戻り値を扱う方法
7-1. RunWorkerAsyncの引数をDoWorkで受け取る
BackgroundWorkerでは、RunWorkerAsyncに引数を渡せます。
C#worker.RunWorkerAsync(10);
この値は、DoWorkイベントで受け取ります。
C#private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
int count = (int)e.Argument;
}
7-2. DoWorkEventArgs.Argumentの使い方
e.Argumentはobject型なので、受け取るときはキャストが必要です。
C#private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
string filePath = (string)e.Argument;
// filePathを使って処理する
}
複数の値を渡す場合は、クラスやタプルを使うと便利です。
7-3. e.Resultで処理結果を返す
DoWork内で処理結果を返したい場合は、e.Resultに値を設定します。
C#private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
int count = (int)e.Argument;
int result = count * 2;
e.Result = result;
}
7-4. RunWorkerCompletedEventArgs.Resultで結果を取得する
e.Resultに設定した値は、RunWorkerCompletedで取得できます。
C#private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
{
MessageBox.Show(e.Error.Message);
return;
}
if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
return;
}
int result = (int)e.Result;
labelStatus.Text = $"結果:{result}";
}
ここでも、Resultへアクセスする前にErrorとCancelledを確認することが重要です。
7-5. 複数の値を渡す場合のクラス・タプル・DTOの使い分け
複数の値を渡す方法はいくつかあります。
| 方法 | 例 | 向いているケース |
|---|---|---|
| クラス | SearchCondition | 意味のあるデータをまとめる |
| タプル | (string path, int count) | 小規模な一時データ |
| DTO | ImportRequestDto | 実務アプリで入力条件を明確にする |
初心者には、まずクラスを作る方法がおすすめです。値の意味がわかりやすく、後から項目を追加しやすいためです。
C#public class SearchRequest
{
public string Keyword { get; set; }
public int MaxCount { get; set; }
}
戻り値もクラスにできます。
C#public class SearchResult
{
public int Count { get; set; }
public string Message { get; set; }
}
7-6. 引数と戻り値を使った実用サンプルコード
C#public class ProcessRequest
{
public int Count { get; set; }
public string Name { get; set; }
}
public class ProcessResult
{
public int Total { get; set; }
public string Message { get; set; }
}
C#private void buttonStart_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
return;
}
var request = new ProcessRequest
{
Count = 10,
Name = "サンプル処理"
};
worker.RunWorkerAsync(request);
}
C#private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
var request = (ProcessRequest)e.Argument;
int total = 0;
for (int i = 1; i <= request.Count; i++)
{
Thread.Sleep(100);
total += i;
}
e.Result = new ProcessResult
{
Total = total,
Message = $"{request.Name}が完了しました"
};
}
C#private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
{
MessageBox.Show(e.Error.Message);
return;
}
if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
return;
}
var result = (ProcessResult)e.Result;
labelStatus.Text = $"{result.Message} 合計:{result.Total}";
}
8. 例外処理とエラー表示の実装
8-1. DoWork内で例外が発生した場合の動き
DoWork内で例外が発生した場合、その例外はRunWorkerCompletedのe.Errorで確認できます。
C#private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
throw new InvalidOperationException("サンプルエラーです");
}
この例外は、RunWorkerCompletedで次のように受け取ります。
C#private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
{
MessageBox.Show(e.Error.Message);
}
}
8-2. RunWorkerCompletedのErrorプロパティを確認する
エラー処理では、まずe.Errorを確認します。
C#if (e.Error != null)
{
MessageBox.Show($"エラーが発生しました:{e.Error.Message}");
return;
}
その後にe.Cancelled、最後に正常完了を処理すると安全です。
C#private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
{
MessageBox.Show($"エラー:{e.Error.Message}");
}
else if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
}
else
{
labelStatus.Text = "正常に完了しました";
}
}
8-3. MessageBoxでエラー内容を表示する
初心者向けの実装では、まずMessageBox.Showでエラー内容を表示するとわかりやすいです。
C#private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
{
labelStatus.Text = "エラーが発生しました";
MessageBox.Show(
e.Error.Message,
"エラー",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
return;
}
labelStatus.Text = "完了しました";
}
実務では、ログファイルにエラー内容を出力する処理も追加するとよいでしょう。
8-4. finally相当の後片付けをどこに書くべきか
BackgroundWorkerでは、後片付けの内容によって書く場所を分けます。
| 後片付けの内容 | 書く場所 |
|---|---|
| ファイルやDB接続の解放 | DoWork内のfinallyまたはusing |
| ボタンを有効化する | RunWorkerCompleted |
| ProgressBarを戻す | RunWorkerCompleted |
| 一時ファイル削除 | 内容によってDoWorkまたはRunWorkerCompleted |
UIに関係する後片付けはRunWorkerCompletedに書きます。
C#private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
buttonStart.Enabled = true;
buttonCancel.Enabled = false;
progressBar1.Style = ProgressBarStyle.Blocks;
}
ファイルやリソースの解放はDoWork内で行います。
C#private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
using (var stream = File.OpenRead("sample.txt"))
{
// ファイル処理
}
}
8-5. エラー時にUIを安全に復旧するコード例
エラーが発生してもボタンが無効のままにならないように、UI復旧処理は最後にまとめます。
C#private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
buttonStart.Enabled = true;
buttonCancel.Enabled = false;
progressBar1.Style = ProgressBarStyle.Blocks;
if (e.Error != null)
{
labelStatus.Text = "エラーが発生しました";
MessageBox.Show(
e.Error.Message,
"エラー",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
return;
}
if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
return;
}
progressBar1.Value = 100;
labelStatus.Text = "完了しました";
}
このように、ボタンやProgressBarを元に戻す処理を最初に書くと、エラー時でも画面が復旧しやすくなります。
9. BackgroundWorkerでよくあるエラーと解決策
9-1. 「別スレッドから呼び出されたため、コントロールにアクセスできません」の原因
このエラーは、DoWork内でUIコントロールを直接操作したときによく発生します。
C#private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
// NG
labelStatus.Text = "処理中...";
}
解決策は、ReportProgressを使ってProgressChangedでUIを更新することです。
C#private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker bw = (BackgroundWorker)sender;
bw.ReportProgress(0, "処理中...");
}
private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
labelStatus.Text = e.UserState?.ToString();
}
9-2. ProgressChangedが呼ばれない原因
ProgressChangedが呼ばれない場合は、次を確認します。
| 確認項目 | 内容 |
|---|---|
WorkerReportsProgress | trueになっているか |
| イベント登録 | ProgressChanged += ...しているか |
ReportProgress | DoWork内で呼んでいるか |
| 例外発生 | DoWork内で途中停止していないか |
よくあるミスは、WorkerReportsProgressの設定忘れです。
C#worker.WorkerReportsProgress = true;
9-3. キャンセルボタンを押しても止まらない原因
キャンセルできない場合は、次を確認します。
C#worker.WorkerSupportsCancellation = true;
C#worker.CancelAsync();
C#if (worker.CancellationPending)
{
e.Cancel = true;
return;
}
CancelAsyncだけでは処理は止まりません。DoWork内でCancellationPendingを確認し、e.Cancel = trueを設定して処理を抜ける必要があります。
9-4. RunWorkerAsyncを連続実行してエラーになる原因
BackgroundWorkerは、実行中に再度RunWorkerAsyncを呼ぶとエラーになります。対策としてIsBusyを確認します。
C#private void buttonStart_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
MessageBox.Show("すでに処理中です。");
return;
}
worker.RunWorkerAsync();
}
また、処理中は開始ボタンを無効化しておくと、二重実行を防ぎやすくなります。
C#buttonStart.Enabled = false;
9-5. 処理完了後にResultを取得すると例外になる原因
RunWorkerCompletedでe.Resultにアクセスする前に、e.Errorとe.Cancelledを確認していない場合、例外が発生することがあります。
悪い例です。
C#private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
// NG
var result = e.Result;
}
良い例です。
C#private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (e.Error != null)
{
MessageBox.Show(e.Error.Message);
return;
}
if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
return;
}
var result = e.Result;
}
RunWorkerCompletedEventArgs.Resultは、エラーやキャンセルの状態によって例外を発生させる可能性があるため、先に状態確認を行うのが基本です。
9-6. デバッグ時に確認すべきチェックリスト
BackgroundWorkerがうまく動かないときは、次のチェックリストを確認してください。
| チェック項目 | 確認内容 |
|---|---|
DoWorkが呼ばれているか | ブレークポイントで確認 |
RunWorkerAsyncを呼んでいるか | ボタンクリック処理を確認 |
IsBusyで弾かれていないか | 条件分岐を確認 |
WorkerReportsProgressがtrueか | 進捗表示に必要 |
WorkerSupportsCancellationがtrueか | キャンセルに必要 |
UIをDoWorkで直接触っていないか | クロススレッドエラーの原因 |
ReportProgressを呼んでいるか | ProgressChanged発火に必要 |
e.Cancel = trueを設定しているか | キャンセル完了判定に必要 |
e.Resultの前にErrorとCancelledを確認しているか | 例外防止 |
| イベント登録が重複していないか | 二重実行や重複表示の原因 |
10. 実践サンプル:時間のかかる処理を進捗表示・キャンセル対応で実装する
10-1. 作成するサンプルアプリの概要
ここでは、次の機能を持つWindows Formsアプリを作成します。
| 機能 | 内容 |
|---|---|
| 開始ボタン | バックグラウンド処理を開始する |
| キャンセルボタン | 処理中にキャンセル要求を出す |
| ProgressBar | 進捗率を表示する |
| Label | 現在の処理状況を表示する |
| 完了処理 | 正常完了、キャンセル、エラーを分岐する |
処理内容は、100件のデータを順番に処理する想定にします。
10-2. 画面に配置するコントロール一覧
フォームには次のコントロールを配置します。
| コントロール | Name | 用途 |
|---|---|---|
| Button | buttonStart | 処理開始 |
| Button | buttonCancel | キャンセル |
| ProgressBar | progressBar1 | 進捗表示 |
| Label | labelStatus | 状態表示 |
初期状態では、キャンセルボタンを無効にしておきます。
C#buttonCancel.Enabled = false;
10-3. BackgroundWorkerの初期設定コード
C#private BackgroundWorker worker = new BackgroundWorker();
public Form1()
{
InitializeComponent();
worker.WorkerReportsProgress = true;
worker.WorkerSupportsCancellation = true;
worker.DoWork += Worker_DoWork;
worker.ProgressChanged += Worker_ProgressChanged;
worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
buttonCancel.Enabled = false;
progressBar1.Minimum = 0;
progressBar1.Maximum = 100;
progressBar1.Value = 0;
labelStatus.Text = "待機中";
}
10-4. 開始ボタンの処理
C#private void buttonStart_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
MessageBox.Show("すでに処理中です。");
return;
}
progressBar1.Value = 0;
labelStatus.Text = "処理を開始しました";
buttonStart.Enabled = false;
buttonCancel.Enabled = true;
worker.RunWorkerAsync(100);
}
ここでは、RunWorkerAsync(100)で処理件数を渡しています。
10-5. DoWork内の重い処理
C#private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker bw = (BackgroundWorker)sender;
int totalCount = (int)e.Argument;
int processedCount = 0;
for (int i = 0; i < totalCount; i++)
{
if (bw.CancellationPending)
{
e.Cancel = true;
return;
}
// 重い処理の代わり
Thread.Sleep(50);
processedCount++;
int percent = processedCount * 100 / totalCount;
string message = $"{processedCount} / {totalCount} 件を処理しました";
bw.ReportProgress(percent, message);
}
e.Result = processedCount;
}
10-6. 進捗表示の更新処理
C#private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
labelStatus.Text = e.UserState?.ToString();
}
ProgressChangedではUIコントロールを安全に更新できます。
10-7. キャンセルボタンの処理
C#private void buttonCancel_Click(object sender, EventArgs e)
{
if (!worker.IsBusy)
{
return;
}
worker.CancelAsync();
buttonCancel.Enabled = false;
labelStatus.Text = "キャンセル要求中...";
}
CancelAsyncはキャンセル要求を出すだけなので、実際の停止はDoWork内のCancellationPendingチェックで行います。
10-8. 完了・キャンセル・エラー時の表示切り替え
C#private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
buttonStart.Enabled = true;
buttonCancel.Enabled = false;
if (e.Error != null)
{
labelStatus.Text = "エラーが発生しました";
MessageBox.Show(
e.Error.Message,
"エラー",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
return;
}
if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
return;
}
int processedCount = (int)e.Result;
progressBar1.Value = 100;
labelStatus.Text = $"{processedCount} 件の処理が完了しました";
}
10-9. サンプルコード全文
C#using System;
using System.ComponentModel;
using System.Threading;
using System.Windows.Forms;
public partial class Form1 : Form
{
private BackgroundWorker worker = new BackgroundWorker();
public Form1()
{
InitializeComponent();
worker.WorkerReportsProgress = true;
worker.WorkerSupportsCancellation = true;
worker.DoWork += Worker_DoWork;
worker.ProgressChanged += Worker_ProgressChanged;
worker.RunWorkerCompleted += Worker_RunWorkerCompleted;
buttonCancel.Enabled = false;
progressBar1.Minimum = 0;
progressBar1.Maximum = 100;
progressBar1.Value = 0;
labelStatus.Text = "待機中";
}
private void buttonStart_Click(object sender, EventArgs e)
{
if (worker.IsBusy)
{
MessageBox.Show("すでに処理中です。");
return;
}
progressBar1.Value = 0;
labelStatus.Text = "処理を開始しました";
buttonStart.Enabled = false;
buttonCancel.Enabled = true;
// 100件のデータを処理する想定
worker.RunWorkerAsync(100);
}
private void buttonCancel_Click(object sender, EventArgs e)
{
if (!worker.IsBusy)
{
return;
}
worker.CancelAsync();
buttonCancel.Enabled = false;
labelStatus.Text = "キャンセル要求中...";
}
private void Worker_DoWork(object sender, DoWorkEventArgs e)
{
BackgroundWorker bw = (BackgroundWorker)sender;
int totalCount = (int)e.Argument;
int processedCount = 0;
for (int i = 0; i < totalCount; i++)
{
if (bw.CancellationPending)
{
e.Cancel = true;
return;
}
// 実際のアプリでは、ここにファイル処理や計算処理を書く
Thread.Sleep(50);
processedCount++;
int percent = processedCount * 100 / totalCount;
string message = $"{processedCount} / {totalCount} 件を処理しました";
bw.ReportProgress(percent, message);
}
e.Result = processedCount;
}
private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
labelStatus.Text = e.UserState?.ToString();
}
private void Worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
buttonStart.Enabled = true;
buttonCancel.Enabled = false;
if (e.Error != null)
{
labelStatus.Text = "エラーが発生しました";
MessageBox.Show(
e.Error.Message,
"エラー",
MessageBoxButtons.OK,
MessageBoxIcon.Error);
return;
}
if (e.Cancelled)
{
labelStatus.Text = "キャンセルされました";
return;
}
int processedCount = (int)e.Result;
progressBar1.Value = 100;
labelStatus.Text = $"{processedCount} 件の処理が完了しました";
}
}
このサンプルでは、BackgroundWorkerの基本である非同期処理、進捗表示、キャンセル、完了後のUI更新、エラー処理をまとめて実装しています。
11. BackgroundWorkerを使うときの注意点とベストプラクティス
11-1. DoWork内でUIコントロールを直接操作しない
最も重要な注意点は、DoWork内でUIコントロールを直接操作しないことです。
C#// NG
labelStatus.Text = "処理中";
UIを更新したい場合は、ReportProgressを使います。
C#bw.ReportProgress(percent, "処理中");
そして、ProgressChangedでUIを更新します。
C#labelStatus.Text = e.UserState?.ToString();
11-2. 長時間ループでは定期的にCancellationPendingを確認する
キャンセル対応を入れる場合は、長時間ループの中でCancellationPendingを定期的に確認します。
C#if (bw.CancellationPending)
{
e.Cancel = true;
return;
}
確認頻度が低すぎると、キャンセルボタンを押してもなかなか止まりません。
11-3. 重い処理とUI更新の責務を分ける
BackgroundWorkerを使うときは、次のように責務を分けると読みやすくなります。
| 場所 | 書く処理 |
|---|---|
DoWork | 重い処理、計算、ファイル処理 |
ProgressChanged | ProgressBar、Label更新 |
RunWorkerCompleted | 完了後の画面復旧、結果表示、エラー表示 |
この分け方を守ると、コードの見通しがよくなります。
11-4. ProgressChangedの呼び出し頻度を上げすぎない
ReportProgressを呼びすぎると、UI更新が多くなりすぎて逆に重くなることがあります。
たとえば、100万件のループで毎回ReportProgressを呼ぶのは避けた方がよいです。
C#// あまりよくない例
for (int i = 0; i < 1000000; i++)
{
bw.ReportProgress(i);
}
一定間隔で更新するようにします。
C#for (int i = 0; i < total; i++)
{
if (i % 100 == 0)
{
int percent = i * 100 / total;
bw.ReportProgress(percent);
}
}
11-5. 複雑な非同期処理ではTaskやasync-awaitも検討する
BackgroundWorkerはわかりやすい一方で、複雑な非同期処理には向かない場合があります。
次のようなケースでは、Taskやasync-awaitも検討しましょう。
| ケース | 検討したい方法 |
|---|---|
| Web APIを呼び出す | async-await |
| ファイルを非同期に読み書きする | async-await |
| 複数処理を並列実行する | Task |
| キャンセルトークンを使いたい | CancellationToken |
| 新規の.NETアプリを作る | async-await |
C#の非同期プログラミングでは、Taskを中心としたasync-await構文が広く使われています。Microsoft Learnでも、async-awaitは非同期処理を順序立ったコードのように書ける仕組みとして説明されています。
11-6. 新規開発でBackgroundWorkerを選ぶべきかの判断基準
新規開発でBackgroundWorkerを使うかどうかは、次の基準で判断するとよいです。
| 判断基準 | おすすめ |
|---|---|
| 既存WinFormsアプリの改修 | BackgroundWorkerでもよい |
| デザイナー中心で作りたい | BackgroundWorkerでもよい |
| 初心者が進捗・キャンセルを学びたい | BackgroundWorkerは理解しやすい |
| 新規のモダンなC#アプリ | Task / async-awaitを検討 |
| 複雑な非同期制御が必要 | Task / async-awaitを検討 |
| WPFやMAUIなども視野に入れる | async-awaitを検討 |
結論として、BackgroundWorkerは古い技術ではありますが、WinFormsアプリでは今でも理解しておく価値があります。特に既存アプリの保守や、UIが固まる問題の改善では役立ちます。
12. C# BackgroundWorkerに関するよくある質問
12-1. BackgroundWorkerは非推奨ですか?
BackgroundWorkerは古くからある仕組みですが、公式APIとして現在の.NET向けドキュメントにも掲載されています。つまり、すぐに使えない技術というわけではありません。
ただし、新規開発ではTaskやasync-awaitを使う設計が一般的です。特に非同期I/Oや複雑なキャンセル制御を扱う場合は、async-awaitの方が適していることが多いです。
12-2. WPFでもBackgroundWorkerは使えますか?
WPFでもBackgroundWorkerは使用できます。System.ComponentModel.BackgroundWorkerはWindows Forms専用というわけではありません。
ただし、WPFではDispatcherやasync-awaitを使ったUI更新もよく使われます。新しいWPFアプリでは、Taskとasync-awaitを使った実装も検討するとよいでしょう。
12-3. 複数のBackgroundWorkerを同時に実行できますか?
複数のBackgroundWorkerインスタンスを作成すれば、同時に実行することは可能です。
C#BackgroundWorker worker1 = new BackgroundWorker();
BackgroundWorker worker2 = new BackgroundWorker();
ただし、同時実行数が増えるとCPUやメモリ、ファイルアクセス、DB接続などに負荷がかかります。複数処理を制御する必要がある場合は、TaskやSemaphoreSlimなども検討しましょう。
12-4. Thread.Sleepを使っても問題ありませんか?
サンプルコードでは、重い処理の代わりにThread.Sleepを使うことがあります。
C#Thread.Sleep(1000);
ただし、実際のアプリでは単に待つためだけにThread.Sleepを多用するのはおすすめしません。この記事のサンプルでは、ファイル処理や計算処理の代わりとして動きをわかりやすくするために使っています。
UIスレッド上でThread.Sleepを使うと画面が固まる原因になりますが、DoWork内で重い処理の模擬として使う分には学習用サンプルとして理解しやすいです。
12-5. async-awaitとBackgroundWorkerはどちらを使うべきですか?
新規開発では、基本的にasync-awaitを優先して検討するとよいです。
一方で、次のような場合はBackgroundWorkerも選択肢になります。
| 状況 | 選択肢 |
|---|---|
| 既存WinFormsアプリの保守 | BackgroundWorker |
| すでにBackgroundWorkerが使われている | 既存方針に合わせる |
| 簡単な進捗表示を追加したい | BackgroundWorker |
| 新規開発で非同期I/Oが中心 | async-await |
| 複雑なキャンセルや並列処理 | Task / async-await |
初心者が「UIスレッドを止めない」「重い処理を別に分ける」という考え方を学ぶには、BackgroundWorkerはよい教材になります。
12-6. ProgressBarが更新されないときは何を確認すべきですか?
ProgressBarが更新されない場合は、次を確認してください。
| 確認項目 | 内容 |
|---|---|
WorkerReportsProgress | trueになっているか |
ReportProgress | DoWork内で呼んでいるか |
ProgressChanged | イベント登録されているか |
progressBar1.Maximum | 通常は100になっているか |
Valueの範囲 | 0〜100の範囲内か |
| UIスレッドで更新しているか | ProgressChangedで更新しているか |
| 処理が速すぎないか | 一瞬で終わって見えないだけではないか |
基本的な実装は次の形です。
C#worker.WorkerReportsProgress = true;
C#bw.ReportProgress(percent);
C#private void Worker_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
progressBar1.Value = e.ProgressPercentage;
}
この3点がそろっていれば、ProgressBarは更新されるはずです。
まとめ
BackgroundWorkerは、C#のWindows FormsアプリでUIを固めずに重い処理を実行するための便利な仕組みです。特に、初心者が非同期処理、進捗表示、キャンセル、完了通知を理解するうえで扱いやすい構造になっています。
重要なポイントを整理すると、次のとおりです。
| ポイント | 内容 |
|---|---|
重い処理はDoWorkに書く | UIスレッドを止めないため |
UI更新はProgressChangedやRunWorkerCompletedで行う | クロススレッドエラーを防ぐため |
開始はRunWorkerAsync | バックグラウンド処理を開始する |
進捗はReportProgress | ProgressBarやLabelに反映する |
キャンセルはCancelAsyncとCancellationPending | 強制停止ではなく協調的に止める |
結果はe.Result | ErrorとCancelled確認後に取得する |
二重実行はIsBusyで防ぐ | 実行中の再開始を避ける |
新規開発ではasync-awaitも検討 | 現代的な非同期処理に向いている |
BackgroundWorkerを正しく使うコツは、「重い処理」と「UI更新」を分けることです。DoWorkでは処理に集中し、画面更新はProgressChangedやRunWorkerCompletedに任せることで、UIが固まらない使いやすいアプリを作れます。
既存のWinFormsアプリを改善したい場合や、進捗表示・キャンセル付きの処理を手軽に実装したい場合は、この記事のサンプルコードをベースにBackgroundWorkerを試してみてください。

