.NETプログラミング研究 第44号 †
.NET Tips †
時間のかかる処理の進行状況を表示する †
大きなファイルを読み込んだり、大量のファイルをコピーするなどその処理に時間がかかるとき、ユーザーにアプリケーションがフリーズしたのではないかという心配をさせないように、処理の進行状況をメッセージやプログレスバーで表示する方法を説明します。
ここでは単純な例として、次のようなアプリケーションを作成します。WindowsフォームにLabelコントロール(Label1)とProgressBarコントロール(ProgressBar1)とButtonコントロール(Button1)を貼り付け、Button1をクリックすると1秒おきにLabel1とProgressBar1の内容を変えて(1から10までカウントアップする)表示されるようにします。
まずは、何も考えないで、Label.TextプロパティとProgressBar.Valueプロパティを変更し、それ以外何もしないでどのように表示されるか確かめてみましょう。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| | Private Sub Button1_Click(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles Button1.Click
ProgressBar1.Minimum = 1
ProgressBar1.Maximum = 10
Dim i As Integer
For i = 1 To 10
ProgressBar1.Value = i
Label1.Text = i.ToString()
System.Threading.Thread.Sleep(1000)
Next i
End Sub
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| | private void Button1_Click(object sender, System.EventArgs e)
{
ProgressBar1.Minimum = 1;
ProgressBar1.Maximum = 10;
for (int i = 1; i <= 10; i++)
{
ProgressBar1.Value = i;
Label1.Text = i.ToString();
System.Threading.Thread.Sleep(1000);
}
}
|
上記のコードを実行すると、ProgressBarは1秒おきにバーが伸びていきますが、Labelに表示される文字列は変化することなく、Button1_Clickイベントハンドラを抜けてはじめて"10"と表示されます。これは、ProgressBarコントロールがそのValueプロパティが変更されるとコントロールを再描画するのに対して、LabelコントロールはTextプロパティを変更してもそうしないためです。ほとんどのコントロールはLabelコントロールと同様の挙動となるでしょう。
ProgressBarコントロールのようにLabelコントロールでも1秒おきに内容を変更して表示するためには、Label.Textプロパティを設定後に、Updateメソッドを呼び出してコントロールを再描画するようにします。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| | Private Sub Button1_Click(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles Button1.Click
ProgressBar1.Minimum = 1
ProgressBar1.Maximum = 10
Dim i As Integer
For i = 1 To 10
ProgressBar1.Value = i
Label1.Text = i.ToString()
Label1.Update()
System.Threading.Thread.Sleep(1000)
Next i
End Sub
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| | private void Button1_Click(object sender, System.EventArgs e)
{
ProgressBar1.Minimum = 1;
ProgressBar1.Maximum = 10;
for (int i = 1; i <= 10; i++)
{
ProgressBar1.Value = i;
Label1.Text = i.ToString();
Label1.Update();
System.Threading.Thread.Sleep(1000);
}
}
|
進行状況を表示するだけであれば、このようにUpdateメソッドを使うのが最も簡単で、安全な方法です。しかしこの方法では処理の間、フォームやコントロールを一切操作することが出来なくなります。このような状況が好ましくなければ、Application.DoEventsメソッドを使うか、マルチスレッド化するという方法をとる必要があります。
Application.DoEventsメソッドを呼び出すと、キュー内で待機中のイベントを処理することができるため、次のようにループ内にDoEventsを入れることにより、フォームやコントロールが再描画されるのはもちろんのこと、フォームの移動やクリックなど、ユーザーによる操作も可能になります。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| | Private Sub Button1_Click(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles Button1.Click
ProgressBar1.Minimum = 1
ProgressBar1.Maximum = 10
Dim i As Integer
For i = 1 To 10
ProgressBar1.Value = i
Label1.Text = i.ToString()
Application.DoEvents()
System.Threading.Thread.Sleep(1000)
Next i
End Sub
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| | private void Button1_Click(object sender, System.EventArgs e)
{
ProgressBar1.Minimum = 1;
ProgressBar1.Maximum = 10;
for (int i = 1; i <= 10; i++)
{
ProgressBar1.Value = i;
Label1.Text = i.ToString();
Application.DoEvents();
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プロパティをいじっています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
| | Delegate Sub SetButtonEnabledDelegate(ByVal en As Boolean)
Delegate Sub SetProgressValueDelegate(ByVal num As Integer)
Dim thread As System.Threading.Thread
Private Sub Button1_Click(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles Button1.Click
ProgressBar1.Minimum = 1
ProgressBar1.Maximum = 10
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)
Me.Invoke(bdlg, New Object() {False})
Dim i As Integer
For i = 1 To 10
Me.Invoke(dlg, New Object() {i})
System.Threading.Thread.Sleep(1000)
Next i
Me.Invoke(bdlg, New Object() {True})
End Sub
Private Sub SetProgressValue(ByVal num As Integer)
ProgressBar1.Value = num
Label1.Text = num.ToString()
End Sub
Private Sub SetButtonEnabled(ByVal en As Boolean)
Button1.Enabled = en
End Sub
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
| | private delegate void SetButtonEnabledDelegate(bool en);
private delegate void SetProgressValueDelegate(int num);
private System.Threading.Thread thread;
private void Button1_Click(object sender, System.EventArgs e)
{
ProgressBar1.Minimum = 1;
ProgressBar1.Maximum = 10;
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);
this.Invoke(bdlg, new object[] {false});
for (int i = 1; i <= 10; i++)
{
this.Invoke(dlg, new object[] {i});
System.Threading.Thread.Sleep(1000);
}
this.Invoke(bdlg, new object[] {true});
}
private void SetProgressValue(int num)
{
ProgressBar1.Value = num;
Label1.Text = num.ToString();
}
private void SetButtonEnabled(bool en)
{
Button1.Enabled = en;
}
|
以上のように、Application.DoEventsメソッドを使う方法と、マルチスレッドによる方法は、Updateメソッドのみを使った方法と比べて、考慮すべき事柄が多く、危険性が高く、面倒であることがわかります。よってこれらの方法は初心者向きとは言えませんが、様々な局面(例えば、次に紹介するTips)においては有効な方法であることは間違いありません。
時間のかかる処理をユーザーが停止できるようにする †
次に、時間のかかる処理をユーザーがキャンセルボタンをクリックするなどして、途中で停止できるようにする方法を考えます。
先の「時間のかかる処理の進行状況を表示する」で考察した通り、Application.DoEventsメソッドを使うか、マルチスレッドを使うかということになるでしょう。
DoEventsメソッドを使用する場合は、キャンセルボタンがクリックされたらフラッグを立てるようにして、ループ内ではDoEventsメソッドを呼び出した後でフラッグをチェックし、フラッグが立っていればループを抜けるようにすればよいでしょう。
このようにして「時間のかかる処理の進行状況を表示する」のサンプルを改良してコードを以下に示します。キャンセルボタン(Button2)をフォームに追加し、Button2をクリックすると、カウントアップ処理が中止されるようにしています。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| | Private canceled As Boolean = False
Private Sub Button1_Click(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles Button1.Click
ProgressBar1.Minimum = 1
ProgressBar1.Maximum = 10
Button1.Enabled = False
Button2.Enabled = True
canceled = False
Dim i As Integer
For i = 1 To 10
ProgressBar1.Value = i
Label1.Text = i.ToString()
Application.DoEvents()
If canceled Then
MessageBox.Show(Me, "ユーザーにより中止されました。")
Exit For
End If
System.Threading.Thread.Sleep(1000)
Next i
Button1.Enabled = True
Button2.Enabled = False
End Sub
Private Sub Button2_Click(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles Button2.Click
canceled = True
End Sub
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| | private bool canceled = false;
private void Button1_Click(object sender, System.EventArgs e)
{
ProgressBar1.Minimum = 1;
ProgressBar1.Maximum = 10;
Button1.Enabled = false;
Button2.Enabled = true;
canceled = false;
for (int i = 1; i <= 10; i++)
{
ProgressBar1.Value = i;
Label1.Text = i.ToString();
Application.DoEvents();
if (canceled)
{
MessageBox.Show(this, "ユーザーにより中止されました。");
break;
}
System.Threading.Thread.Sleep(1000);
}
Button1.Enabled = true;
Button2.Enabled = false;
}
private void Button2_Click(object sender, System.EventArgs e)
{
canceled = true;
}
|
次はマルチスレッドを使った方法です。この場合、キャンセルボタンが押された時に、処理中のスレッドのThread.Abortメソッドを呼び出してスレッドを中止させればよいという考えもあるでしょう。しかしAbortメソッドを使って別のスレッドを終了させることは、どこで中断されるか予測できなく、それだけ危険といえます。よって、Abortメソッドはなるべく使わないようにして、フラッグを使って適当なタイミングでループを終了させる方法をお勧めします。
以下にこのような方法により、「時間のかかる処理の進行状況を表示する」で紹介したコードにキャンセルボタン(Button2)の処理を付けたコードを示します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
| | Private canceled As Boolean = False
Delegate Sub SetButtonEnabledDelegate(ByVal en As Boolean)
Delegate Sub SetProgressValueDelegate(ByVal num As Integer)
Private thread As System.Threading.Thread
Private Sub Button1_Click(ByVal sender As Object, _
ByVal e As System.EventArgs) Handles Button1.Click
ProgressBar1.Minimum = 1
ProgressBar1.Maximum = 10
thread = New System.Threading.Thread( _
New System.Threading.ThreadStart(AddressOf CountUp))
thread.IsBackground = True
thread.Start()
End Sub
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)
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})
System.Threading.Thread.Sleep(1000)
Next i
Me.Invoke(bdlg, New Object() {True})
End Sub
Private Sub SetProgressValue(ByVal num As Integer)
ProgressBar1.Value = num
Label1.Text = num.ToString()
End Sub
Private Sub SetButtonEnabled(ByVal en As Boolean)
Button1.Enabled = en
End Sub
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
| | private volatile bool canceled = false;
private delegate void SetButtonEnabledDelegate(bool en);
private delegate void SetProgressValueDelegate(int num);
private System.Threading.Thread thread;
private void Button1_Click(object sender, System.EventArgs e)
{
ProgressBar1.Minimum = 1;
ProgressBar1.Maximum = 10;
thread = new System.Threading.Thread(
new System.Threading.ThreadStart(CountUp));
thread.IsBackground = true;
thread.Start();
}
private void Button2_Click(object sender, System.EventArgs e)
{
canceled = true;
}
private void CountUp()
{
SetButtonEnabledDelegate bdlg =
new SetButtonEnabledDelegate(SetButtonEnabled);
SetProgressValueDelegate dlg =
new SetProgressValueDelegate(SetProgressValue);
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});
System.Threading.Thread.Sleep(1000);
}
this.Invoke(bdlg, new object[] {true});
}
private void SetProgressValue(int num)
{
ProgressBar1.Value = num;
Label1.Text = num.ToString();
}
private void SetButtonEnabled(bool en)
{
Button1.Enabled = en;
}
|
最後にダイアログ使って進行状況を表示し、キャンセルできるようにする例を紹介しようかと考えていたのですが、このコードがちょっと長いため、ここに載せることができなくなってしまいました。このコードはその内に私のサイトで紹介します。
コメント †