• 追加された行はこの色です。
  • 削除された行はこの色です。
#title(.NETプログラミング研究 第45号)

#navi(.NETプログラミング研究)

#contents

*.NETプログラミング研究 第45号 [#z31e25ab]

**.NET Tips [#vaa1db20]

***進行状況ダイアログを表示する [#qf3d3efd]

#column(注意){{
この記事の最新版は「[[進行状況ダイアログを表示する>http://dobon.net/vb/dotnet/programing/progressdialog.html]]」で公開しています。
この記事の最新版は「[[進行状況ダイアログを表示する>https://dobon.net/vb/dotnet/programing/progressdialog.html]]」で公開しています。
}}

前回は時間のかかる処理で進行状況を表示する方法と、さらにキャンセルボタンを付ける方法を紹介しました。その記事の最後でダイアログ使って進行状況を表示し、キャンセルできるようにするサンプルを私のサイトで紹介すると書きましたが、それをここで紹介します。

このような進行状況ダイアログを作成するには、前号で説明したとおり、Application.DoEventsメソッドを使うか、マルチスレッドを使うということになります。

DoEventsメソッドを使って進行状況ダイアログを表示する場合、前回紹介したような欠点があるだけでなく、別の問題が生じます。というのは、本来ならば進行状況ダイアログはShowDialogメソッドによりモーダルで表示したいところですが、モーダルで表示するとShowDialog以降のコードはダイアログが閉じられるまで実行されないため、進行状況を示したい時間のかかる処理はダイアログのクラス内に記述(あるいは、デリゲートやイベントを使用)しなければならなくなります。ダイアログを表示し、その直後に時間のかかる処理を記述できるようにするには、ダイアログはモードレスでメインフォームをオーナーとして表示し、メインフォームのEnabledプロパティをFalseにして操作できないようにするといったごまかしが必要になります。

ここではこのようなDoEventsメソッドを使った方法ではなく、マルチスレッドによる方法のコードのみを紹介します(DoEventsメソッドを使った方法は今までの応用で簡単にできるでしょう)。マルチスレッドによる方法では、上記のような問題はクリアされます。

下に進行状況ダイアログを表示するためのクラス(ProgressDialogクラス)のサンプルを示します。ここでProgressFormクラスはVisual Studio .NETのフォームデザイナで作成されたフォームで、プログレスバーとキャンセルボタン、さらにメッセージを表示するラベルが配置されており、進行状況ダイアログとして表示されるものですが、このフォームのShowDialogメソッドでダイアログを表示されるのではなく、ProgressDialogクラスを使って進行状況ダイアログを表示、操作します。

なお.NETのマルチスレッドプログラミングについて、ここでは一切説明しません。これらについて詳しくは、このメールマガジンの第19号から第26号等を参考にしてください。

(VB.NETのコードは、C#のコードを「C# to VB.NET Translator」を使って変換し、修正を加えたものです。C#のvolatileにあたる処理は省略しています。)

-[[C# to VB.NET Translator>http://authors.aspalliance.com/aldotnet/examples/translate.aspx]]

#code(vbnet){{
Public Class ProgressForm
    Inherits System.Windows.Forms.Form

#Region " Windows フォーム デザイナで生成されたコード "

    Public Sub New()
        MyBase.New()

        ' この呼び出しは Windows フォーム デザイナで必要です。
        InitializeComponent()

        ' InitializeComponent() 呼び出しの後に初期化を追加します。

    End Sub

    ' Form は、コンポーネント一覧に後処理を実行するために dispose をオーバーライドします。
    Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
        If disposing Then
            If Not (components Is Nothing) Then
                components.Dispose()
            End If
        End If
        MyBase.Dispose(disposing)
    End Sub

    ' Windows フォーム デザイナで必要です。
    Private components As System.ComponentModel.IContainer

    ' メモ : 以下のプロシージャは、Windows フォーム デザイナで必要です。
    'Windows フォーム デザイナを使って変更してください。  
    ' コード エディタを使って変更しないでください。
    Friend WithEvents Label1 As System.Windows.Forms.Label
    Friend WithEvents ProgressBar1 As System.Windows.Forms.ProgressBar
    Friend WithEvents Button1 As System.Windows.Forms.Button
    <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
        Me.Label1 = New System.Windows.Forms.Label
        Me.ProgressBar1 = New System.Windows.Forms.ProgressBar
        Me.Button1 = New System.Windows.Forms.Button
        Me.SuspendLayout()
        '
        'Label1
        '
        Me.Label1.Location = New System.Drawing.Point(8, 8)
        Me.Label1.Name = "Label1"
        Me.Label1.Size = New System.Drawing.Size(296, 48)
        Me.Label1.TabIndex = 0
        '
        'ProgressBar1
        '
        Me.ProgressBar1.Location = New System.Drawing.Point(8, 56)
        Me.ProgressBar1.Name = "ProgressBar1"
        Me.ProgressBar1.Size = New System.Drawing.Size(216, 23)
        Me.ProgressBar1.TabIndex = 1
        '
        'Button1
        '
        Me.Button1.Location = New System.Drawing.Point(232, 56)
        Me.Button1.Name = "Button1"
        Me.Button1.TabIndex = 2
        Me.Button1.Text = "キャンセル"
        '
        'ProgressForm
        '
        Me.AutoScaleBaseSize = New System.Drawing.Size(5, 12)
        Me.ClientSize = New System.Drawing.Size(320, 85)
        Me.Controls.Add(Me.Button1)
        Me.Controls.Add(Me.ProgressBar1)
        Me.Controls.Add(Me.Label1)
        Me.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog
        Me.MaximizeBox = False
        Me.MinimizeBox = False
        Me.Name = "ProgressForm"
        Me.Text = "ProgressForm"
        Me.ResumeLayout(False)

    End Sub

#End Region

End Class

''' <summary>
''' 進行状況ダイアログを表示するためのクラス
''' </summary>
Public Class ProgressDialog
    Implements IDisposable

    'キャンセルボタンがクリックされたか
    Private _canceled As Boolean = False
    'ダイアログフォーム
    Private form As ProgressForm
    'フォームが表示されるまで待機するための待機ハンドル
    Private startEvent As System.Threading.ManualResetEvent
    'フォームが一度表示されたか
    Private showed As Boolean = False
    'フォームをコードで閉じているか
    Private closing As Boolean = False
    'オーナーフォーム
    Private ownerForm As form

    '別処理をするためのスレッド
    Private thread As System.Threading.Thread

    'フォームのタイトル
    Private _title As String = "進行状況"
    'ProgressBarの最小、最大、現在の値
    Private _minimum As Integer = 0
    Private _maximum As Integer = 100
    Private _value As Integer = 0
    '表示するメッセージ
    Private _message As String = ""

    ''' <summary>
    ''' ダイアログのタイトルバーに表示する文字列
    ''' </summary>
    Public Property Title() As String
        Get
            Return _message
        End Get
        Set(ByVal Value As String)
            _title = Value
            If Not (form Is Nothing) Then
                form.Invoke(New MethodInvoker(AddressOf SetTitle))
            End If
        End Set
    End Property

    ''' <summary>
    ''' プログレスバーの最小値
    ''' </summary>
    Public Property Minimum() As Integer
        Get
            Return _minimum
        End Get
        Set(ByVal Value As Integer)
            _minimum = Value
            If Not (form Is Nothing) Then
                form.Invoke(New MethodInvoker( _
                    AddressOf SetProgressMinimum))
            End If
        End Set
    End Property

    ''' <summary>
    ''' プログレスバーの最大値
    ''' </summary>
    Public Property Maximum() As Integer
        Get
            Return _maximum
        End Get
        Set(ByVal Value As Integer)
            _maximum = Value
            If Not (form Is Nothing) Then
                form.Invoke(New MethodInvoker( _
                    AddressOf SetProgressMaximun))
            End If
        End Set
    End Property

    ''' <summary>
    ''' プログレスバーの値
    ''' </summary>
    Public Property Value() As Integer
        Get
            Return _value
        End Get
        Set(ByVal Value As Integer)
            _value = Value
            If Not (form Is Nothing) Then
                form.Invoke(New MethodInvoker( _
                    AddressOf SetProgressValue))
            End If
        End Set
    End Property

    ''' <summary>
    ''' ダイアログに表示するメッセージ
    ''' </summary>
    Public Property Message() As String
        Get
            Return _message
        End Get
        Set(ByVal Value As String)
            _message = Value
            If Not (form Is Nothing) Then
                form.Invoke(New MethodInvoker(AddressOf SetMessage))
            End If
        End Set
    End Property

    ''' <summary>
    ''' キャンセルされたか
    ''' </summary>
    Public ReadOnly Property Canceled() As Boolean
        Get
            Return _canceled
        End Get
    End Property

    ''' <summary>
    ''' ダイアログを表示する
    ''' </summary>
    ''' <param name="owner">
    ''' ownerの中央にダイアログが表示される
    ''' </param>
    ''' <remarks>
    ''' このメソッドは一回しか呼び出せません。
    ''' </remarks>
    Public Overloads Sub Show(ByVal owner As form)
        If showed Then
            Throw New Exception("ダイアログは一度表示されています。")
        End If
        showed = True

        _canceled = False
        startEvent = New System.Threading.ManualResetEvent(False)
        ownerForm = owner

        'スレッドを作成
        thread = New System.Threading.Thread( _
            New System.Threading.ThreadStart(AddressOf Run))
        thread.IsBackground = True
        Me.thread.ApartmentState = System.Threading.ApartmentState.STA
        thread.Start()

        'フォームが表示されるまで待機する
        startEvent.WaitOne()
    End Sub

    Public Overloads Sub Show()
        Show(Nothing)
    End Sub

    '別スレッドで処理するメソッド
    Private Sub Run()
        'フォームの設定
        form = New ProgressForm
        form.Text = _title
        AddHandler form.Button1.Click, AddressOf Button1_Click
        AddHandler form.Closing, AddressOf form_Closing
        AddHandler form.Activated, AddressOf form_Activated
        form.ProgressBar1.Minimum = _minimum
        form.ProgressBar1.Maximum = _maximum
        form.ProgressBar1.Value = _value
        'フォームの表示位置をオーナーの中央へ
        If Not (ownerForm Is Nothing) Then
            form.StartPosition = FormStartPosition.Manual
            form.Left = _
                ownerForm.Left + (ownerForm.Width - form.Width) \ 2
            form.Top = _
                ownerForm.Top + (ownerForm.Height - form.Height) \ 2
        End If
        'フォームの表示
        form.ShowDialog()

        form.Dispose()
    End Sub

    ''' <summary>
    ''' ダイアログを閉じる
    ''' </summary>
    Public Sub Close()
        closing = True
        form.Invoke(New MethodInvoker(AddressOf form.Close))
    End Sub 'Close

    Public Sub Dispose() Implements System.IDisposable.Dispose
        form.Invoke(New MethodInvoker(AddressOf form.Dispose))
    End Sub

    Private Sub SetProgressValue()
        If Not (form Is Nothing) And Not form.IsDisposed Then
            form.ProgressBar1.Value = _value
        End If
    End Sub

    Private Sub SetMessage()
        If Not (form Is Nothing) And Not form.IsDisposed Then
            form.Label1.Text = _message
        End If
    End Sub

    Private Sub SetTitle()
        If Not (form Is Nothing) And Not form.IsDisposed Then
            form.Text = _title
        End If
    End Sub

    Private Sub SetProgressMaximun()
        If Not (form Is Nothing) And Not form.IsDisposed Then
            form.ProgressBar1.Maximum = _maximum
        End If
    End Sub

    Private Sub SetProgressMinimum()
        If Not (form Is Nothing) And Not form.IsDisposed Then
            form.ProgressBar1.Minimum = _minimum
        End If
    End Sub

    Private Sub Button1_Click(ByVal sender As Object, _
        ByVal e As EventArgs)
        _canceled = True
    End Sub

    Private Sub form_Closing(ByVal sender As Object, _
        ByVal e As System.ComponentModel.CancelEventArgs)
        If Not closing Then
            e.Cancel = True
            _canceled = True
        End If
    End Sub

    Private Sub form_Activated(ByVal sender As Object, _
        ByVal e As EventArgs)
        RemoveHandler form.Activated, AddressOf form_Activated
        startEvent.Set()
    End Sub
End Class
}}

#code(csharp){{
using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;

/// <summary>
/// ProgressForm の概要の説明です。
/// </summary>
public class ProgressForm : System.Windows.Forms.Form
{
    internal System.Windows.Forms.Label Label1;
    internal System.Windows.Forms.ProgressBar ProgressBar1;
    internal System.Windows.Forms.Button Button1;
    /// <summary>
    /// 必要なデザイナ変数です。
    /// </summary>
    private System.ComponentModel.Container components = null;

    public ProgressForm()
    {
        //
        // Windows フォーム デザイナ サポートに必要です。
        //
        InitializeComponent();

        //
        // TODO: InitializeComponent 呼び出しの後に、コンストラクタ コードを追加してください。
        //
    }

    /// <summary>
    /// 使用されているリソースに後処理を実行します。
    /// </summary>
    protected override void Dispose( bool disposing )
    {
        if( disposing )
        {
            if(components != null)
            {
                components.Dispose();
            }
        }
        base.Dispose( disposing );
    }

    #region Windows フォーム デザイナで生成されたコード 
    /// <summary>
    /// デザイナ サポートに必要なメソッドです。このメソッドの内容を
    /// コード エディタで変更しないでください。
    /// </summary>
    private void InitializeComponent()
    {
        this.Label1 = new System.Windows.Forms.Label();
        this.ProgressBar1 = new System.Windows.Forms.ProgressBar();
        this.Button1 = new System.Windows.Forms.Button();
        this.SuspendLayout();
        // 
        // Label1
        // 
        this.Label1.Location = new System.Drawing.Point(16, 8);
        this.Label1.Name = "Label1";
        this.Label1.Size = new System.Drawing.Size(288, 40);
        this.Label1.TabIndex = 0;
        // 
        // ProgressBar1
        // 
        this.ProgressBar1.Location = new System.Drawing.Point(8, 48);
        this.ProgressBar1.Name = "ProgressBar1";
        this.ProgressBar1.Size = new System.Drawing.Size(216, 23);
        this.ProgressBar1.TabIndex = 1;
        // 
        // Button1
        // 
        this.Button1.Location = new System.Drawing.Point(232, 48);
        this.Button1.Name = "Button1";
        this.Button1.TabIndex = 2;
        this.Button1.Text = "キャンセル";
        // 
        // ProgressForm
        // 
        this.AutoScaleBaseSize = new System.Drawing.Size(5, 12);
        this.ClientSize = new System.Drawing.Size(320, 85);
        this.Controls.Add(this.Button1);
        this.Controls.Add(this.ProgressBar1);
        this.Controls.Add(this.Label1);
        this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
        this.MaximizeBox = false;
        this.MinimizeBox = false;
        this.Name = "ProgressForm";
        this.ShowInTaskbar = false;
        this.Text = "ProgressForm";
        this.ResumeLayout(false);

    }
    #endregion
}

/// <summary>
/// 進行状況ダイアログを表示するためのクラス
/// </summary>
public class ProgressDialog : IDisposable
{
    //キャンセルボタンがクリックされたか
    private volatile bool _canceled = false;
    //ダイアログフォーム
    private volatile ProgressForm form;
    //フォームが表示されるまで待機するための待機ハンドル
    private System.Threading.ManualResetEvent startEvent;
    //フォームが一度表示されたか
    private bool showed = false;
    //フォームをコードで閉じているか
    private volatile bool closing = false;
    //オーナーフォーム
    private Form ownerForm;

    //別処理をするためのスレッド
    private System.Threading.Thread thread;

    //フォームのタイトル
    private volatile string _title = "進行状況";
    //ProgressBarの最小、最大、現在の値
    private volatile int _minimum = 0;
    private volatile int _maximum = 100;
    private volatile int _value = 0;
    //表示するメッセージ
    private volatile string _message = "";

    /// <summary>
    /// ダイアログのタイトルバーに表示する文字列
    /// </summary>
    public string Title
    {
        set
        {
            _title = value;
            if (form != null)
                form.Invoke(new MethodInvoker(SetTitle));
        }
        get
        {
            return _message;
        }
    }
    
    /// <summary>
    /// プログレスバーの最小値
    /// </summary>
    public int Minimum
    {
        set
        {
            _minimum = value;
            if (form != null)
                form.Invoke(new MethodInvoker(SetProgressMinimum));
        }
        get
        {
            return _minimum;
        }
    }

    /// <summary>
    /// プログレスバーの最大値
    /// </summary>
    public int Maximum
    {
        set
        {
            _maximum = value;
            if (form != null)
                form.Invoke(new MethodInvoker(SetProgressMaximun));
        }
        get
        {
            return _maximum;
        }
    }

    /// <summary>
    /// プログレスバーの値
    /// </summary>
    public int Value
    {
        set
        {
            _value = value;
            if (form != null)
                form.Invoke(new MethodInvoker(SetProgressValue));
        }
        get
        {
            return _value;
        }
    }

    /// <summary>
    /// ダイアログに表示するメッセージ
    /// </summary>
    public string Message
    {
        set
        {
            _message = value;
            if (form != null)
                form.Invoke(new MethodInvoker(SetMessage));
        }
        get
        {
            return _message;
        }
    }

    /// <summary>
    /// キャンセルされたか
    /// </summary>
    public bool Canceled
    {
        get { return _canceled; }
    }

    /// <summary>
    /// ダイアログを表示する
    /// </summary>
    /// <param name="owner">
    /// ownerの中央にダイアログが表示される
    /// </param>
    /// <remarks>
    /// このメソッドは一回しか呼び出せません。
    /// </remarks>
    public void Show(Form owner)
    {
        if (showed)
            throw new Exception("ダイアログは一度表示されています。");
        showed = true;

        _canceled = false;
        startEvent = new System.Threading.ManualResetEvent(false);
        ownerForm = owner;

        //スレッドを作成
        thread = new System.Threading.Thread(
            new System.Threading.ThreadStart(Run));
        thread.IsBackground = true;
        this.thread.ApartmentState =
            System.Threading.ApartmentState.STA;
        thread.Start();

        //フォームが表示されるまで待機する
        startEvent.WaitOne();
    }
    public void Show()
    {
        Show(null);
    }

    //別スレッドで処理するメソッド
    private void Run()
    {
        //フォームの設定
        form = new ProgressForm();
        form.Text = _title;
        form.Button1.Click += new EventHandler(Button1_Click);
        form.Closing += new CancelEventHandler(form_Closing);
        form.Activated += new EventHandler(form_Activated);
        form.ProgressBar1.Minimum = _minimum;
        form.ProgressBar1.Maximum = _maximum;
        form.ProgressBar1.Value = _value;
        //フォームの表示位置をオーナーの中央へ
        if (ownerForm != null)
        {
            form.StartPosition = FormStartPosition.Manual;
            form.Left =
                ownerForm.Left + (ownerForm.Width - form.Width) / 2;
            form.Top =
                ownerForm.Top + (ownerForm.Height - form.Height) / 2;
        }
        //フォームの表示
        form.ShowDialog();

        form.Dispose();
    }

    /// <summary>
    /// ダイアログを閉じる
    /// </summary>
    public void Close()
    {
        closing = true;
        form.Invoke(new MethodInvoker(form.Close));
    }

    public void Dispose()
    {
        form.Invoke(new MethodInvoker(form.Dispose));
    }

    private void SetProgressValue()
    {
        if (form != null && !form.IsDisposed)
            form.ProgressBar1.Value = _value;
    }

    private void SetMessage()
    {
        if (form != null && !form.IsDisposed)
            form.Label1.Text = _message;
    }

    private void SetTitle()
    {
        if (form != null && !form.IsDisposed)
            form.Text = _title;
    }

    private void SetProgressMaximun()
    {
        if (form != null && !form.IsDisposed)
            form.ProgressBar1.Maximum = _maximum;
    }

    private void SetProgressMinimum()
    {
        if (form != null && !form.IsDisposed)
            form.ProgressBar1.Minimum = _minimum;
    }

    private void Button1_Click(object sender, EventArgs e)
    {
        _canceled = true;
    }

    private void form_Closing(object sender, CancelEventArgs e)
    {
        if (!closing)
        {
            e.Cancel = true;
            _canceled = true;
        }
    }

    private void form_Activated(object sender, EventArgs e)
    {
        form.Activated -= new EventHandler(form_Activated);
        startEvent.Set();
    }
}
}}

次にこのクラスの使用法を示します。このコードはメインフォームから呼び出されるものとします。

#code(vbnet){{
Dim pd As New ProgressDialog
'ダイアログのタイトルを設定
pd.Title = "カウントアップ"
'プログレスバーの最小値を設定
pd.Minimum = 0
'プログレスバーの最大値を設定
pd.Maximum = 10
'プログレスバーの初期値を設定
pd.Value = 0

'進行状況ダイアログを表示する
pd.Show(Me)

'処理を開始
Dim i As Integer
For i = 1 To 10
    'プログレスバーの値を変更する
    pd.Value = i
    'メッセージを変更する
    pd.Message = i.ToString() + "番目を処理中..."

    'キャンセルされた時はループを抜ける
    If pd.Canceled Then
        Exit For
    End If

    '1秒間待機する(本来なら何らかの処理を行う)
    System.Threading.Thread.Sleep(1000)
Next i

'ダイアログを閉じる
pd.Close()
}}

#code(csharp){{
ProgressDialog pd = new ProgressDialog();
//ダイアログのタイトルを設定
pd.Title = "カウントアップ";
//プログレスバーの最小値を設定
pd.Minimum = 0;
//プログレスバーの最大値を設定
pd.Maximum = 10;
//プログレスバーの初期値を設定
pd.Value = 0;

//進行状況ダイアログを表示する
pd.Show(this);

//処理を開始
for (int i = 1; i <= 10; i++)
{
    //プログレスバーの値を変更する
    pd.Value = i;
    //メッセージを変更する
    pd.Message = i.ToString() + "番目を処理中...";

    //キャンセルされた時はループを抜ける
    if (pd.Canceled)
        break;

    //1秒間待機する(本来なら何らかの処理を行う)
    System.Threading.Thread.Sleep(1000);
}

//ダイアログを閉じる
pd.Close();
}}

ここで紹介した方法では、メインフォームと同じスレッドで時間のかかる処理をしているため、ダイアログが表示されている間、メインフォームは基本的に再描画されません。この問題は、時間のかかる処理を別スレッドにすることにより解決できます。この方法により進行状況ダイアログを表示するサンプルが、下の「参考」の「The Code Project - A .NET Progress Dialog」で紹介されています。

参考:

-[[Windows Forms FAQ | 14.1 How to display a status dialog in a background thread during a long operation and alow the user to cancel?>http://www.syncfusion.com/faq/winforms/search/490.asp]]
-[[The Code Project - A .NET Progress Dialog>http://www.codeproject.com/cs/miscctrl/progressdialog.asp]]

**コメント [#nda38e15]
#comment

//これより下は編集しないでください
#pageinfo([[:Category/.NET]],2004-11-09 (火) 06:00:00,DOBON!,2010-03-21 (日) 19:44:21,DOBON!)

[ トップ ]   [ 新規 | 子ページ作成 | 一覧 | 単語検索 | 最終更新 | ヘルプ ]