午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

処理中に進捗率表示とキャンセルボタンで中止はasync、await、Task.Run、Progress、CancellationTokenSource

時間のかかる処理中に
  • 進捗率を表示
  • キャンセルボタンで処理中止
いつもどおりよくわかっていないけどできた!のでメモしとく
 
イメージ 1
 
 
 
進捗率表示更新は
WPFWindowsフォーム:時間のかかる処理をバックグラウンドで実行するには?(async/await編)[C#VB]:.NET TIPS - @IT
http://www.atmarkit.co.jp/ait/articles/1512/02/news019.html
こちらからのコピペなので問題ないはず
 
キャンセルの処理はあちこち見ながら自分で書いたので間違っているかも
でも期待どおりの動きなのでOK
 
 
デザイン画面

f:id:gogowaten:20191212152358p:plain

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

namespace _20180503_時間のかかる処理を途中でキャンセル
{
    public partial class MainWindow : Window
    {
        CancellationTokenSource cancelTokensource;//キャンセル判定用
        public MainWindow()
        {
            InitializeComponent();
            MyButton実行.Click += MyButton実行_Click;
            Mybuttonキャンセル.Click += Mybuttonキャンセル_Click;
        }
        private void Mybuttonキャンセル_Click(object sender, RoutedEventArgs e)
        {
            if (cancelTokensource != null)
            {
                cancelTokensource.Cancel();//キャンセルを発行
            }
        }
        //非同期メソッドにするのでvoidの前にasyncを付けている
        private async void MyButton実行_Click(object sender, RoutedEventArgs e)
        {
            MyButton実行.IsEnabled = false;
            MyStatusText.Text = "処理中…";
            MyProgressBar.Value = 0;

            //キャンセル用トークン作成
            cancelTokensource = new CancellationTokenSource();
            var cToken = cancelTokensource.Token;

            //Progress作成時に表示更新用のメソッドを指定する
            //これを時間のかかる処理に渡す
            var p = new Progress<int>(ShowProgress);
            //非同期で時間のかかる処理を実行、これが別スレッドで実行される           
            bool result = await Task.Run(() => DoWork時間かかるYO(p, cToken));

            if (result == false)
            {
                MyStatusText.Text = "キャンセルされた";
            }
            else
            {
                MyStatusText.Text = "処理完了!";
            }

            MyButton実行.IsEnabled = true;
        }
        //5秒かかる処理
        //ProgressはIProgressで受け取る
        private bool DoWork時間かかるYO(IProgress<int> p, CancellationToken cancelToken)
        {
            for (int i = 1; i <= 50; ++i)//0.1秒の50回ループ、合計5秒
            {
                //キャンセル判定
                if (cancelToken.IsCancellationRequested == true)
                {
                    return false;
                }

                Thread.Sleep(100);//0.1秒待機
                int percentage = i * 100 / 50;//進捗率               
                p.Report(percentage);//状況の報告
            }
            return true;
        }
        //表示更新用
        private void ShowProgress(int percent)
        {
            MyProgressBar.Value = percent;
            MyTextBlock.Text = percent.ToString() + "%完了";
        }
    }
}
 
 

f:id:gogowaten:20191212152421p:plain

 
時間のかかる処理は別スレッドで実行する
それにはasyncとawait、System.ThreadingクラスのTask.Runを使う
これで処理中にアプリが固まることがなくなるので
キャンセルボタンを押すことができるようになる
 
表示更新、Progressクラスを使う
別スレッドから直接UIスレッドの表示を更新することはできないので
表示更新用のメソッドを持たせたProgressオブジェクトを作成して
これを時間のかかる処理メソッドに渡す
受け取る側はProgressじゃなくてIProgressで受け取る
処理中にProgressのReportメソッドを実行して表示更新する
 
処理のキャンセル
CancellationTokenSourceクラスのTokenを使う
これを時間のかかる処理に渡しておく
キャンセルボタンが押されたらキャンセルを発行
時間のかかる処理中にはキャンセルが発行されたかどうかの判定をする
発行されたら停止処理
 
 
 
UI
UIはユーザーインターフェース、アプリのウィンドウやボタンとかのコントロールのことらしい
 
UIスレッド(メインスレッド)と別スレッド(ワーカースレッド)
普通の処理は全部UIスレッド
時間のかかる処理をUIスレッドで行うと終了するまでUI(アプリ)が固まる
これを避けるためにUIスレッドとは別スレッドで時間のかかる処理を実行する
 
別スレッド
別スレッドを使うにはasyncとawait、System.ThreadingクラスのTask.Runを使う
これでUIスレッドは自由になるからUI(アプリ)は固まることがなくなるので
表示更新やボタンをクリックとかできるはずなんだけど
 
別スレッドからはUIにアクセスできない
UIスレッドからはボタンとかのコントロールにアクセスできるけど
 
別スレッドからはボタンとかのコントロールにアクセスできない
 
進捗率表示を更新したい
進捗率は別スレッドで行っている時間のかかる処理中に変化するから、別スレッドから表示更新することになる、けど別スレッドからUIにはアクセスできないっていう矛盾
これを解決するのに使うのがProgressクラス
 
ProgressBarのValueとTextBlockのTextを更新するメソッドを用意しておいて
イメージ 3
 
これをProgressのインスタンス作成時に渡す(70行目)
イメージ 4
これを時間のかかる処理に渡す(72行目)
 
受け取るときの引数の型はIProgressで受け取る(88行目)

f:id:gogowaten:20191212152444p:plain

別スレッドからでもIProgressのReportメソッドを使うと、Progressに渡しておいたメソッド(ShowProgress)をUIスレッドで実行できるので表示の更新ができる
(100行目)
キャンセルの方法もだいたい同じ感じで、使うのはCancellationTokenSourceクラス
 
イメージ 6
フィールドにCancellationTokenSourceを用意しておいて
 
イメージ 10
キャンセルボタンのクリックイベントの中で
CancellationTokenSourceのCancelメソッドでキャンセルを発行するようにしておいて
 
 
イメージ 7
別スレッド実行前にCancellationTokenSourceのインスタンスを作成して
そのTokenプロパティを
イメージ 8
別スレッドで実行する時間のかかる処理に渡す
 

f:id:gogowaten:20191212152458p:plain

93行目、時間のかかる処理の中でキャンセルが発行されたかどうかの判定をしている
95行目、キャンセルされていたら処理中止
 
表示更新はProgress
キャンセルはCancellationTokenSourceの違いくらいで同じ感じだねえ
これらを別スレッドとして実行するメソッドに渡して中でそれぞれの動作をさせる
 
 
 
参照したところ
WPFWindowsフォーム:時間のかかる処理をバックグラウンドで実行するには?(async/await編)[C#VB]:.NET TIPS - @IT
http://www.atmarkit.co.jp/ait/articles/1512/02/news019.html
C# WPF】Task Runの引数にControl.Textを使うと別のスレッドに所有され... - Yahoo!知恵袋
https://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q11182269146
非同期で複数処理を実行し、対話式で制御する - Qiita
https://qiita.com/haminiku/items/cc299c1ed94d7ba3f9ec
 
 
コード
アプリ
 
 
 
 
関連記事
続きは2018/05/07
画像処理中のプログレスバー更新とキャンセルボタンで中止 ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15494790.html
 
 
2015/11/7、あれからもう3年…なの?
BackgroundWorkerの使い方メモ.NET VB、大量の画像ファイルを読み込んでListViewに縮小画像を表示 ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/13625471.html
今ではBackgroundWorkerよりも今回の記事の方法のがいいみたい?