[C#]なんでも実行ありのプログレスバーFormを作成する。

時間がかかる処理とか大量の処理がある場合、実行しているときはプログレスバー表示して、で、ついでに途中でキャンセルしたい、ああ、今なにしているかもみたい、、なんてのはよくある話です。

キャンセルボタンをつける場合、スレッド処理を考えますが、今のC#ではいろいろな実現方法が考えられます。
僕がはじめ作成していたのは、BackgroundWorkerクラスを使用して、あらかじめ実行する関数の型定義を固定(引数int,戻り値intなど)して決めておき、それを順次実行させる、というものでした。

もどかしかったのは実行させる関数定義が固定されているので、引数を追加したい場合、関係ない関数の定義を変えたり、また新たな関数定義を実行できるようにコーディングしたりしたところでした。
そこで、よく見かけるInvokeメソッドをヒントになんでも実行できるプログレスバーFormを作ってみた。

Control.Invokeメソッド定義

Control.Invoke (Delegate)
Control.Invoke (Delegate, Object[])

↑これを参考に次の関数をデリゲートと引数に分解する。
※Func<>,Action<>はDelegateのtypedefみたいなものです。

実行させたい関数定義の例

int Func1();                 // Delegate -> Func<int>,            object[] -> null
string Func2(int v);         // Delegate -> Func<string,int>,     object[] -> object[]{"string"}
int Func3(int v, string s);  // Delegate -> Func<int,string,int>, object[] -> object[]{123, "string"}
int Func4(ParamClass param); // Delegate -> Func<ParamClass,int>, object[] -> object[]{param}
void Func5();                // Delegate -> Action,               object[] -> null

やりたいこと
戻り値はint(0正常,0以外異常)だけど、引数可変の処理が幾つかがあるが、これをまるごとずばっとプログレスバーFormで処理したい。

MiniProgressForm.csコントロール

MiniProgressFormクラスソース

        /// <summary>
        /// 関数と引数とラベルを格納
        /// </summary>
        public List<Tuple<Delegate, object[],string>> FuncList { get; set; }

        /// プログレスバーに関するプロパティ値
        public ProgressBarStyle ProgBarStyle { get; set; }
        public bool CancelEnable { get; set; }
        public bool LabelEnable { get; set; }
        
        ///  キャンセル押下用
        private bool _cancel = false;

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public MiniProgressForm()
        {
            InitializeComponent();

            FuncList = new List<Tuple<Delegate, object[], string>>();
            CancelEnable = false;
            LabelEnable = false;

            /// ThreadTask実行中に閉じられると困るので操作させない。
            this.ControlBox = false;
        }

        /// <summary>
        /// 画面がロードされた際に、1度だけ実行される
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void MiniProgressForm_Load(object sender, EventArgs e)
        {
            button1.Enabled = CancelEnable;
            label1.Visible = LabelEnable;
            progressBar1.Style = ProgBarStyle;
            if (ProgBarStyle != ProgressBarStyle.Marquee)
            {
                progressBar1.Minimum = 0;
                progressBar1.Maximum = FuncList.Count;
            }

            /// 処理実行
            ThreadTask();
        }

        /// <summary>
        /// 処理を別スレッドで実行させる関数
        /// </summary>
        private void ThreadTask()
        {
            var task = Task.Factory.StartNew(() =>
            {
                ///
                /// スレッドからFormのコントロールを操作するためのデリゲートを定義
                ///

                /// Formを閉じる
                Action EndForm = () => this.Close();
                /// ラベルを変える
                Action<string> LabelChange = (s) => label1.Text = s;
                /// プログレスバーの進捗を変更する
                Action<int> ProgressChange = (p) => progressBar1.Value = p;

                ///
                /// 順次処理開始
                /// 
                for (int i = 0; i < FuncList.Count; i++)
                {
                    var funcSet = FuncList[i];
                    
                    /// ラベルを変更
                    if (LabelEnable)
                    {
                        this.Invoke(LabelChange, new object[] { funcSet.Item3 });
                    }

                    /// プログレスバーを変更
                    if (ProgBarStyle != ProgressBarStyle.Marquee)
                    {
                        this.Invoke(ProgressChange, new object[] { i });
                    }
                    
                    /// retには戻り値があるメソッドが値を返す。戻りがない関数(void型)を実行した場合nullになる。
                    /// FuncListに格納されているメソッドは必ず0正常,0以外異常の関数が格納されているとする
                    var ret = funcSet.Item1.DynamicInvoke(funcSet.Item2);
                    /// 戻りがnull以外で戻り値が0以外の時、エラー
                    if ((ret != null) && ((int)ret != 0))
                    {
                        MessageBox.Show("エラーが発生しました。");
                        break;
                    }

                    /// キャンセルボタンが押されていたら途中で終了する
                    if (_cancel)
                    {
                        MessageBox.Show("キャンセルされました");
                        break;
                    }
                }

                /// 処理が終了すると画面が閉じられる
                this.Invoke(EndForm);
            });

        }

        private void button1_Click(object sender, EventArgs e)
        {
            /// キャンセルボタンの押下フラグON
            _cancel = true;
        }
    }

