C#で処理中を表示する方法|画面を固めないasync/awaitとプログレスバー実装ガイド

はじめに

C#で時間のかかる処理を実行するとき、何も表示しないまま画面が止まって見えると、ユーザーは「フリーズしたのではないか」「もう一度ボタンを押したほうがよいのではないか」と不安になります。

特にWinFormsやWPFなどのデスクトップアプリでは、ボタンクリック後にデータ取得、ファイル読み込み、帳票作成、画像処理、API通信などを行う場面が多くあります。このとき重要なのが、処理中であることを画面に表示しつつ、UIを固めないことです。

C#で処理中を表示する方法には、ラベルで「処理中です」と表示する簡単な方法から、プログレスバー、待機カーソル、オーバーレイ、キャンセルボタン、async/awaitIProgress<T>を使った実装まで、さまざまなパターンがあります。

この記事では、C#で「処理中」を表示する基本から、画面を固めない非同期処理、WinForms・WPFでの具体的な実装、よくある失敗の解決方法までを解説します。

1. C#で「処理中」を表示する前に知っておくべき基本

1-1. 「処理中」を表示したい場面とは

C#アプリで処理中表示が必要になるのは、ユーザーの操作後に完了まで時間がかかる処理を行う場面です。

代表的な例は次のようなケースです。

  • データベースから大量データを取得する

  • Web APIへリクエストを送信する

  • CSVやExcelファイルを読み込む

  • PDFや帳票を生成する

  • 画像や動画を変換する

  • フォルダ内のファイルを一括処理する

  • 検索処理に時間がかかる

  • 外部サービスとの通信を待つ

このような処理で画面に何も表示しないと、ユーザーは処理が進んでいるのか判断できません。

そのため、C#で処理中を表示する場合は、単にラベルを出すだけでなく、処理が開始されたこと、現在進行中であること、完了または失敗したことをわかりやすく伝える必要があります。

1-2. 検索ユーザーが困っている主な症状

「C# 処理中」と検索する人が困っている症状には、次のようなものがあります。

  • ボタンを押した後に画面が固まる

  • 「処理中」ラベルを表示したいのに表示されない

  • プログレスバーが動かない

  • awaitを使っているのにUIがフリーズする

  • 別スレッドからLabelやProgressBarを更新して例外が出る

  • 処理中にボタンを連打されて二重実行される

  • 処理が終わったあとにボタンやカーソルが元に戻らない

  • 進捗率を画面に表示したいが実装方法がわからない

これらの多くは、UIスレッドの使い方非同期処理の理解不足が原因です。

1-3. 画面が固まる原因はUIスレッドのブロック

WinFormsやWPFなどの画面アプリには、UIを描画したり、ボタンクリックなどのイベントを処理したりするためのメインスレッドがあります。これを一般的にUIスレッドと呼びます。

UIスレッドで時間のかかる処理をそのまま実行すると、画面の再描画やユーザー操作の受付ができなくなります。

例えば、次のようなコードは画面を固める原因になります。

C#
private void button1_Click(object sender, EventArgs e)
{
labelStatus.Text = "処理中です...";

// 重い処理をUIスレッドで実行している
Thread.Sleep(5000);

labelStatus.Text = "完了しました";
}

このコードでは、labelStatus.Textに「処理中です...」を設定していますが、その直後にThread.SleepでUIスレッドを止めています。そのため、ラベルの表示が画面に反映される前に画面が固まることがあります。

C#で処理中表示を正しく行うには、時間のかかる処理をUIスレッドから切り離し、UIの更新だけをUIスレッドで行うことが大切です。

1-4. 処理中表示で実現したい3つのこと

C#で処理中を表示する実装では、主に次の3つを実現します。

1つ目は、ユーザーに処理中であることを伝えることです。ラベル、プログレスバー、待機カーソル、オーバーレイなどを使って、処理が進行中であることを明示します。

2つ目は、画面を固めないことです。async/awaitTask.Runを適切に使い、UIスレッドをブロックしないようにします。

3つ目は、二重実行や不正操作を防ぐことです。処理中はボタンを無効化したり、画面全体を操作不可にしたりして、同じ処理が何度も実行されないようにします。

この3つを意識すると、実務でも使いやすい処理中表示を実装できます。

2. C#で画面を固めずに処理中を表示する考え方

2-1. 同期処理と非同期処理の違い

同期処理とは、処理が終わるまで次の処理に進まない実行方法です。

C#
DoHeavyWork();
labelStatus.Text = "完了しました";

この場合、DoHeavyWorkが完了するまで次の行には進みません。もしDoHeavyWorkがUIスレッド上で長時間動作すると、その間は画面が応答しなくなります。

一方、非同期処理では、時間のかかる処理の完了を待っている間もUIスレッドを解放できます。

C#
await DoHeavyWorkAsync();
labelStatus.Text = "完了しました";

awaitを使うと、処理の完了を待ちながらもUIスレッドをブロックしにくくなります。そのため、画面の再描画やユーザー操作を継続できます。

2-2. async/awaitを使う理由

C#で処理中を表示する場合、async/awaitを使う理由は、コードを読みやすく保ちながら非同期処理を書けるからです。

従来は、バックグラウンド処理、完了イベント、UIスレッドへの戻し処理を分けて書く必要がありました。しかしasync/awaitを使うと、処理の流れを上から下へ自然に書けます。

