#title(.NETプログラミング研究 第44号) #contents *.NETプログラミング研究 第44号 [#n06037ae] **.NET Tips [#n8c78676] ***時間のかかる処理の進行状況を表示する [#g0ca99f0] #column(注意){{ この記事の最新版は「[[時間のかかる処理の進行状況を表示する>http://dobon.net/vb/dotnet/programing/displayprogress.html]]」で公開しています。 }} 大きなファイルを読み込んだり、大量のファイルをコピーするなどその処理に時間がかかるとき、ユーザーにアプリケーションがフリーズしたのではないかという心配をさせないように、処理の進行状況をメッセージやプログレスバーで表示する方法を説明します。 ここでは単純な例として、次のようなアプリケーションを作成します。WindowsフォームにLabelコントロール(Label1)とProgressBarコントロール(ProgressBar1)とButtonコントロール(Button1)を貼り付け、Button1をクリックすると1秒おきにLabel1とProgressBar1の内容を変えて(1から10までカウントアップする)表示されるようにします。 まずは、何も考えないで、Label.TextプロパティとProgressBar.Valueプロパティを変更し、それ以外何もしないでどのように表示されるか確かめてみましょう。 #code(vbnet){{ '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 }} #code(csharp){{ //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); } } }} 上記のコードを実行すると、ProgressBarは1秒おきにバーが伸びていきますが、Labelに表示される文字列は変化することなく、Button1_Clickイベントハンドラを抜けてはじめて"10"と表示されます。これは、ProgressBarコントロールがそのValueプロパティが変更されるとコントロールを再描画するのに対して、LabelコントロールはTextプロパティを変更してもそうしないためです。ほとんどのコントロールはLabelコントロールと同様の挙動となるでしょう。 ProgressBarコントロールのようにLabelコントロールでも1秒おきに内容を変更して表示するためには、Label.Textプロパティを設定後に、Updateメソッドを呼び出してコントロールを再描画するようにします。 #code(vbnet){{ '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 }} #code(csharp){{ //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); } } }} 進行状況を表示するだけであれば、このようにUpdateメソッドを使うのが最も簡単で、安全な方法です。しかしこの方法では処理の間、フォームやコントロールを一切操作することが出来なくなります。このような状況が好ましくなければ、Application.DoEventsメソッドを使うか、マルチスレッド化するという方法をとる必要があります。 Application.DoEventsメソッドを呼び出すと、キュー内で待機中のイベントを処理することができるため、次のようにループ内にDoEventsを入れることにより、フォームやコントロールが再描画されるのはもちろんのこと、フォームの移動やクリックなど、ユーザーによる操作も可能になります。 #code(vbnet){{ '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 }} #code(csharp){{ //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); } } }} Application.DoEventsメソッド(あるいはマルチスレッドでも同じことが言えます)を使う時に注意すべきことは、DoEventsメソッドが呼び出された時に発生しては困るすべてのイベントを把握し、それらが発生しないように対策を施す必要があるということです。例えば上の例では、明らかにButton1_Clickイベントハンドラを処理中にもう一度Button1_Clickイベントハンドラが呼び出されてはいけません。よってこのようなことが起こらないように、ループの前にButton1のEnabledプロパティを無効にし、終了後に再び有効にするといった処理が必要になります。 また、DoEventsメソッドならではの欠点もあります。イベントが処理されるのはDoEventsメソッドが呼び出された時だけですので、例えば上記の例でユーザーがフォームの「閉じる」ボタンをクリックしてもすぐには閉じなかったり、タイミングによっては閉じなかったりします。 しかも、DoEventsメソッドが呼び出されイベントが処理される時、その処理がすべて終わるまでDoEventsメソッド以降の処理はブロックされます。例えば上記の例の場合、フォームをマウスで移動している間ループ処理は中断され、移動をやめてからようやくカウントアップが再開されます。 このようなDoEventsメソッドの欠点はマルチスレッドを使用することにより、解消されます。 .NETのマルチスレッドプログラミングについては、このメールマガジンの第19号から第26号まで散々やってきましたので、ここでは一切説明せず、いきなりサンプルを示します。このサンプルでは、カウントアップするメソッドをメインとは別のスレッドで処理しています。ここでは、Button1がカウントアップ中にクリックされないように、Button1のEnabledプロパティをいじっています。 #code(vbnet){{ 'ボタンの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 }} #code(csharp){{ //ボタンの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; } }} 以上のように、Application.DoEventsメソッドを使う方法と、マルチスレッドによる方法は、Updateメソッドのみを使った方法と比べて、考慮すべき事柄が多く、危険性が高く、面倒であることがわかります。よってこれらの方法は初心者向きとは言えませんが、様々な局面(例えば、次に紹介するTips)においては有効な方法であることは間違いありません。 ***時間のかかる処理をユーザーが停止できるようにする [#pe7c7d98] #column(注意){{ この記事の最新版は「[[時間のかかる処理をユーザーが停止できるようにする>http://dobon.net/vb/dotnet/programing/abortprocess.html]]」で公開しています。 }} 次に、時間のかかる処理をユーザーがキャンセルボタンをクリックするなどして、途中で停止できるようにする方法を考えます。 先の「時間のかかる処理の進行状況を表示する」で考察した通り、Application.DoEventsメソッドを使うか、マルチスレッドを使うかということになるでしょう。 DoEventsメソッドを使用する場合は、キャンセルボタンがクリックされたらフラッグを立てるようにして、ループ内ではDoEventsメソッドを呼び出した後でフラッグをチェックし、フラッグが立っていればループを抜けるようにすればよいでしょう。 このようにして「時間のかかる処理の進行状況を表示する」のサンプルを改良してコードを以下に示します。キャンセルボタン(Button2)をフォームに追加し、Button2をクリックすると、カウントアップ処理が中止されるようにしています。 #code(vbnet){{ 'キャンセルボタンがクリックされたか 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 }} #code(csharp){{ //キャンセルボタンがクリックされたか 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; } }} 次はマルチスレッドを使った方法です。この場合、キャンセルボタンが押された時に、処理中のスレッドのThread.Abortメソッドを呼び出してスレッドを中止させればよいという考えもあるでしょう。しかしAbortメソッドを使って別のスレッドを終了させることは、どこで中断されるか予測できなく、それだけ危険といえます。よって、Abortメソッドはなるべく使わないようにして、フラッグを使って適当なタイミングでループを終了させる方法をお勧めします。 以下にこのような方法により、「時間のかかる処理の進行状況を表示する」で紹介したコードにキャンセルボタン(Button2)の処理を付けたコードを示します。 #code(vbnet){{ 'キャンセルボタンがクリックされたか 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 }} #code(csharp){{ //キャンセルボタンがクリックされたか 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; } }} 最後にダイアログ使って進行状況を表示し、キャンセルできるようにする例を紹介しようかと考えていたのですが、このコードがちょっと長いため、ここに載せることができなくなってしまいました。このコードはその内に私のサイトで紹介します。 **コメント [#dd94c2f0] #comment //これより下は編集しないでください #pageinfo([[:Category/.NET]],2004-10-27 (水) 06:00:00,DOBON!,2010-03-21 (日) 19:42:45,DOBON!) |