いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Windows Formsスレッド処理-002 (BackgroundWorker, Control.Invokeの使用)

Swingのスレッド処理にて、作成したアプリと同じ動作を行うC#アプリを作成してみました。
処理の書き方には、ほとんど違いがありませんね。作りやすいです。

てことで、以下サンプルです。

[Window表示時]

[開始ボタン押下時]

[キャンセルボタン押下時]

[ダウンロード完了時]


このアプリでは、以下の名称でコントロールが定義されています。

  • ダウンロード先のラベル:_downloadUrlLabel
  • 開始ボタン:_startButton
  • キャンセルボタン:_cancelButton
  • プログレスバー:_progressBar
  • ステータス表示ラベル:_statusLabel
  • BackgroundWorker:_worker
  • Timer: _timer
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Net;
using System.IO;

namespace Gsf.Samples.WinForms {

    /// <summary>
    /// BackgroundWorkerコンポーネントの動作確認用クラスです。
    /// 本アプリケーションは、開始ボタンが押下されるとJava EE 5のチュートリアルPDFの
    /// ダウンロードを行います。
    /// </summary>
    public partial class BackgroundWorkerSampleForm : Form {

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

        /// <summary>
        /// フォームが最初にロードされた際にコールバックされるメソッドです。
        /// </summary>
        /// <param name="sender">送信元コンポーネント</param>
        /// <param name="e">イベントオブジェクト</param>
        private void BackgroundWorkerSampleForm_Load(object sender, EventArgs e) {
            ChangeFormButtonState(true, false);
        }

        /// <summary>
        /// ボタンの状態を変更します。
        /// それぞれ、trueに設定すると有効化。falseだと無効化します。
        /// </summary>
        /// <param name="startButtonState">開始ボタンの状態</param>
        /// <param name="cancelButtonState">キャンセルボタンの状態</param>
        private void ChangeFormButtonState(bool startButtonState, bool cancelButtonState) {
            _startButton.Enabled  = startButtonState;
            _cancelButton.Enabled = cancelButtonState;
        }

        /// <summary>
        /// 開始ボタンが押下された際にコールバックされるメソッドです。
        /// </summary>
        /// <param name="sender">送信元オブジェクト</param>
        /// <param name="e">イベントオブジェクト</param>
        private void _startButton_Click(object sender, EventArgs e) {
            ChangeFormButtonState(false, true);

            _worker.RunWorkerAsync(_downloadUrlLabel.Text);
        }

        /// <summary>
        /// キャンセルボタンが押下された際にコールバックされるメソッドです。
        /// </summary>
        /// <param name="sender">送信元オブジェクト</param>
        /// <param name="e">イベントオブジェクト</param>
        private void _cancelButton_Click(object sender, EventArgs e) {
            _worker.CancelAsync();

            ChangeFormButtonState(true, false);
        }

        /// <summary>
        /// BackgroundWorkerコンポーネントにて、非同期で処理が行われる際にコールバックされるメソッドです。
        /// 該当ファイルのダウンロードを行います。
        /// </summary>
        /// <param name="sender">送信元オブジェクト</param>
        /// <param name="e">イベントオブジェクト</param>
        private void _worker_DoWork(object sender, DoWorkEventArgs e) {
            BackgroundWorker worker = sender as BackgroundWorker;

            if (worker.CancellationPending) {
                e.Cancel = true;
            }

            string url = e.Argument as string;

            worker.ReportProgress(10, "コネクションを確立しています・・・・");

            HttpWebRequest request = HttpWebRequest.Create(url) as HttpWebRequest;

            worker.ReportProgress(20, "コネクションを確立しました。");
            worker.ReportProgress(25, "ダウンロード中です・・・・・・");

            using (Stream stream = request.GetResponse().GetResponseStream()) {

                BinaryReader br = new BinaryReader(stream);

                //
                // TimerをONにする
                // (UIスレッド以外からのコンポーネント更新になるのでInvokeを使用。)
                // (匿名delegateはDelegate型ではないので、MethodInvokerにキャスト.)
                //
                Invoke((MethodInvoker)delegate() {
                    _timer.Enabled = true;
                });

                byte[] buffer = new byte[1024];
                while (br.Read(buffer, 0, buffer.Length) > 0) {

                    if (worker.CancellationPending) {
                        //
                        // キャンセルのイベントを発行するには、以下のようにCancelプロパティをtrueにする
                        //
                        e.Cancel = true;
                        return;
                    }

                    buffer = new byte[1024];

                }
            }

            worker.ReportProgress(100, "ダウンロードが完了しました。");

        }

        /// <summary>
        /// BackgroundWorkerコンポーネントにて、ReportProgressがコールされた際にコールバックされるメソッドです。
        /// ProgressBarへの進捗状況更新および、ステータスバーへのメッセージ表示を行います。
        /// </summary>
        /// <param name="sender">送信元オブジェクト</param>
        /// <param name="e">イベントオブジェクト</param>
        private void _worker_ProgressChanged(object sender, ProgressChangedEventArgs e) {
            _progressBar.Value = e.ProgressPercentage;

            if(e.UserState != null){
                string message = e.UserState as string;

                _statusLabel.Text = message;
            }
        }

        /// <summary>
        /// BackgroundWorkerコンポーネントにて、非同期処理完了時にコールバックされるメソッドです。
        /// 処理終了メッセージの表示および、連携して使用していたコンポーネントの後処理を行います。
        /// </summary>
        /// <param name="sender">送信元オブジェクト</param>
        /// <param name="e">イベントオブジェクト</param>
        private void _worker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e) {
            if(e.Cancelled){
                _statusLabel.Text = "処理がキャンセルされました。";
                _progressBar.Value = 0;
            } else if (e.Error != null) {
                _statusLabel.Text = "処理中にエラーが発生しました。";                
                MessageBox.Show(e.Error.Message);
                _progressBar.Value = 0;
            } else {
                _statusLabel.Text = "ダウンロードが完了しました。";
            }

            _timer.Enabled = false;

            ChangeFormButtonState(true, false);
        }

        /// <summary>
        /// TimerコンポーネントのTickイベントが発生した際にコールバックされるメソッドです。
        /// ProgressBarへの周期的な値のアップデートを行います。
        /// </summary>
        /// <param name="sender">送信元オブジェクト</param>
        /// <param name="e">イベントオブジェクト</param>
        private void _timer_Tick(object sender, EventArgs e) {
            if (_progressBar.Value == _progressBar.Maximum) {
                _progressBar.Value = 0;
            }

            _progressBar.Value++;
        }

    }

}

ちなみに、上記のコードで別スレッドからInvokeを行っている部分(TimerをEnableにする部分)を
Invokeを使用せずに直接、_timer.Enabled = trueと書くとうまく動きません。
DoWorkを実行しているのは、UIスレッドではないからです。そのため、DoWork内部でコンポーネント
更新をする場合は、Invokeのコールが必要となります。その他のイベント(ProgressChanged, RunWorkerCompleted)は
UIスレッドから実行されます。なので、この部分では直接コンポーネントを処理しても大丈夫です。


リソースとしては、以下のものを参考にしています。


キャンセル処理をする際に、自動で割り込みが発生するわけでなく自分でCancelプロパティを設定しなければ
いけないのかどうかがよくわかりません・・・・。
詳しい方いらっしゃいましたら、教えてください。m(_ _)m