まえがき
本当はPCに一瞬たりとも待たされたくないんです。待ち時間は操作するユーザーのためにあるもので、PCがユーザーを待たせるなど言語道断。とは言うものの全ての処理が一瞬で終了するわけでもなし。
というわけでPCがユーザーを待たせている間は「このへんまで進んでるでー」というのをお知らせしたい。1秒やそこら待たされるのであればちょっと反応悪いなぐらいで済みますが数秒から数十秒ともなればちゃんと処理が進んでいるのかそれともなんかの都合でハングしちゃてるのかぐらいは知りたい。昔何かで読みましたが同じ秒数を待たせるにしても進捗表示のありなしで体感時間が変わってくるとか。
で、今回は超単純な例を出します。フォーム上のボタンをクリックすると5秒後に「終わり!」と表示します。5秒なので1秒おきに進捗通知(%)を行いラベルに表示します。
何も考えずに実装した
using System;
using System.Linq;
using System.Threading;
using System.Windows.Forms;
namespace WindowsFormsApp4
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
button1.Click += Button1_Click;
}
private void Button1_Click(object sender, EventArgs e)
{
foreach (var i in Enumerable.Range(1, 5))
{
Thread.Sleep(1000);
label1.Text = $"{i * 20}%";
}
MessageBox.Show("終わり!");
}
}
}
1秒おきに進捗表示を更新するので1秒スリープx5にし、スリープが終わると進捗をラベルに表示する計画。動かしてみるとどうでしょう。全然ラベルの表示が更新されず、5秒後に100%表示になります。まぁ当たり前の結果です。
古の手法(現在では全く推奨されない)
using System;
using System.Linq;
using System.Threading;
using System.Windows.Forms;
namespace WindowsFormsApp4
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
button1.Click += Button1_Click;
}
private void Button1_Click(object sender, EventArgs e)
{
foreach (var i in Enumerable.Range(1, 5))
{
Thread.Sleep(1000);
label1.Text = $"{i * 20}%";
Application.DoEvents(); // この行を追加
}
MessageBox.Show("終わり!");
}
}
}
VB/VBAで良くやった古の手法。
Application.DoEvents メソッド
メッセージ キューに現在ある Windows メッセージをすべて処理します。
これはこれでちゃんと進捗表示になりはしますが今時こんなコード書く人もいないでしょう。
BackgroundWorkerを使ってみる
using System;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Windows.Forms;
namespace WindowsFormsApp4
{
public partial class Form1 : Form
{
private BackgroundWorker bw;
public Form1()
{
InitializeComponent();
button1.Click += Button1_Click;
}
private void Button1_Click(object sender, EventArgs e)
{
bw = new BackgroundWorker();
bw.WorkerReportsProgress = true; // 進捗通知可能にする
bw.DoWork += Bw_DoWork;
bw.ProgressChanged += Bw_ProgressChanged;
bw.RunWorkerCompleted += Bw_RunWorkerCompleted;
bw.RunWorkerAsync();
}
private void Bw_DoWork(object sender, DoWorkEventArgs e)
{
foreach (var i in Enumerable.Range(1, 5))
{
Thread.Sleep(1000);
bw.ReportProgress(i * 20); // 進捗(%)を報告
}
}
// 進捗通知を受けたらラベルに表示
private void Bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
label1.Text = $"{e.ProgressPercentage}%";
}
private void Bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
MessageBox.Show("終わり!");
}
}
}
シンプルさではBackgroundWorkerかなと思わないでもないですが。今頃BackgroundWorker云々言うのは単に私が慣れ親しんでいるからです。
Task(async/await)版
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApp5
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
button1.Click += Button1_Click;
}
private async void Button1_Click(object sender, EventArgs e)
{
var progress = new Progress<int>(onProgressChanged);
var task = Task.Run(() =>
{
Waiting(progress);
});
await task;
MessageBox.Show("終わり!");
}
private void Waiting(IProgress<int> iProgress)
{
foreach (var i in Enumerable.Range(1, 5))
{
Thread.Sleep(1000);
iProgress.Report(i * 20); // 進捗(%)を報告
}
}
// 進捗通知を受けたらラベルに表示
private void onProgressChanged(int per)
{
label1.Text = $"{per}%";
}
}
}
Task使った同じく非同期処理。ちょっと構造が複雑ですがこれもイディオムだと思えば馴れの範疇でしょうか。
あとがき
ソフトウェア作成当初は扱うデータ量も少なくて特に進捗表示不要にも思うこともあるかもしれませんが余力があれば盛り込んでおくと幸せになることがあるかも。
進捗表示は別に%表示に限ったものではなくて処理ファイル数やダウンロードバイト数など百分率ではない表示でも全然いいと思います。知りたいのは「処理は進んでいるのか」「進んでいるならどのへんまで」ですから。そうは言ってもプログレスバーで表示するなら大概プロパティは百分率な気もしますけど。