1
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

BackgroundWorkerを使った進捗表示画面の実装

Last updated at Posted at 2021-05-16

初めに

ファイルの読み込みや大量のループといった重い処理をする際は、進捗を表示してあげることがUI的にもよろしいです。
画面ごとにプログレスバーを実装して表示してもよいのですが、Backgroundworkerを使うことで「進捗を表示する画面」を処理と分離して実装できると分かったのでまとめました。

TL;DR

以下のように表示側、呼び出し側のコードを実装します。
VisualStudioの自動生成分は省略してます。

表示側

public partial class ProgressForm : Form
{

    private BackgroundWorker backgroundWorker;

    public ProgressForm(BackgroundWorker bw)
    {
        InitializeComponent();
        this.backgroundWorker = bw;
        if (!bw.WorkerSupportsCancellation)
        {
            //キャンセル
            this.cancelBtn.Enabled = false;
            this.cancelBtn.Visible = false;
            this.ControlBox = false;
        }
        backgroundWorker.ProgressChanged += BackgroundWorker_ProgressChanged;
        backgroundWorker.RunWorkerCompleted += BackgroundWorker_RunWorkerCompleted;            
    }

    //完了時の挙動
    private void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if((e.Error is null) == false)
        {
            //エラーの表示等の処理
            Console.WriteLine("error");
            DialogResult = DialogResult.Cancel;
        }
        else if (e.Cancelled)
        {
            //キャンセル時の処理
            Console.WriteLine("canceled");
            DialogResult = DialogResult.Cancel;                
        }
        else
        {
            //完了時の処理
            Console.WriteLine("completed");
            DialogResult = DialogResult.OK;
        }
        //フォームを閉じる
        this.Close();
    }

    //進捗の表示
    private void BackgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        //プログレスバーの値を変更する
        //設定されてる値の範囲外の値を渡すとエラーが発生することに注意
        if (e.ProgressPercentage < this.progressBar1.Minimum)
        {
            this.progressBar1.Value = this.progressBar1.Minimum;
        }
        else if (this.progressBar1.Maximum < e.ProgressPercentage)
        {
            this.progressBar1.Value = this.progressBar1.Maximum;
        }
        else
        {
            this.progressBar1.Value = e.ProgressPercentage;
        }
        //UserStateの内容を表示する
        progressLabel.Text = e.UserState.ToString();
    }

    private void ProgressForm_Shown(object sender, EventArgs e)
    {
        //画面表示を開始した際に動いてない場合はRunWorkerAsync()を呼ぶ
        //二重に叩かないように判定を入れる
        if (!backgroundWorker.IsBusy)
        { 
            backgroundWorker.RunWorkerAsync();
        }
    }

    private void cancelBtn_Click(object sender, EventArgs e)
    {
        //処理をキャンセルする
        if (backgroundWorker.IsBusy)
        {
            this.backgroundWorker.CancelAsync();
        }
    }

    private void ProgressForm_FormClosing(object sender, FormClosingEventArgs e)
    {
        //×ボタンで閉じられた際の対処
        //backgroundWorkerが動いており
        if (backgroundWorker.IsBusy && !backgroundWorker.CancellationPending)
        {
            //一度Closingをキャンセルしたうえで、改めてキャンセル処理をする。
            e.Cancel = true;
            backgroundWorker.CancelAsync();                
        }
    }

    private void ProgressForm_FormClosed(object sender, FormClosedEventArgs e)
    {            
        //フォーム側でイベントハンドラーに加えた処理を外す      
        backgroundWorker.ProgressChanged -= BackgroundWorker_ProgressChanged;
        backgroundWorker.RunWorkerCompleted -= BackgroundWorker_RunWorkerCompleted;
    }
}

呼び出し側


public partial class Form1 : Form
{
    BackgroundWorker bw;
    public Form1()
    {
        InitializeComponent();
        bw = new BackgroundWorker();
    }

    private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
    {
        for (int i = 0; i < 100; i++)
        {
            bw.ReportProgress(i, $"count is {i}");
            if(bw.CancellationPending)
            {
                //処理がキャンセルされた場合
                //※重要:競合を防ぐため、Cancel状態をDoWorkEventArgsにも伝える。
                //この処理を省くと、 RunWorkerCompletedEventArgs.Canceledがfalseのまま伝わる。
                e.Cancel = true;
                return;
            }
            System.Threading.Thread.Sleep(100);
        }
    }

    private void startBtn_Click(object sender, EventArgs e)
    {
        //キャンセルできる処理を呼び出す
        bw.WorkerSupportsCancellation = true;
        bw.WorkerReportsProgress = true;            
        bw.DoWork += this.backgroundWorker_DoWork;
        using (var prgForm = new ProgressForm(bw))
        {
            //進捗表示フォームを呼び出す。
            var res = prgForm.ShowDialog();
            if(res == DialogResult.OK)
            {
                //正常終了時の処理
            }
            else{
                //正常終了でない場合の処理
            }
        }
        bw.DoWork -= this.backgroundWorker_DoWork;
    }

