┏第44号━━━━━━━━━━━━━━━━━━━━━━━━━━┓ ┃         .NETプログラミング研究         ┃ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ ──<メニュー>─────────────────────── ■.NET Tips ・時間のかかる処理の進行状況を表示する ・時間のかかる処理をユーザーが停止できるようにする ─────────────────────────────── ─────────────────────────────── ■.NET Tips ─────────────────────────────── ●時間のかかる処理の進行状況を表示する 大きなファイルを読み込んだり、大量のファイルをコピーするなどそ の処理に時間がかかるとき、ユーザーにアプリケーションがフリーズ したのではないかという心配をさせないように、処理の進行状況をメ ッセージやプログレスバーで表示する方法を説明します。 ここでは単純な例として、次のようなアプリケーションを作成します。 WindowsフォームにLabelコントロール(Label1)とProgressBarコント ロール(ProgressBar1)とButtonコントロール(Button1)を貼り付け、 Button1をクリックすると1秒おきにLabel1とProgressBar1の内容を変 えて(1から10までカウントアップする)表示されるようにします。 まずは、何も考えないで、Label.TextプロパティとProgressBar. Valueプロパティを変更し、それ以外何もしないでどのように表示さ れるか確かめてみましょう。 ‥‥▽VB.NET ここから▽‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ 'Button1のクリックイベントハンドラ Private Sub Button1_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Button1.Click 'Button1のクリックイベントハンドラ 'ProgressBar1の設定 ProgressBar1.Minimum = 1 ProgressBar1.Maximum = 10 Dim i As Integer For i = 1 To 10 'ProgressBar1の値を変更する ProgressBar1.Value = i 'Label1のテキストを変更する Label1.Text = i.ToString() '1秒間待機する(本来なら何らかの処理を行う) System.Threading.Thread.Sleep(1000) Next i End Sub ‥‥△VB.NET ここまで△‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ ‥‥▽C# ここから▽‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ //Button1のクリックイベントハンドラ private void Button1_Click(object sender, System.EventArgs e) { //ProgressBar1の設定 ProgressBar1.Minimum = 1; ProgressBar1.Maximum = 10; for (int i = 1; i <= 10; i++) { //ProgressBar1の値を変更する ProgressBar1.Value = i; //Label1のテキストを変更する Label1.Text = i.ToString(); //1秒間待機する(本来なら何らかの処理を行う) System.Threading.Thread.Sleep(1000); } } ‥‥△C# ここまで△‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ 上記のコードを実行すると、ProgressBarは1秒おきにバーが伸びてい きますが、Labelに表示される文字列は変化することなく、 Button1_Clickイベントハンドラを抜けてはじめて"10"と表示されま す。これは、ProgressBarコントロールがそのValueプロパティが変更 されるとコントロールを再描画するのに対して、Labelコントロール はTextプロパティを変更してもそうしないためです。ほとんどのコン トロールはLabelコントロールと同様の挙動となるでしょう。 ProgressBarコントロールのようにLabelコントロールでも1秒おきに 内容を変更して表示するためには、Label.Textプロパティを設定後に、 Updateメソッドを呼び出してコントロールを再描画するようにします。 ‥‥▽VB.NET ここから▽‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ 'Button1のクリックイベントハンドラ Private Sub Button1_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Button1.Click 'ProgressBar1の設定 ProgressBar1.Minimum = 1 ProgressBar1.Maximum = 10 Dim i As Integer For i = 1 To 10 'ProgressBar1の値を変更する ProgressBar1.Value = i 'Label1のテキストを変更する Label1.Text = i.ToString() 'Label1を再描画する Label1.Update() '(フォーム全体を再描画するには、次のようにする) 'Me.Update() '1秒間待機する(本来なら何らかの処理を行う) System.Threading.Thread.Sleep(1000) Next i End Sub ‥‥△VB.NET ここまで△‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ ‥‥▽C# ここから▽‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ //Button1のクリックイベントハンドラ private void Button1_Click(object sender, System.EventArgs e) { //ProgressBar1の設定 ProgressBar1.Minimum = 1; ProgressBar1.Maximum = 10; for (int i = 1; i <= 10; i++) { //ProgressBar1の値を変更する ProgressBar1.Value = i; //Label1のテキストを変更する Label1.Text = i.ToString(); //Label1を再描画する Label1.Update(); //(フォーム全体を再描画するには、次のようにする) //this.Update(); //1秒間待機する(本来なら何らかの処理を行う) System.Threading.Thread.Sleep(1000); } } ‥‥△C# ここまで△‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ 進行状況を表示するだけであれば、このようにUpdateメソッドを使う のが最も簡単で、安全な方法です。しかしこの方法では処理の間、フ ォームやコントロールを一切操作することが出来なくなります。この ような状況が好ましくなければ、Application.DoEventsメソッドを使 うか、マルチスレッド化するという方法をとる必要があります。 Application.DoEventsメソッドを呼び出すと、キュー内で待機中のイ ベントを処理することができるため、次のようにループ内にDoEvents を入れることにより、フォームやコントロールが再描画されるのはも ちろんのこと、フォームの移動やクリックなど、ユーザーによる操作 も可能になります。 ‥‥▽VB.NET ここから▽‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ 'Button1のクリックイベントハンドラ Private Sub Button1_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Button1.Click 'ProgressBar1の設定 ProgressBar1.Minimum = 1 ProgressBar1.Maximum = 10 Dim i As Integer For i = 1 To 10 'ProgressBar1の値を変更する ProgressBar1.Value = i 'Label1のテキストを変更する Label1.Text = i.ToString() '待機中のイベントを処理する Application.DoEvents() '1秒間待機する(本来なら何らかの処理を行う) System.Threading.Thread.Sleep(1000) Next i End Sub ‥‥△VB.NET ここまで△‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ ‥‥▽C# ここから▽‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ //Button1のクリックイベントハンドラ private void Button1_Click(object sender, System.EventArgs e) { //ProgressBar1の設定 ProgressBar1.Minimum = 1; ProgressBar1.Maximum = 10; for (int i = 1; i <= 10; i++) { //ProgressBar1の値を変更する ProgressBar1.Value = i; //Label1のテキストを変更する Label1.Text = i.ToString(); //待機中のイベントを処理する Application.DoEvents(); //1秒間待機する(本来なら何らかの処理を行う) System.Threading.Thread.Sleep(1000); } } ‥‥△C# ここまで△‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ Application.DoEventsメソッド(あるいはマルチスレッドでも同じこ とが言えます)を使う時に注意すべきことは、DoEventsメソッドが呼 び出された時に発生しては困るすべてのイベントを把握し、それらが 発生しないように対策を施す必要があるということです。例えば上の 例では、明らかにButton1_Clickイベントハンドラを処理中にもう一 度Button1_Clickイベントハンドラが呼び出されてはいけません。よ ってこのようなことが起こらないように、ループの前にButton1の Enabledプロパティを無効にし、終了後に再び有効にするといった処 理が必要になります。 また、DoEventsメソッドならではの欠点もあります。イベントが処理 されるのはDoEventsメソッドが呼び出された時だけですので、例えば 上記の例でユーザーがフォームの「閉じる」ボタンをクリックしても すぐには閉じなかったり、タイミングによっては閉じなかったりしま す。 しかも、DoEventsメソッドが呼び出されイベントが処理される時、そ の処理がすべて終わるまでDoEventsメソッド以降の処理はブロックさ れます。例えば上記の例の場合、フォームをマウスで移動している間 ループ処理は中断され、移動をやめてからようやくカウントアップが 再開されます。 このようなDoEventsメソッドの欠点はマルチスレッドを使用すること により、解消されます。 .NETのマルチスレッドプログラミングについては、このメールマガジ ンの第19号から第26号まで散々やってきましたので、ここでは一切説 明せず、いきなりサンプルを示します。このサンプルでは、カウント アップするメソッドをメインとは別のスレッドで処理しています。こ こでは、Button1がカウントアップ中にクリックされないように、 Button1のEnabledプロパティをいじっています。 ‥‥▽VB.NET ここから▽‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ 'ボタンのEnabledを変更するためのデリゲート Delegate Sub SetButtonEnabledDelegate(ByVal en As Boolean) 'コントロールの値を変更するためのデリゲート Delegate Sub SetProgressValueDelegate(ByVal num As Integer) '別処理をするためのスレッド Dim thread As System.Threading.Thread 'Button1のクリックイベントハンドラ Private Sub Button1_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Button1.Click 'ProgressBar1の設定 ProgressBar1.Minimum = 1 ProgressBar1.Maximum = 10 'CountUpメソッドを別スレッドで処理する thread = New System.Threading.Thread( _ New System.Threading.ThreadStart(AddressOf CountUp)) thread.IsBackground = True thread.Start() End Sub Private Sub CountUp() 'デリゲートの作成 Dim bdlg As New SetButtonEnabledDelegate( _ AddressOf SetButtonEnabled) Dim dlg As New SetProgressValueDelegate( _ AddressOf SetProgressValue) 'Button1を無効にする Me.Invoke(bdlg, New Object() {False}) Dim i As Integer For i = 1 To 10 'コントロールの値を変更する Me.Invoke(dlg, New Object() {i}) '1秒間待機する(本来なら何らかの処理を行う) System.Threading.Thread.Sleep(1000) Next i 'Button1を有効に戻す Me.Invoke(bdlg, New Object() {True}) End Sub 'コントロールの値を変更する Private Sub SetProgressValue(ByVal num As Integer) 'ProgressBar1の値を変更する ProgressBar1.Value = num 'Label1のテキストを変更する Label1.Text = num.ToString() End Sub 'ボタンのEnabledを変更する Private Sub SetButtonEnabled(ByVal en As Boolean) Button1.Enabled = en End Sub ‥‥△VB.NET ここまで△‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ ‥‥▽C# ここから▽‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ //ボタンのEnabledを変更するためのデリゲート private delegate void SetButtonEnabledDelegate(bool en); //コントロールの値を変更するためのデリゲート private delegate void SetProgressValueDelegate(int num); //別処理をするためのスレッド private System.Threading.Thread thread; //Button1のクリックイベントハンドラ private void Button1_Click(object sender, System.EventArgs e) { //ProgressBar1の設定 ProgressBar1.Minimum = 1; ProgressBar1.Maximum = 10; //CountUpメソッドを別スレッドで処理する thread = new System.Threading.Thread( new System.Threading.ThreadStart(CountUp)); thread.IsBackground = true; thread.Start(); } private void CountUp() { //デリゲートの作成 SetButtonEnabledDelegate bdlg = new SetButtonEnabledDelegate(SetButtonEnabled); SetProgressValueDelegate dlg = new SetProgressValueDelegate(SetProgressValue); //Button1を無効にする this.Invoke(bdlg, new object[] {false}); for (int i = 1; i <= 10; i++) { //コントロールの値を変更する this.Invoke(dlg, new object[] {i}); //1秒間待機する(本来なら何らかの処理を行う) System.Threading.Thread.Sleep(1000); } //Button1を有効に戻す this.Invoke(bdlg, new object[] {true}); } //コントロールの値を変更する private void SetProgressValue(int num) { //ProgressBar1の値を変更する ProgressBar1.Value = num; //Label1のテキストを変更する Label1.Text = num.ToString(); } //ボタンのEnabledを変更する private void SetButtonEnabled(bool en) { Button1.Enabled = en; } ‥‥△C# ここまで△‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ 以上のように、Application.DoEventsメソッドを使う方法と、マルチ スレッドによる方法は、Updateメソッドのみを使った方法と比べて、 考慮すべき事柄が多く、危険性が高く、面倒であることがわかります。 よってこれらの方法は初心者向きとは言えませんが、様々な局面(例 えば、次に紹介するTips)においては有効な方法であることは間違い ありません。 ─────────────────────────────── ●時間のかかる処理をユーザーが停止できるようにする 次に、時間のかかる処理をユーザーがキャンセルボタンをクリックす るなどして、途中で停止できるようにする方法を考えます。 先の「時間のかかる処理の進行状況を表示する」で考察した通り、 Application.DoEventsメソッドを使うか、マルチスレッドを使うかと いうことになるでしょう。 DoEventsメソッドを使用する場合は、キャンセルボタンがクリックさ れたらフラッグを立てるようにして、ループ内ではDoEventsメソッド を呼び出した後でフラッグをチェックし、フラッグが立っていればルー プを抜けるようにすればよいでしょう。 このようにして「時間のかかる処理の進行状況を表示する」のサンプ ルを改良してコードを以下に示します。キャンセルボタン(Button2) をフォームに追加し、Button2をクリックすると、カウントアップ処 理が中止されるようにしています。 ‥‥▽VB.NET ここから▽‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ 'キャンセルボタンがクリックされたか Private canceled As Boolean = False 'Button1のクリックイベントハンドラ Private Sub Button1_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Button1.Click 'ProgressBar1の設定 ProgressBar1.Minimum = 1 ProgressBar1.Maximum = 10 '初期化 Button1.Enabled = False Button2.Enabled = True canceled = False Dim i As Integer For i = 1 To 10 'ProgressBar1の値を変更する ProgressBar1.Value = i 'Label1のテキストを変更する Label1.Text = i.ToString() '待機中のイベントを処理する Application.DoEvents() 'キャンセルボタンがクリックされたか調べる If canceled Then 'キャンセルボタンがクリックされた時 MessageBox.Show(Me, "ユーザーにより中止されました。") 'ループを抜ける Exit For End If '1秒間待機する(本来なら何らかの処理を行う) System.Threading.Thread.Sleep(1000) Next i '終了時の処理 Button1.Enabled = True Button2.Enabled = False End Sub 'Button2のクリックイベントハンドラ Private Sub Button2_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Button2.Click 'フラッグを立てる canceled = True End Sub ‥‥△VB.NET ここまで△‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ ‥‥▽C# ここから▽‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ //キャンセルボタンがクリックされたか private bool canceled = false; //Button1のクリックイベントハンドラ private void Button1_Click(object sender, System.EventArgs e) { //ProgressBar1の設定 ProgressBar1.Minimum = 1; ProgressBar1.Maximum = 10; //初期化 Button1.Enabled = false; Button2.Enabled = true; canceled = false; for (int i = 1; i <= 10; i++) { //ProgressBar1の値を変更する ProgressBar1.Value = i; //Label1のテキストを変更する Label1.Text = i.ToString(); //待機中のイベントを処理する Application.DoEvents(); //キャンセルボタンがクリックされたか調べる if (canceled) { //キャンセルボタンがクリックされた時 MessageBox.Show(this, "ユーザーにより中止されました。"); //ループを抜ける break; } //1秒間待機する(本来なら何らかの処理を行う) System.Threading.Thread.Sleep(1000); } //終了時の処理 Button1.Enabled = true; Button2.Enabled = false; } //Button2のクリックイベントハンドラ private void Button2_Click(object sender, System.EventArgs e) { //フラッグを立てる canceled = true; } ‥‥△C# ここまで△‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ 次はマルチスレッドを使った方法です。この場合、キャンセルボタン が押された時に、処理中のスレッドのThread.Abortメソッドを呼び出 してスレッドを中止させればよいという考えもあるでしょう。しかし Abortメソッドを使って別のスレッドを終了させることは、どこで中 断されるか予測できなく、それだけ危険といえます。よって、Abort メソッドはなるべく使わないようにして、フラッグを使って適当なタ イミングでループを終了させる方法をお勧めします。 以下にこのような方法により、「時間のかかる処理の進行状況を表示 する」で紹介したコードにキャンセルボタン(Button2)の処理を付け たコードを示します。 ‥‥▽VB.NET ここから▽‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ 'キャンセルボタンがクリックされたか Private canceled As Boolean = False 'ボタンのEnabledを変更するためのデリゲート Delegate Sub SetButtonEnabledDelegate(ByVal en As Boolean) 'コントロールの値を変更するためのデリゲート Delegate Sub SetProgressValueDelegate(ByVal num As Integer) '別処理をするためのスレッド Private thread As System.Threading.Thread 'Button1のクリックイベントハンドラ Private Sub Button1_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Button1.Click 'ProgressBar1の設定 ProgressBar1.Minimum = 1 ProgressBar1.Maximum = 10 'CountUpメソッドを別スレッドで処理する thread = New System.Threading.Thread( _ New System.Threading.ThreadStart(AddressOf CountUp)) thread.IsBackground = True thread.Start() End Sub 'Button2のクリックイベントハンドラ Private Sub Button2_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles Button2.Click 'フラッグを立てる canceled = True End Sub Private Sub CountUp() 'デリゲートの作成 Dim bdlg As New SetButtonEnabledDelegate( _ AddressOf SetButtonEnabled) Dim dlg As New SetProgressValueDelegate( _ AddressOf SetProgressValue) 'Button1を無効にする Me.Invoke(bdlg, New Object() {False}) Dim i As Integer For i = 1 To 10 'キャンセルボタンがクリックされたか調べる If canceled Then 'キャンセルボタンがクリックされた時 MessageBox.Show(Me, "ユーザーにより中止されました。") 'ループを抜ける Exit For End If 'コントロールの値を変更する Me.Invoke(dlg, New Object() {i}) '1秒間待機する(本来なら何らかの処理を行う) System.Threading.Thread.Sleep(1000) Next i 'Button1を有効に戻す Me.Invoke(bdlg, New Object() {True}) End Sub 'コントロールの値を変更する Private Sub SetProgressValue(ByVal num As Integer) 'ProgressBar1の値を変更する ProgressBar1.Value = num 'Label1のテキストを変更する Label1.Text = num.ToString() End Sub 'ボタンのEnabledを変更する Private Sub SetButtonEnabled(ByVal en As Boolean) Button1.Enabled = en End Sub ‥‥△VB.NET ここまで△‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ ‥‥▽C# ここから▽‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ //キャンセルボタンがクリックされたか private volatile bool canceled = false; //ボタンのEnabledを変更するためのデリゲート private delegate void SetButtonEnabledDelegate(bool en); //コントロールの値を変更するためのデリゲート private delegate void SetProgressValueDelegate(int num); //別処理をするためのスレッド private System.Threading.Thread thread; //Button1のクリックイベントハンドラ private void Button1_Click(object sender, System.EventArgs e) { //ProgressBar1の設定 ProgressBar1.Minimum = 1; ProgressBar1.Maximum = 10; //CountUpメソッドを別スレッドで処理する thread = new System.Threading.Thread( new System.Threading.ThreadStart(CountUp)); thread.IsBackground = true; thread.Start(); } //Button2のクリックイベントハンドラ private void Button2_Click(object sender, System.EventArgs e) { //フラッグを立てる canceled = true; } private void CountUp() { //デリゲートの作成 SetButtonEnabledDelegate bdlg = new SetButtonEnabledDelegate(SetButtonEnabled); SetProgressValueDelegate dlg = new SetProgressValueDelegate(SetProgressValue); //Button1を無効にする this.Invoke(bdlg, new object[] {false}); for (int i = 1; i <= 10; i++) { //キャンセルボタンがクリックされたか調べる if (canceled) { //キャンセルボタンがクリックされた時 MessageBox.Show(this, "ユーザーにより中止されました。"); //ループを抜ける break; } //コントロールの値を変更する this.Invoke(dlg, new object[] {i}); //1秒間待機する(本来なら何らかの処理を行う) System.Threading.Thread.Sleep(1000); } //Button1を有効に戻す this.Invoke(bdlg, new object[] {true}); } //コントロールの値を変更する private void SetProgressValue(int num) { //ProgressBar1の値を変更する ProgressBar1.Value = num; //Label1のテキストを変更する Label1.Text = num.ToString(); } //ボタンのEnabledを変更する private void SetButtonEnabled(bool en) { Button1.Enabled = en; } ‥‥△C# ここまで△‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥‥ 最後にダイアログ使って進行状況を表示し、キャンセルできるように する例を紹介しようかと考えていたのですが、このコードがちょっと 長いため、ここに載せることができなくなってしまいました。このコー ドはその内に私のサイトで紹介します。 =============================== ■このマガジンの購読、購読中止、バックナンバー、説明に関しては  次のページをご覧ください。  http://www.mag2.com/m/0000104516.htm ■発行人・編集人:どぼん!  (Microsoft MVP for Visual Basic, Oct 2004-Oct 2005)  http://dobon.net  dobon_info@yahoo.co.jp ■ご質問等はメールではなく、掲示板へお願いいたします。  http://dobon.net/vb/bbs.html ■上記メールアドレスへのメールは確実に読まれる保障はありません  (スパム、ウィルス対策です)。メールは下記URLのフォームメール  から送信してください。  http://dobon.net/mail.html Copyright (c) 2003 - 2004 DOBON! All rights reserved. ===============================