C#
private async void button1_Click(object sender, EventArgs e)
{
labelStatus.Text = "処理中です...";

await Task.Delay(3000);

labelStatus.Text = "完了しました";
}

このコードでは、Task.Delayの待機中にUIスレッドをブロックしません。そのため、画面は固まらず、ラベルの表示も反映されます。

ただし、async/awaitを使っていても、重い処理をUIスレッドで実行してしまうと画面は固まります。重要なのは、何を非同期にしているかです。

2-3. Task.Runを使うべき処理と使わない処理

Task.Runは、CPUを使う重い処理をバックグラウンドスレッドで実行したい場合に使います。

例えば、次のような処理ではTask.Runが有効です。

  • 大量データの計算

  • 画像変換

  • ファイルの一括解析

  • CSVの大量加工

  • 暗号化や圧縮処理

  • 時間のかかる同期メソッドしかない処理

例:

C#
await Task.Run(() =>
{
// CPU負荷の高い処理
DoHeavyCalculation();
});

一方、Web API通信やファイル読み込みなど、もともと非同期メソッドが用意されている処理では、基本的にTask.Runで包む必要はありません。

C#
// よい例:非同期APIをそのままawaitする
var result = await httpClient.GetStringAsync(url);

次のように、非同期メソッドを無理にTask.Runで包む必要はありません。

C#
// あまり意味がない例
var result = await Task.Run(() => httpClient.GetStringAsync(url));

C#で処理中表示を実装するときは、CPU負荷の高い処理にはTask.Run、I/O待ちの処理には非同期APIのawaitを使う、という考え方が基本です。

2-4. UI更新はUIスレッドで行う必要がある

WinFormsやWPFのコントロールは、基本的に作成されたUIスレッドから更新する必要があります。

バックグラウンドスレッドから直接LabelやProgressBarを更新すると、WinFormsではクロススレッド操作の例外が発生することがあります。

悪い例:

C#
await Task.Run(() =>
{
// バックグラウンドスレッドからUIを直接更新している
labelStatus.Text = "処理中です";
});

このようなコードは避けるべきです。

awaitの後にUIスレッドへ戻る通常のケースでは、次のように安全にUIを更新できます。

C#
labelStatus.Text = "処理中です";

await Task.Run(() =>
{
DoHeavyWork();
});

labelStatus.Text = "完了しました";

進捗をバックグラウンド処理から通知したい場合は、IProgress<T>Progress<T>を使うと安全です。

3. まずは簡単に「処理中です」を表示する方法

3-1. ボタンクリック時にラベルを表示する基本例

最も簡単な処理中表示は、ボタンクリック時にLabelへメッセージを表示する方法です。

WinFormsの場合は次のように書けます。

C#
private async void buttonStart_Click(object sender, EventArgs e)
{
labelStatus.Text = "処理中です...";
labelStatus.Visible = true;

await Task.Delay(3000);

labelStatus.Text = "完了しました";
}

Task.Delayは待機時間を表すサンプルです。実際のアプリでは、ここにデータ取得やファイル処理などを入れます。

ポイントは、Thread.Sleepではなくawait Task.Delayを使っていることです。Thread.Sleepはスレッドを止めますが、Task.Delayは非同期に待機できるため、UIを固めにくくなります。

3-2. 処理開始前にボタンを無効化する

処理中に同じボタンを何度も押されると、同じ処理が重複して実行される可能性があります。

そのため、処理開始時にボタンを無効化し、完了後に戻すのが基本です。

C#
private async void buttonStart_Click(object sender, EventArgs e)
{
buttonStart.Enabled = false;
labelStatus.Text = "処理中です...";
labelStatus.Visible = true;

await Task.Delay(3000);

labelStatus.Text = "完了しました";
buttonStart.Enabled = true;
}

これで、処理中にボタンを連打されることを防げます。

3-3. 処理完了後に表示を戻す

処理が終わったら、ラベルやボタンの状態を元に戻します。

C#
private async void buttonStart_Click(object sender, EventArgs e)
{
buttonStart.Enabled = false;
labelStatus.Text = "処理中です...";
labelStatus.Visible = true;

await Task.Delay(3000);

labelStatus.Visible = false;
buttonStart.Enabled = true;

MessageBox.Show("処理が完了しました。");
}

処理中表示を残すか非表示にするかは、アプリの設計によります。

完了状態をユーザーに伝えたい場合は「完了しました」と表示し、すぐに通常状態へ戻したい場合は非表示にするとよいでしょう。

3-4. try/finallyで処理中表示を確実に解除する

実務では、処理中に例外が発生することがあります。

その場合、最後まで処理が進まず、ボタンが無効化されたままになることがあります。これを防ぐには、try/finallyを使います。

C#
private async void buttonStart_Click(object sender, EventArgs e)
{
try
{
buttonStart.Enabled = false;
labelStatus.Text = "処理中です...";
labelStatus.Visible = true;

await Task.Delay(3000);

labelStatus.Text = "完了しました";
}
finally
{
buttonStart.Enabled = true;
labelStatus.Visible = false;
}
}

finallyは、処理が成功しても失敗しても最後に実行されます。

C#で処理中表示を実装する場合、ボタンの有効化、カーソルの復元、プログレスバーの非表示などはfinallyで行うと安全です。

4. async/awaitで処理中表示を実装する基本コード

4-1. asyncイベントハンドラの書き方

WinFormsやWPFのボタンクリックイベントでは、次のようにasync voidを使えます。