    private void NoCancelStartBtn_Click(object sender, EventArgs e)
    {
        //キャンセルできない処理を呼び出す。
        bw.WorkerSupportsCancellation = false;
        bw.WorkerReportsProgress = true;
        bw.DoWork += this.backgroundWorker_DoWork;
        using (var prgForm = new ProgressForm(bw))
        {
            //進捗表示フォームを呼び出す。
            var res = prgForm.ShowDialog();
            if (res == DialogResult.OK)
            {
                //正常終了時の処理
            }
            else
            {
                //正常終了でない場合の処理
            }
        }
        bw.DoWork -= this.backgroundWorker_DoWork;
    }
}

メソッドとイベントの関係

Backgroundworkerには以下の3つのMethodが実装されており、それぞれ叩かれた瞬間にイベントを発生させます。

  • RunWorkerDoAsync()/RunWorkerDoAsync(object? argument)
    • DoWorkイベントを発生させる
      • object? argumentで渡した値がDoWorkEventArgs.Argumentに格納されて引き渡される。
  • ReportProgress(int percentProgress)/ReportProgress(int percentProgress, object? userState)
    • ProgressChangedイベントを発生させる
      • 引数で渡した値がProgressChangedEventArgsに格納されて引き渡される。
        • int percentProgressProgressChangedEventArgs.ProgressPercentage
        • object? userStateProgressChangedEventArgs.UserState
  • CancelAsync()
    • BackgroundWorker.CancellationPendingがtrueにされる。
    • RunWorkerCompletedイベントを発生させる

これらのメソッドの挙動を元に、画面ごとにイベントハンドラへイベントを追加します。

進捗表示画面側の処理

進捗表示画面では以下の処理を行います。

  • 進捗の表示
    • ProgressChangedイベントハンドラーに表示の更新処理を追加する。
    private void BackgroundWorker_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        //プログレスバーの値を変更する
        //設定されてる値の範囲外の値を渡すとエラーが発生することに注意
        if (e.ProgressPercentage < this.progressBar1.Minimum)
        {
            this.progressBar1.Value = this.progressBar1.Minimum;
        }
        else if (this.progressBar1.Maximum < e.ProgressPercentage)
        {
            this.progressBar1.Value = this.progressBar1.Maximum;
        }
        else
        {
            this.progressBar1.Value = e.ProgressPercentage;
        }
        //UserStateの内容を表示する
        progressLabel.Text = e.UserState.ToString();
    }
  • キャンセルの受付
    • キャンセルボタン/×ボタンを押した際、CancelAsync()を呼ぶ
    private void cancelBtn_Click(object sender, EventArgs e)
    {
        //処理をキャンセルする
        if (backgroundWorker.IsBusy)
        {
            this.backgroundWorker.CancelAsync();
        }
    }
    private void ProgressForm_FormClosing(object sender, FormClosingEventArgs e)
    {
        //×ボタンで閉じられた際の対処
        //backgroundWorkerが動いており
        if (backgroundWorker.IsBusy && !backgroundWorker.CancellationPending)
        {
            //一度Closingをキャンセルしたうえで、改めてキャンセル処理をする。
            e.Cancel = true;
            backgroundWorker.CancelAsync();                
        }
    }
  • 完了時の処理
    • RunWorkerCompletedイベントハンドラに表示処理を足す
      • エラーで終了する場合:エラー内容の表示等を行う
      • キャンセルされた場合:DialogResultの値を変更する
    //完了時の挙動
    private void BackgroundWorker_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if((e.Error is null) == false)
        {
            //エラーの表示等の処理
            Console.WriteLine("error");
            DialogResult = DialogResult.Cancel;
        }
        else if (e.Cancelled)
        {
            //キャンセル時の処理
            Console.WriteLine("canceled");
            DialogResult = DialogResult.Cancel;                
        }
        else
        {
            //完了時の処理
            Console.WriteLine("completed");
            DialogResult = DialogResult.OK;
        }
        //フォームを閉じる
        this.Close();
    }

以上を加えたフォームを呼び出すコードが舌のようになります(上記部分は省略)

public partial class ProgressForm : Form
{

    private BackgroundWorker backgroundWorker;

