進捗状況画面の表示その2

dobon.net さんの記事で、進行状況ダイアログを表示するという記事があるのですが、.NET Framework 1.1版のものと、.NET Framework 2.0版のもので、操作仕様が違っています。Thread と BackgroundWorker の違いもありますが、操作性は、.NET Framework 1.1版のものの方が分かりやすいかなぁと個人的には思っていました。

というわけで、以下サンプルです。細かい差異はありますが大体の流れは同じです。

やっぱり非同期関連は難しいです。Thread と ManualResetEvent、Async/Await と Task、UI スレッドと別スレッド、フォアスレッドとバッググラウンドスレッド、ShowDialog() しつつ次の行以降の処理を進めるとか、よくわからなくなってきますね。

目次

進捗ダイアログ画面(ProgressForm)

デザイン

f:id:sutefu7:20200305235801p:plain

ソースコード

using System;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    /// <summary>
    /// 進行状況ダイアログを表示するためのクラスです。
    /// </summary>
    public partial class ProgressForm : Form
    {
        /// <summary>
        /// コンストラクタです。画面デザイン上でセットしてしまった方がいいかも。
        /// </summary>
        public ProgressForm()
        {
            InitializeComponent();


            //
            label1.Text = string.Empty;

            //
            button1.Text = "キャンセル";

            //
            FormBorderStyle = FormBorderStyle.FixedDialog;
            MaximizeBox = false;
            MinimizeBox = false;
            ShowInTaskbar = false;
            Text = "進捗状況";
        }

        // コントロールを公開したくないので(private から internal に変えてもいいんだけど、なんとなく)
        // 操作メソッドを準備・公開しておく

        //
        public void SetTitle(string value)
        {
            Text = value;
        }

        public void SetMessage(string value)
        {
            label1.Text = value;
        }

        //
        public void SetProgressMaximum(int value)
        {
            progressBar1.Maximum = value;
        }

        public void SetProgressMinimum(int value)
        {
            progressBar1.Minimum = value;
        }

        public void SetProgressValue(int value)
        {
            progressBar1.Value = value;
        }

        //
        public void AddButtonEvent(EventHandler action)
        {
            button1.Click += action;
        }
    }
}

進捗ダイアログ画面を操作するクラス(ProgressDialog)