C#
private async void buttonStart_Click(object sender, EventArgs e)
{
await ExecuteAsync();
}

通常、async voidは避けるべきですが、イベントハンドラでは例外的に使われます。

イベントハンドラの中に処理をすべて書くと長くなりやすいため、実際の処理はTaskを返すメソッドに分けると管理しやすくなります。

C#
private async void buttonStart_Click(object sender, EventArgs e)
{
await ExecuteAsync();
}

private async Task ExecuteAsync()
{
await Task.Delay(3000);
}

この形にしておくと、テストや再利用もしやすくなります。

4-2. awaitで重い処理の完了を待つ

CPU負荷の高い処理を行う場合は、Task.Runでバックグラウンドに逃がし、その完了をawaitで待ちます。

C#
private async void buttonStart_Click(object sender, EventArgs e)
{
labelStatus.Text = "処理中です...";

await Task.Run(() =>
{
DoHeavyWork();
});

labelStatus.Text = "完了しました";
}

private void DoHeavyWork()
{
// 重い処理の例
for (int i = 0; i < 100000000; i++)
{
var value = Math.Sqrt(i);
}
}

このようにすると、重い処理はバックグラウンドスレッドで実行され、UIスレッドは画面描画やユーザー操作に使える状態を保ちやすくなります。

4-3. 処理中メッセージを表示・非表示にする実装例

実務で使いやすい基本形は次のようになります。

C#
private async void buttonStart_Click(object sender, EventArgs e)
{
try
{
buttonStart.Enabled = false;
labelStatus.Visible = true;
labelStatus.Text = "処理中です...";

await Task.Run(() =>
{
DoHeavyWork();
});

MessageBox.Show("処理が完了しました。");
}
catch (Exception ex)
{
MessageBox.Show($"エラーが発生しました。{ex.Message}");
}
finally
{
labelStatus.Visible = false;
buttonStart.Enabled = true;
}
}

このコードでは、処理中だけラベルを表示し、処理が終わったら非表示にしています。

また、例外が発生してもfinallyで必ずボタンを有効化するため、画面が操作不能のまま残ることを防げます。

4-4. 例外発生時にも画面を元に戻す書き方

例外が起きたときに画面状態を戻すには、try/catch/finallyを組み合わせます。