    public ProgressForm(BackgroundWorker bw)
    {
        InitializeComponent();
        this.backgroundWorker = bw;
        if (!bw.WorkerSupportsCancellation)
        {
            //キャンセル
            this.cancelBtn.Enabled = false;
            this.cancelBtn.Visible = false;
            this.ControlBox = false;
        }
        backgroundWorker.ProgressChanged += BackgroundWorker_ProgressChanged;
        backgroundWorker.RunWorkerCompleted += BackgroundWorker_RunWorkerCompleted;            
    }
    private void ProgressForm_Shown(object sender, EventArgs e)
    {
        //画面表示を開始した際に動いてない場合はRunWorkerAsync()を呼ぶ
        //二重に叩かないように判定を入れる
        if (!backgroundWorker.IsBusy)
        { 
            backgroundWorker.RunWorkerAsync();
        }
    }
    private void ProgressForm_FormClosed(object sender, FormClosedEventArgs e)
    {            
        //フォーム側でイベントハンドラーに加えた処理を外す      
        backgroundWorker.ProgressChanged -= BackgroundWorker_ProgressChanged;
        backgroundWorker.RunWorkerCompleted -= BackgroundWorker_RunWorkerCompleted;
    }
}

注意すべき点(筆者が作ってて躓いた点)は以下のようになります

  • フォームが閉じた際に、backgroundWorkerのイベントハンドラーに足した処理を外す
    • 渡されたbackgroundWorkerを呼び出し側が再利用した際に副作用が起こる危険があるため
    private void ProgressForm_FormClosed(object sender, FormClosedEventArgs e)
    {            
        //フォーム側でイベントハンドラーに加えた処理を外す      
        backgroundWorker.ProgressChanged -= BackgroundWorker_ProgressChanged;
        backgroundWorker.RunWorkerCompleted -= BackgroundWorker_RunWorkerCompleted;
    }
  • キャンセル時にRunWorkerCompletedEventArgs.Result呼び出さない
    • RunWorkerCompletedEventArgs.Cancelledがtrueになった状態でこの値を呼ぶとInvalidOperationExceptionが発生する仕様になっているため
  • キャンセルボタン以外に×ボタンを押して終了させる可能性を忘れない
    • ×ボタンそのものを非表示にする/閉じる前にイベントを検知する、等の対策を組み込む
    private void ProgressForm_FormClosing(object sender, FormClosingEventArgs e)
    {
        //×ボタンで閉じられた際の対処
        //backgroundWorkerが動いており
        if (backgroundWorker.IsBusy && !backgroundWorker.CancellationPending)
        {
            //一度Closingをキャンセルしたうえで、改めてキャンセル処理をする。
            e.Cancel = true;
            backgroundWorker.CancelAsync();                
        }
    }
  • プログレスバーに値を反映する際、プログレスバー側の最大値・最小値を超えた値を設定しないようにする。
    • 範囲内に無い値を代入するとエラーが出るため。

呼び出し側の処理

呼び出し側では以下の処理を行います。

  • 進捗の管理:DoWorkイベントハンドラーに以下の内容を含んだ処理を足す。
    • 行いたい重たい処理
    • ReportProgressメソッドによる進捗の報告
    • キャンセル時の挙動

public partial class Form1 : Form
{
    BackgroundWorker bw;
    public Form1()
    {
        InitializeComponent();
        bw = new BackgroundWorker();
    }

    private void backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
    {
        for (int i = 0; i < 100; i++)
        {
            bw.ReportProgress(i, $"count is {i}");
            if(bw.CancellationPending)
            {
                //処理がキャンセルされた場合
                e.Cancel = true;//※重要:競合を防ぐため、Cancel状態をDoWorkEventArgsにも伝える。
                return;
            }
            System.Threading.Thread.Sleep(100);
        }
    }

    private void startBtn_Click(object sender, EventArgs e)
    {
        //イベントハンドラーに処理を渡す。
        bw.DoWork += this.backgroundWorker_DoWork;
        using (var prgForm = new ProgressForm(bw))
        {
            //進捗表示フォームを呼び出す。
            var res = prgForm.ShowDialog();
            if(res == DialogResult.OK){
                //正常終了時の処理
            }
            else{
                //正常終了でない場合の処理
            }
        }
        bw.DoWork -= this.backgroundWorker_DoWork;
    }

注意すべき点(筆者が作ってて躓いた点)は以下のようになります。

  • BackgroundWorker.CancellationPendingの値とDoWorkEventArgs.Cancel必ず揃える。
    • DoWorkEventArgs.Cancelの値がRunWorkerCompletedEventArgs.Cancelledの値になるため、この処理を挟まないと完了時のキャンセル処理が行えなくなる。
    if(bw.CancellationPending)
    {
        //処理がキャンセルされた場合
        e.Cancel = true;//※重要:競合を防ぐため、Cancel状態をDoWorkEventArgsにも伝える。
        return;
    }

参照

以下のサイトを参考にしました。

DOBON.Netさん

MSの公式

1
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?