ソースコード

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace WindowsFormsApp1
{
    /// <summary>
    /// 進行状況ダイアログを操作するためのクラスです。
    /// </summary>
    public class ProgressDialog : IDisposable
    {
        //
        private volatile bool _IsCanceled = false;
        private volatile bool _IsShown = false;
        private volatile bool _IsClosing = false;
        private volatile ProgressForm _ProgressForm = null;
        private Form OwnerForm = null;

        private volatile string _Title = "進捗状況";
        private volatile int _Minimum = 0;
        private volatile int _Maximum = 100;
        private volatile int _Value = 0;
        private volatile string _Message = string.Empty;



        /// <summary>
        /// ダイアログのタイトルバーに表示する文字列 を取得、または設定します。
        /// </summary>
        public string Title
        {
            get
            {
                return _Title;
            }
            set
            {
                _Title = value;
                InvokeForm(new Action(SetTitle));
            }
        }

        /// <summary>
        /// ダイアログに表示するメッセージ を取得、または設定します。
        /// </summary>
        public string Message
        {
            get
            {
                return _Message;
            }
            set
            {
                _Message = value;
                InvokeForm(new Action(SetMessage));
            }
        }

        /// <summary>
        /// プログレスバーの最小値 を取得、または設定します。
        /// </summary>
        public int Minimum
        {
            get
            {
                return _Minimum;
            }
            set
            {
                _Minimum = value;
                InvokeForm(new Action(SetProgressMinimum));
            }
        }

        /// <summary>
        /// プログレスバーの最大値 を取得、または設定します。
        /// </summary>
        public int Maximum
        {
            get
            {
                return _Maximum;
            }
            set
            {
                _Maximum = value;
                InvokeForm(new Action(SetProgressMaximum));
            }
        }

        /// <summary>
        /// プログレスバーの現在値 を取得、または設定します。
        /// </summary>
        public int Value
        {
            get
            {
                return _Value;
            }
            set
            {
                _Value = value;
                InvokeForm(new Action(SetProgressValue));
            }
        }

        /// <summary>
        /// キャンセルボタンを押したかどうか を取得します。
        /// </summary>
        public bool IsCanceled
        {
            get
            {
                return _IsCanceled;
            }
        }



        /// <summary>
        /// ダイアログを表示します。
        /// </summary>
        /// <returns></returns>
        public async Task Show()
        {
            await Show(null);
        }

        /// <summary>
        /// ダイアログを表示します。
        /// </summary>
        /// <param name="owner"></param>
        /// <remarks>
        /// このメソッドは一回しか呼び出せません。
        /// </remarks>
        /// <returns></returns>
        public async Task Show(Form owner)
        {
            if (_IsShown)
                throw new InvalidOperationException("ダイアログは一度表示されています。");

            _IsShown = true;
            _IsCanceled = false;
            OwnerForm = owner;

            await Task.Run(() => Run());
        }

        public void Close()
        {
            _IsClosing = true;
            InvokeForm(new Action(_ProgressForm.Close));
        }

        public void Dispose()
        {
            _IsClosing = true;
            InvokeForm(new Action(_ProgressForm.Dispose));
        }



        //
        private bool IsAliveProgressForm()
        {
            return (_ProgressForm != null && !_ProgressForm.IsDisposed);
        }

        private void InvokeForm(Action action)
        {
            if (IsAliveProgressForm())
            {
                _ProgressForm.Invoke(action);
            }
        }

        //
        private void SetTitle()
        {
            if (IsAliveProgressForm())
                _ProgressForm.SetTitle(_Title);
        }

        private void SetMessage()
        {
            if (IsAliveProgressForm())
                _ProgressForm.SetMessage(_Message);
        }

        //
        private void SetProgressMinimum()
        {
            if (IsAliveProgressForm())
                _ProgressForm.SetProgressMinimum(_Minimum);
        }

        private void SetProgressMaximum()
        {
            if (IsAliveProgressForm())
                _ProgressForm.SetProgressMaximum(_Maximum);
        }

        private void SetProgressValue()
        {
            if (IsAliveProgressForm())
                _ProgressForm.SetProgressValue(_Value);
        }

        //
        private void Run()
        {
            // System.InvalidOperationException: 有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール 'Form1' がアクセスされました。
            var dummy = Application.OpenForms[0];
            if (dummy.InvokeRequired)
            {
                var action = new Action(Run);
                //dummy.Invoke(action, new object[] { });    // UI スレッド上で ShowDialog() なので、閉じるまで停止してしまう
                dummy.BeginInvoke(action, new object[] { }); // UI スレッド上で非同期で実行するから?ShowDialog() でも止まらない。なんでうまくいくんだったか忘れた...orz
                return;
            }

            //
            _ProgressForm = new ProgressForm();
            //_ProgressForm.SetTitle(Title);
            _ProgressForm.Text = Title;

            if (OwnerForm != null)
            {
                //_ProgressForm.StartPosition = FormStartPosition.Manual;
                //_ProgressForm.Left = OwnerForm.Left + (OwnerForm.Width - _ProgressForm.Width) / 2;
                //_ProgressForm.Top = OwnerForm.Top + (OwnerForm.Height - _ProgressForm.Height) / 2;
                _ProgressForm.Owner = OwnerForm;
                _ProgressForm.StartPosition = FormStartPosition.CenterParent;
            }

            _ProgressForm.FormClosing += (sender, e) =>
            {
                if (!_IsClosing)
                {
                    e.Cancel = true;
                    _IsCanceled = true;
                }
            };

            //
            _ProgressForm.SetProgressMinimum(Minimum);
            _ProgressForm.SetProgressMaximum(Maximum);
            _ProgressForm.SetProgressValue(Value);

            _ProgressForm.AddButtonEvent(new EventHandler((sender, e) =>
            {
                _IsCanceled = true;
            }));

            _ProgressForm.ShowDialog();

            if (!_ProgressForm.IsDisposed)
                _ProgressForm.Dispose();
        }
    }
}

操作する側(Form1)

ソースコード

using System;
using System.Threading.Tasks;
using System.Windows.Forms;

/*
 * Button を配置しています。
 * 
 */

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private async void button1_Click(object sender, EventArgs e)
        {
            using (var dlg = new ProgressDialog())
            {
                dlg.Title = "カウントアップ";
                dlg.Minimum = 0;
                dlg.Maximum = 10;
                dlg.Value = 0;
                await dlg.Show(this);

                for (var i = 0; i < 10; i++)
                {
                    dlg.Value = i + 1;
                    dlg.Message = $"{i + 1} 番目を処理中...";

                    if (dlg.IsCanceled)
                        break;

                    await Task.Delay(1000);
                }

                if (dlg.IsCanceled)
                    Text = "Canceled.";
                else
                    Text = "Completed!";
            }
        }
    }
}