C#
private async void buttonStart_Click(object sender, EventArgs e)
{
try
{
SetBusy(true);

await Task.Run(() =>
{
DoHeavyWork();
});

labelStatus.Text = "完了しました";
}
catch (Exception ex)
{
labelStatus.Text = "失敗しました";
MessageBox.Show(ex.Message, "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
finally
{
SetBusy(false);
}
}

private void SetBusy(bool isBusy)
{
buttonStart.Enabled = !isBusy;
labelStatus.Visible = isBusy;
labelStatus.Text = isBusy ? "処理中です..." : "";
UseWaitCursor = isBusy;
}

SetBusyのような共通メソッドを用意すると、複数の処理で同じ処理中表示を再利用できます。

5. プログレスバーで処理の進捗を表示する方法

5-1. ProgressBarを使うメリット

処理中表示にはラベルだけでも十分な場合がありますが、処理に時間がかかる場合はプログレスバーを使うとユーザーに安心感を与えられます。

ProgressBarを使うメリットは次のとおりです。

  • 処理が進んでいることを視覚的に伝えられる

  • 進捗率がわかる場合は完了までの目安を示せる

  • 進捗率が不明な場合でも動きのある表示にできる

  • 処理中であることが直感的に伝わる

C#で処理中を表示する場合、進捗率がわかる処理では通常のプログレスバー、進捗率がわからない処理ではMarquee表示を使うとよいでしょう。

5-2. Marqueeで進捗率不明の処理中表示をする

処理の進捗率が計算できない場合は、WinFormsのProgressBarStyle.Marqueeを使います。

C#
private async void buttonStart_Click(object sender, EventArgs e)
{
try
{
buttonStart.Enabled = false;

progressBar1.Style = ProgressBarStyle.Marquee;
progressBar1.Visible = true;
labelStatus.Text = "処理中です...";

await Task.Run(() =>
{
DoHeavyWork();
});

labelStatus.Text = "完了しました";
}
finally
{
progressBar1.Visible = false;
progressBar1.Style = ProgressBarStyle.Blocks;
buttonStart.Enabled = true;
}
}

Marqueeは、進捗率を表示するのではなく、バーが動き続けることで「処理中」であることを示します。

API通信や外部サービス待ちなど、完了までの割合を計算しにくい処理に向いています。

5-3. Valueを更新して進捗率を表示する

処理全体の件数がわかっている場合は、ProgressBarのValueを更新して進捗率を表示できます。

C#
private async void buttonStart_Click(object sender, EventArgs e)
{
progressBar1.Minimum = 0;
progressBar1.Maximum = 100;
progressBar1.Value = 0;
progressBar1.Visible = true;

for (int i = 1; i <= 100; i++)
{
await Task.Delay(50);
progressBar1.Value = i;
}

progressBar1.Visible = false;
MessageBox.Show("完了しました。");
}

この例では簡単のためUIスレッド上でawait Task.Delayを使っています。

実際に重い処理をバックグラウンドで実行しながら進捗を更新する場合は、後述するIProgress<T>を使うのがおすすめです。

5-4. パーセント表示をラベルに反映する

ProgressBarだけでなく、Labelにパーセントを表示すると、ユーザーにより明確に進捗を伝えられます。

C#
private async void buttonStart_Click(object sender, EventArgs e)
{
progressBar1.Minimum = 0;
progressBar1.Maximum = 100;
progressBar1.Value = 0;
progressBar1.Visible = true;

for (int i = 1; i <= 100; i++)
{
await Task.Delay(50);

progressBar1.Value = i;
labelStatus.Text = $"処理中です... {i}%";
}

labelStatus.Text = "完了しました";
progressBar1.Visible = false;
}

「何パーセント完了したか」を表示することで、長い処理でもユーザーが待ちやすくなります。

6. IProgress<T>で安全にプログレスバーを更新する

6-1. バックグラウンド処理から直接UIを触ると危険な理由

バックグラウンド処理の中から直接ProgressBarやLabelを更新すると、クロススレッド例外が発生することがあります。

悪い例:

C#
await Task.Run(() =>
{
for (int i = 0; i <= 100; i++)
{
// バックグラウンドスレッドからUIを直接更新しているため危険
progressBar1.Value = i;
}
});

WinFormsやWPFのUI部品は、UIスレッドから操作する必要があります。

この問題を避けるために便利なのが、IProgress<T>Progress<T>です。

6-2. IProgress<T>とProgress<T>の基本

IProgress<T>は、処理の進捗を通知するためのインターフェースです。

Progress<T>をUIスレッド上で作成すると、Reportされた内容をUIスレッド側で受け取りやすくなります。

基本形は次のとおりです。

C#
var progress = new Progress<int>(value =>
{
progressBar1.Value = value;
labelStatus.Text = $"処理中です... {value}%";
});

バックグラウンド処理側では、UI部品を直接触らず、Reportで進捗だけを通知します。

C#
progress.Report(50);

このようにすると、処理本体とUI更新を分離できます。

6-3. Reportで進捗を通知する実装例

WinFormsでIProgress<int>を使ってプログレスバーを更新する例です。

C#
private async void buttonStart_Click(object sender, EventArgs e)
{
try
{
buttonStart.Enabled = false;

progressBar1.Minimum = 0;
progressBar1.Maximum = 100;
progressBar1.Value = 0;
progressBar1.Visible = true;

labelStatus.Visible = true;
labelStatus.Text = "処理中です... 0%";

var progress = new Progress<int>(value =>
{
progressBar1.Value = value;
labelStatus.Text = $"処理中です... {value}%";
});

await Task.Run(() =>
{
DoWorkWithProgress(progress);
});

labelStatus.Text = "完了しました";
}
catch (Exception ex)
{
labelStatus.Text = "失敗しました";
MessageBox.Show(ex.Message);
}
finally
{
buttonStart.Enabled = true;
progressBar1.Visible = false;
}
}

private void DoWorkWithProgress(IProgress<int> progress)
{
for (int i = 1; i <= 100; i++)
{
// 重い処理の代わり
Thread.Sleep(50);

progress.Report(i);
}
}

このコードでは、DoWorkWithProgressの中でUI部品を一切触っていません。

バックグラウンド処理は進捗値だけを通知し、UI更新はProgress<int>側で行います。これにより、クロススレッド例外を避けやすくなります。

6-4. InvokeやDispatcherとの使い分け

WinFormsではInvoke、WPFではDispatcherを使ってUIスレッドに処理を戻すこともできます。

WinFormsの例:

C#
this.Invoke(() =>
{
labelStatus.Text = "処理中です...";
});

WPFの例:

C#
Dispatcher.Invoke(() =>
{
StatusText.Text = "処理中です...";
});

ただし、進捗通知のように何度もUIを更新する処理では、IProgress<T>を使うほうがコードが整理しやすいです。

使い分けの目安は次のとおりです。

  • 単発でUIを更新したい場合:InvokeDispatcher

  • 進捗を継続的に通知したい場合:IProgress<T>

  • await後にUIを更新するだけの場合:そのままUIを更新

  • MVVMで状態管理したい場合:プロパティバインディング

C#で処理中表示を安全に実装するなら、進捗表示にはIProgress<T>を使う方法を覚えておくと便利です。

7. WinFormsで処理中を表示する実装パターン

7-1. LabelとProgressBarを組み合わせる

WinFormsでは、LabelとProgressBarを組み合わせると簡単に処理中表示を作れます。

C#
private async void buttonStart_Click(object sender, EventArgs e)
{
try
{
SetBusy(true);

await Task.Run(() =>
{
DoHeavyWork();
});

MessageBox.Show("完了しました。");
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "エラー");
}
finally
{
SetBusy(false);
}
}

private void SetBusy(bool isBusy)
{
labelStatus.Visible = isBusy;
labelStatus.Text = isBusy ? "処理中です..." : "";

progressBar1.Visible = isBusy;
progressBar1.Style = ProgressBarStyle.Marquee;

buttonStart.Enabled = !isBusy;
}

このようにSetBusyを作っておくと、処理中表示の切り替えをまとめて管理できます。

7-2. ボタン連打を防ぐためのEnabled制御

処理中はボタンを無効化しましょう。

C#
buttonStart.Enabled = false;

処理が終わったら有効化します。

C#
buttonStart.Enabled = true;

複数のボタンをまとめて制御したい場合は、PanelやGroupBoxごと無効化する方法もあります。

C#
panelButtons.Enabled = false;

ただし、画面全体を無効化するとキャンセルボタンまで押せなくなることがあります。

キャンセルボタンを用意する場合は、無効化する範囲を分ける必要があります。

C#
buttonStart.Enabled = false;
buttonCancel.Enabled = true;

7-3. UseWaitCursorで待機カーソルを表示する

WinFormsでは、UseWaitCursorを使うと待機カーソルを表示できます。

C#
private async void buttonStart_Click(object sender, EventArgs e)
{
try
{
UseWaitCursor = true;
buttonStart.Enabled = false;
labelStatus.Text = "処理中です...";

await Task.Run(() =>
{
DoHeavyWork();
});
}
finally
{
UseWaitCursor = false;
buttonStart.Enabled = true;
labelStatus.Text = "";
}
}

フォーム全体に待機カーソルを出したい場合は、フォームのUseWaitCursorを変更します。

アプリ全体に反映したい場合は、Application.UseWaitCursorを使う方法もあります。

C#
Application.UseWaitCursor = true;

ただし、戻し忘れると待機カーソルが残ってしまうため、必ずfinallyで戻しましょう。

7-4. モーダルな処理中ダイアログを表示する場合の注意点

処理中に別フォームを表示して「処理中です」と出す方法もあります。

ただし、モーダルダイアログを使う場合は注意が必要です。

C#
using var dialog = new ProcessingForm();
dialog.Show();

ShowDialogを使うと、そのフォームが閉じるまで呼び出し元の処理が止まります。処理中表示として使う場合、実装を間違えると逆に画面が固まったように見えます。

処理中ダイアログを使う場合は、次の点に注意します。

  • 重い処理はUIスレッドで実行しない

  • ダイアログの表示後にバックグラウンド処理を開始する

  • 処理完了後に必ずダイアログを閉じる

  • 例外発生時にも閉じる

  • キャンセル操作を可能にするか検討する

単純な処理中表示であれば、まずはフォーム内のLabel、ProgressBar、待機カーソルで対応するほうが実装しやすいです。

8. WPFで処理中を表示する実装パターン

8-1. TextBlockとProgressBarで処理中を表示する

WPFでは、TextBlockとProgressBarを使って処理中表示を作れます。

XAMLの例です。

XML
<StackPanel>
<Button x:Name="StartButton"
Content="開始"
Click="StartButton_Click" />

<TextBlock x:Name="StatusText"
Text="処理中です..."
Visibility="Collapsed"
Margin="0,10,0,0" />

<ProgressBar x:Name="ProgressBar"
Height="20"
Minimum="0"
Maximum="100"
Visibility="Collapsed"
IsIndeterminate="True" />
</StackPanel>

コードビハインドでは次のように制御します。

C#
private async void StartButton_Click(object sender, RoutedEventArgs e)
{
try
{
StartButton.IsEnabled = false;
StatusText.Visibility = Visibility.Visible;
ProgressBar.Visibility = Visibility.Visible;

await Task.Run(() =>
{
DoHeavyWork();
});

StatusText.Text = "完了しました";
}
catch (Exception ex)
{
StatusText.Text = "失敗しました";
MessageBox.Show(ex.Message);
}
finally
{
StartButton.IsEnabled = true;
ProgressBar.Visibility = Visibility.Collapsed;
}
}

WPFでは、進捗率が不明な場合はProgressBarIsIndeterminateTrueにします。

8-2. IsEnabledで画面操作を一時的に止める

WPFでは、IsEnabledを使って画面操作を一時的に止められます。

C#
MainPanel.IsEnabled = false;

処理完了後に戻します。

C#
MainPanel.IsEnabled = true;

ただし、画面全体を無効化すると、キャンセルボタンも押せなくなることがあります。

キャンセルボタンを使いたい場合は、入力エリアだけを無効化し、キャンセルボタンは有効なままにします。

C#
InputArea.IsEnabled = false;
CancelButton.IsEnabled = true;

処理中にユーザー操作を完全に止めるのか、一部操作だけ許可するのかは、処理内容に応じて決めましょう。

8-3. オーバーレイ表示で処理中状態をわかりやすくする

WPFでは、Gridを重ねてオーバーレイ表示を作る方法がよく使われます。

XML
<Grid>
<Grid x:Name="MainContent">
<!-- 通常画面 -->
</Grid>

<Grid x:Name="BusyOverlay"
Visibility="Collapsed"
Background="#80000000">
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Text="処理中です..."
Foreground="White"
FontSize="18"
HorizontalAlignment="Center" />
<ProgressBar Width="200"
Height="20"
IsIndeterminate="True"
Margin="0,10,0,0" />
</StackPanel>
</Grid>
</Grid>

コード側で表示を切り替えます。

C#
private async void StartButton_Click(object sender, RoutedEventArgs e)
{
try
{
BusyOverlay.Visibility = Visibility.Visible;

await Task.Run(() =>
{
DoHeavyWork();
});
}
finally
{
BusyOverlay.Visibility = Visibility.Collapsed;
}
}

オーバーレイを使うと、ユーザーに「現在操作できない状態である」と伝えやすくなります。

8-4. MVVMでIsBusyを使って表示を切り替える

WPFでMVVMを使っている場合は、IsBusyプロパティを用意して処理中表示を切り替える方法が一般的です。

ViewModelの例です。

C#
private bool _isBusy;

public bool IsBusy
{
get => _isBusy;
set
{
_isBusy = value;
OnPropertyChanged();
}
}

処理実行時にIsBusyを切り替えます。

C#
public async Task ExecuteAsync()
{
try
{
IsBusy = true;

await Task.Run(() =>
{
DoHeavyWork();
});
}
finally
{
IsBusy = false;
}
}

XAMLではIsBusyにバインドして表示を切り替えます。

XML
<ProgressBar IsIndeterminate="True"
Visibility="{Binding IsBusy, Converter={StaticResource BooleanToVisibilityConverter}}" />

MVVMでは、コードビハインドで直接UIを操作するのではなく、状態をプロパティとして持ち、View側で表示に反映する設計にすると保守しやすくなります。

9. 処理中表示でよくある失敗と解決方法

9-1. 処理中ラベルが表示されない

「処理中です」と設定しているのに画面に表示されない場合、直後にUIスレッドで重い処理を実行している可能性があります。

悪い例:

C#
labelStatus.Text = "処理中です...";
DoHeavyWork();

この場合、ラベルを更新しても画面が再描画される前に重い処理が始まるため、表示されないことがあります。

解決方法は、重い処理をTask.RunなどでUIスレッドから切り離すことです。

C#
labelStatus.Text = "処理中です...";

await Task.Run(() =>
{
DoHeavyWork();
});

UIに表示を反映させたいからといって、安易にApplication.DoEvents()を使うのはおすすめしません。イベントの再入や予期しない動作を招くことがあります。

9-2. プログレスバーが動かない

プログレスバーが動かない原因として多いのは、UIスレッドがブロックされていることです。

特にMarquee表示は、UIスレッドが動いていないとアニメーションできません。

悪い例:

C#
progressBar1.Style = ProgressBarStyle.Marquee;
DoHeavyWork();

解決方法は、重い処理をバックグラウンドで実行することです。

C#
progressBar1.Style = ProgressBarStyle.Marquee;

await Task.Run(() =>
{
DoHeavyWork();
});

プログレスバーは表示して終わりではなく、UIスレッドが動ける状態にしておく必要があります。

9-3. awaitしているのに画面が固まる

awaitを使っているのに画面が固まる場合、非同期になっているように見えて、実際には重い処理をUIスレッドで実行している可能性があります。

例えば、次のようなコードです。

C#
private async Task DoWorkAsync()
{
DoHeavyWork();
}

メソッド名にAsyncが付いていても、中身が同期処理なら画面は固まります。

CPU負荷の高い処理なら、次のようにTask.Runを使います。

C#
private async Task DoWorkAsync()
{
await Task.Run(() =>
{
DoHeavyWork();
});
}

一方、HTTP通信やファイル読み込みなどで非同期APIがある場合は、そのAPIをawaitします。

C#
var text = await File.ReadAllTextAsync(path);

重要なのは、awaitを書くことではなく、待っている処理が本当にUIスレッドをブロックしない形になっていることです。

9-4. クロススレッド例外が発生する

バックグラウンドスレッドからUI部品を直接更新すると、クロススレッド例外が発生することがあります。

悪い例:

C#
await Task.Run(() =>
{
labelStatus.Text = "処理中です...";
});

解決方法は、UI更新をUIスレッド側で行うことです。

基本的には、awaitの前後でUIを更新します。

C#
labelStatus.Text = "処理中です...";

await Task.Run(() =>
{
DoHeavyWork();
});

labelStatus.Text = "完了しました";

進捗を通知したい場合は、IProgress<T>を使います。

C#
var progress = new Progress<int>(value =>
{
progressBar1.Value = value;
});

await Task.Run(() =>
{
DoWorkWithProgress(progress);
});

9-5. async voidを使ってよい場面と避けるべき場面

async voidを使ってよい代表的な場面は、WinFormsやWPFのイベントハンドラです。

C#
private async void buttonStart_Click(object sender, EventArgs e)
{
await ExecuteAsync();
}

一方、通常のメソッドではasync voidではなくasync Taskを使うべきです。

C#
private async Task ExecuteAsync()
{
await Task.Delay(1000);
}

async voidは呼び出し元で完了を待てず、例外処理もしにくくなります。

そのため、イベントハンドラ以外の非同期メソッドでは、基本的にTaskを返すようにしましょう。

10. 実務で使いやすい処理中表示の設計ポイント

10-1. 処理中・成功・失敗をユーザーに伝える

実務では、単に「処理中です」と表示するだけでなく、状態ごとにメッセージを分けると親切です。

C#
labelStatus.Text = "処理中です...";

成功時:

C#
labelStatus.Text = "完了しました";

失敗時:

C#
labelStatus.Text = "失敗しました";

ユーザー向けメッセージは、専門的な例外内容をそのまま出すのではなく、何が起きたか、次にどうすればよいかがわかる表現にするとよいです。

例:

C#
MessageBox.Show("データの取得に失敗しました。通信状態を確認してから再実行してください。");

10-2. キャンセルボタンを用意する

処理に時間がかかる場合は、キャンセルボタンを用意するとユーザー体験が良くなります。

特に、次のような処理ではキャンセルできる設計が望ましいです。

  • 大量ファイルの処理

  • 長時間の検索

  • 大容量データの読み込み

  • ネットワーク通信

  • 一括変換処理

キャンセルできない処理の場合でも、「しばらくお待ちください」「この処理は数分かかる場合があります」のように表示すると、ユーザーが状況を理解しやすくなります。

10-3. CancellationTokenで処理を中断する

C#でキャンセル可能な処理を実装するには、CancellationTokenを使います。

C#
private CancellationTokenSource? _cts;

private async void buttonStart_Click(object sender, EventArgs e)
{
_cts = new CancellationTokenSource();

try
{
buttonStart.Enabled = false;
buttonCancel.Enabled = true;
labelStatus.Text = "処理中です...";

await Task.Run(() =>
{
DoWork(_cts.Token);
});

labelStatus.Text = "完了しました";
}
catch (OperationCanceledException)
{
labelStatus.Text = "キャンセルされました";
}
finally
{
buttonStart.Enabled = true;
buttonCancel.Enabled = false;
_cts.Dispose();
_cts = null;
}
}

private void buttonCancel_Click(object sender, EventArgs e)
{
_cts?.Cancel();
}

private void DoWork(CancellationToken token)
{
for (int i = 0; i < 100; i++)
{
token.ThrowIfCancellationRequested();

// 重い処理の代わり
Thread.Sleep(100);
}
}

CancellationTokenを使うと、処理中にキャンセル要求を受け取り、安全に中断できます。

ただし、強制的にスレッドを止めるのではなく、処理側が定期的にキャンセル要求を確認して終了する形にするのが基本です。

10-4. ログ出力とエラーメッセージを分ける

例外が発生したとき、ユーザーに詳細なスタックトレースをそのまま見せるのは避けたほうがよいです。

ユーザーにはわかりやすいメッセージを表示し、開発者向けの詳細情報はログに出力します。

C#
try
{
await ExecuteAsync();
}
catch (Exception ex)
{
// 開発者向けログ
File.AppendAllText("error.log", ex.ToString());

// ユーザー向けメッセージ
MessageBox.Show("処理中にエラーが発生しました。時間をおいて再実行してください。");
}

こうすることで、ユーザー体験を損なわず、トラブル調査に必要な情報も残せます。

10-5. 共通メソッド化して再利用しやすくする

処理中表示は、画面内の複数箇所で使うことが多いため、共通メソッド化しておくと便利です。

C#
private void SetBusy(bool isBusy, string message = "処理中です...")
{
labelStatus.Visible = isBusy;
labelStatus.Text = isBusy ? message : "";

progressBar1.Visible = isBusy;
progressBar1.Style = ProgressBarStyle.Marquee;

buttonStart.Enabled = !isBusy;
UseWaitCursor = isBusy;
}

使用例:

C#
private async void buttonStart_Click(object sender, EventArgs e)
{
try
{
SetBusy(true, "データを取得しています...");

await Task.Run(() =>
{
DoHeavyWork();
});
}
finally
{
SetBusy(false);
}
}

画面ごとに同じようなコードを書くよりも、共通化したほうが修正漏れを防げます。

11. BackgroundWorkerとasync/awaitはどちらを使うべきか

11-1. BackgroundWorkerが使われてきた背景

BackgroundWorkerは、以前からWinFormsなどでバックグラウンド処理を行うためによく使われてきたクラスです。

主に次のような機能があります。

  • バックグラウンドで処理を実行する

  • 進捗を通知する

  • 完了時にイベントを受け取る

  • キャンセルを扱う

例:

C#
backgroundWorker1.DoWork += (s, e) =>
{
// バックグラウンド処理
};

backgroundWorker1.RunWorkerCompleted += (s, e) =>
{
// 完了後の処理
};

backgroundWorker1.RunWorkerAsync();

古いWinFormsアプリでは、今でもBackgroundWorkerが使われていることがあります。

11-2. 現在はasync/awaitが基本になる理由

現在のC#では、新規実装であればasync/awaitを使うのが基本です。

理由は、コードの流れがわかりやすく、TaskベースのAPIと相性がよいからです。

BackgroundWorkerでは、処理本体、進捗通知、完了処理をイベントとして分けて書く必要があります。

一方、async/awaitでは次のように自然な流れで書けます。

C#
try
{
SetBusy(true);

await Task.Run(() =>
{
DoHeavyWork();
});

MessageBox.Show("完了しました。");
}
finally
{
SetBusy(false);
}

HTTP通信、ファイルI/O、データベースアクセスなど、多くの処理がTaskベースの非同期APIを提供しているため、async/awaitのほうが現代的なC#開発に合っています。

11-3. 既存コードでBackgroundWorkerを使う場合

既存アプリでBackgroundWorkerを使っている場合、すぐにすべてを置き換える必要はありません。

安定して動いている処理であれば、そのまま保守する選択もあります。

ただし、次のような場合はasync/awaitへの移行を検討するとよいです。

  • 新しい非同期APIを使いたい

  • 処理の流れがイベントで分散して読みにくい

  • エラー処理が複雑になっている

  • キャンセル処理を整理したい

  • 新規機能を追加する予定がある

BackgroundWorkerを使い続ける場合でも、UI更新は適切なイベント内で行い、バックグラウンド処理から直接UIを触らないようにします。

11-4. BackgroundWorkerからasync/awaitへ移行する考え方

移行する場合は、まずDoWorkに書かれている処理を通常のメソッドに切り出します。

C#
private void DoHeavyWork()
{
// 元のDoWorkの中身
}

次に、ボタンクリックなどからTask.Runで呼び出します。

C#
private async void buttonStart_Click(object sender, EventArgs e)
{
try
{
SetBusy(true);

await Task.Run(() =>
{
DoHeavyWork();
});

MessageBox.Show("完了しました。");
}
finally
{
SetBusy(false);
}
}

進捗通知が必要な場合は、IProgress<T>を引数に追加します。

C#
private void DoHeavyWork(IProgress<int> progress)
{
for (int i = 1; i <= 100; i++)
{
Thread.Sleep(50);
progress.Report(i);
}
}

そして呼び出し側でProgress<int>を作成します。

C#
var progress = new Progress<int>(value =>
{
progressBar1.Value = value;
});

await Task.Run(() =>
{
DoHeavyWork(progress);
});

このように段階的に移行すると、既存コードを大きく壊さずにasync/awaitへ置き換えられます。

12. C#の処理中表示に関するよくある質問

12-1. 処理中に画面操作できないようにするには?

WinFormsなら、ボタンやPanelのEnabledfalseにします。

C#
buttonStart.Enabled = false;
panelInput.Enabled = false;

WPFなら、IsEnabledfalseにします。

C#
InputArea.IsEnabled = false;

処理完了後は必ず元に戻します。

C#
buttonStart.Enabled = true;
panelInput.Enabled = true;

戻し忘れを防ぐため、finallyで制御するのがおすすめです。

12-2. 処理中にくるくる回る表示を出すには?

WinFormsの標準ProgressBarでは、くるくる回るスピナーは用意されていません。

標準機能だけで実装するなら、ProgressBarStyle.Marqueeを使うのが簡単です。

C#
progressBar1.Style = ProgressBarStyle.Marquee;
progressBar1.Visible = true;

WPFでは、標準のProgressBarにIsIndeterminate="True"を設定すると、進捗率不明の処理中表示ができます。

XML
<ProgressBar IsIndeterminate="True" />

本格的なスピナー表示を使いたい場合は、自作コントロールやUIライブラリを使う方法もあります。

12-3. 処理時間が読めない場合はどう表示する?

処理時間や進捗率が読めない場合は、パーセント表示ではなく、進捗率不明の処理中表示を使います。

WinFormsならMarqueeです。

C#
progressBar1.Style = ProgressBarStyle.Marquee;

WPFならIsIndeterminateです。

XML
<ProgressBar IsIndeterminate="True" />

この場合、ユーザーには次のようなメッセージを表示するとよいです。

C#
labelStatus.Text = "処理中です。しばらくお待ちください...";

完了までの割合がわからないのに無理にパーセント表示をすると、逆に不自然になります。

12-4. 処理中に別フォームを表示してもよい?

別フォームで「処理中です」と表示しても問題ありません。

ただし、重い処理をUIスレッドで実行すると、その別フォームも固まります。

重要なのは、別フォームを表示することではなく、重い処理をUIスレッドで実行しないことです。

基本形は次のようになります。

C#
var dialog = new ProcessingForm();

try
{
dialog.Show();

await Task.Run(() =>
{
DoHeavyWork();
});
}
finally
{
dialog.Close();
}

ただし、モーダル表示や所有フォームの扱い、キャンセル処理などを考える必要があるため、単純な処理中表示であれば、まずは同一フォーム内のラベルやプログレスバーで実装するのがおすすめです。

12-5. プログレスバーの値を別クラスから更新するには?

別クラスからProgressBarを直接操作するのではなく、IProgress<T>を渡して進捗を通知する方法がおすすめです。

処理クラスの例です。

C#
public class ImportService
{
public void Import(IProgress<int> progress)
{
for (int i = 1; i <= 100; i++)
{
Thread.Sleep(50);
progress.Report(i);
}
}
}

画面側の例です。

C#
private async void buttonStart_Click(object sender, EventArgs e)
{
var service = new ImportService();

var progress = new Progress<int>(value =>
{
progressBar1.Value = value;
labelStatus.Text = $"処理中です... {value}%";
});

await Task.Run(() =>
{
service.Import(progress);
});
}

この方法なら、処理クラスはUIに依存せず、WinFormsでもWPFでも使いやすい設計になります。

まとめ

C#で処理中を表示するには、ラベルやプログレスバーを表示するだけでなく、UIスレッドをブロックしない実装にすることが重要です。

画面が固まる主な原因は、時間のかかる処理をUIスレッド上で実行していることです。async/awaitを使い、CPU負荷の高い処理はTask.Runでバックグラウンドに逃がし、通信やファイルI/Oなどは非同期APIをawaitするのが基本です。

処理中表示の実装では、次のポイントを押さえておきましょう。

  • 処理開始時に「処理中です」と表示する

  • 処理中はボタンを無効化して二重実行を防ぐ

  • 処理完了後は表示やボタン状態を元に戻す

  • 例外発生時にもfinallyで必ず状態を復元する

  • 進捗率が不明ならMarqueeやIsIndeterminateを使う

  • 進捗率がわかるならProgressBarのValueを更新する

  • バックグラウンド処理からUIを直接触らない

  • 進捗通知にはIProgress<T>を使う

  • 長時間処理にはキャンセル機能も検討する

  • 新規実装ではBackgroundWorkerよりasync/awaitを基本にする

C#の処理中表示は、ユーザーに安心感を与えるための重要なUIです。async/awaitTask.RunIProgress<T>、ProgressBarを適切に組み合わせることで、画面を固めずにわかりやすい処理中表示を実装できます。