さっそく使ってみる
次の3つの関数を実行させたい。

        /// <summary>
        /// 引数なし、int型の戻り値の関数
        /// </summary>
        /// <returns></returns>
        private int Func1()
        {
            System.Threading.Thread.Sleep(1);

            Trace.WriteLine("Func1>");

            return 0;
        }

        /// <summary>
        /// int型引数を1つ、int型の戻り値の関数
        /// </summary>
        /// <param name="val"></param>
        /// <returns></returns>
        private int Func2(int val)
        {
            System.Threading.Thread.Sleep(1);

            Trace.WriteLine("Func2>val = " + val.ToString());

            return 0;
        }

        /// <summary>
        /// string型引数を1つ、int型の戻り値の関数
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        private int Func3(string str)
        {
            System.Threading.Thread.Sleep(1);

            Trace.WriteLine("Func3>str = " + str.ToString());

            return 0;
        }

上記、3つの関数を実行している間、プログレスバーを表示する。

            /// プログレスバーFormのインスタンス
            var miniPrgBar = new MiniProgressForm();

            /// プログレスバーForm内で実行させるための関数、引数、実行時ラベルのセット
            List<Tuple<Delegate, object[], string>> SetFunc = new List<Tuple<Delegate, object[], string>>();

            /// いっぱい実行してみる
            for (int i = 0; i < 1000; i++)
            {
                /// 実行のポイント
                /// デリゲートはFunc<>,Action<>を使用して、自分が使いたい関数とその関数の引数を指定する。
                /// ちょうど、Invoke()メソッドと似た使い方ができる。
                /// ここでは、必ずint値を戻す関数をデリゲートとして定義するものとする
                /// Tupleを使用しているがクラスを定義することもできる。てかクラスのほうがわかりやすい。
                /// 例)
                /// class SetFunc
                /// {
                ///     public Delegate Method;
                ///     public object[] Params;
                ///     public string Label;
                /// }

                SetFunc.Add(Tuple.Create<Delegate, object[], string>(new Func<int>(() => Func1()), null, "Func1()を実行しています"));
                SetFunc.Add(Tuple.Create<Delegate, object[], string>(new Func<int, int>((p) => Func2(p)), new object[] { 123 }, "Func2()を実行しています"));
                SetFunc.Add(Tuple.Create<Delegate, object[], string>(new Func<string, int>((p) => Func3(p)), new object[] { "(--;" }, "Func3()を実行しています"));
                               
                
                SetFunc.Add(Tuple.Create<Delegate, object[], string>(new Func<int>(() => Func1()), null, "Func1()を実行しています"));
                SetFunc.Add(Tuple.Create<Delegate, object[], string>(new Func<int, int>((p) => Func2(p)), new object[] { 456 }, "Func2()を実行しています"));
                SetFunc.Add(Tuple.Create<Delegate, object[], string>(new Func<string, int>((p) => Func3(p)), new object[] { "(;;" }, "Func3()を実行しています"));
                SetFunc.Add(Tuple.Create<Delegate, object[], string>(new Func<int>(() => Func1()), null, "Func1()を実行しています"));
                SetFunc.Add(Tuple.Create<Delegate, object[], string>(new Func<int, int>((p) => Func2(p)), new object[] { 789 }, "Func2()を実行しています"));
                SetFunc.Add(Tuple.Create<Delegate, object[], string>(new Func<string, int>((p) => Func3(p)), new object[] { "(^^;" }, "Func3()を実行しています"));
            }

            ///
            /// プログレスバーFormの設定
            ///

            /// 実行関数をぶっこむ
            miniPrgBar.FuncList = SetFunc;

            /// プログレスバースタイルを指定
            miniPrgBar.ProgBarStyle = ProgressBarStyle.Continuous;

            /// ラベルの変更ありなし
            miniPrgBar.LabelEnable = true;

            /// 途中でキャンセルありなし
            miniPrgBar.CancelEnable = true;

            /// プログレスバーの実行
            miniPrgBar.ShowDialog();

        }


実行画面

今回作成したプロジェクトはこちら(自己責任で)
MiniProgress.zip