午後わてんのブログ

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

画像処理中のプログレスバー更新とキャンセルボタンで中止

テストアプリダウンロード先
時間のかかりがちな画像処理のときに

 

 

動作の様子
できた!
 
 
 
 
最初に作る時には前回の
処理中に進捗率表示とキャンセルボタンで中止はasync、await、Task.Run、Progress、CancellationTokenSource ( ソフトウェア ) - 午後わてんのブログ - Yahoo!ブログ
https://blogs.yahoo.co.jp/gogowaten/15489172.html
これを何も考えずに画像処理に使ってみようとしたら
 

f:id:gogowaten:20191212154024p:plain

スレッドが違うっていうエラーが出る
どうやら別スレッドでBitmapSourceを直接変更するような処理はできないみたい?
ってことでいくつか試してみた
 
 
 
イメージ 4
 
別スレッドで実行するメソッドへ渡す引数を
A: int
B: string
C: BitmapSource.Width
D: BitmapSource.Widthを入れたint
で試したら
CのBitmapSourceだけがエラーになる
 
A: int i = await Task.Run*1; //OK
C: int i = await Task.Run(() => BitmapSource.PixelWidth * 2); //エラー
D: int w = BitmapSource.PixelWidth;
int i = await Task.Run(() => w * 2); //OK
 
Cみたいに直接BitmapSourceのWidthを渡して処理しようとするとエラーになるけど
Dのようにint型の変数に入れたのを引数にすればエラーにならない
 
 
デザイン画面

f:id:gogowaten:20191212154041p:plain

C#コード

f:id:gogowaten:20191212154055p:plain

using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;

namespace _20180507_BitmapSourceは別スレッドで処理できない
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            MyButtonInteger.Click += MyButtonInteger_Click;
            MyButtonString.Click += MyButtonString_Click;
            MyButtonBitmap.Click += MyButtonBitmap_Click;
            MyButtonBitmap2.Click += MyButtonBitmap2_Click;
        }
        //int
        private async void MyButtonInteger_Click(object sender, RoutedEventArgs e)
        {
            int i = await Task.Run(() => 100 * 2);
            MyTextBlock.Text = i.ToString();
        }
        //string
        private async void MyButtonString_Click(object sender, RoutedEventArgs e)
        {
            int i = await Task.Run(() => ("test文字数").Count());
            MyTextBlock.Text = i.ToString();
        }
        //BitmapSource
        private async void MyButtonBitmap_Click(object sender, RoutedEventArgs e)
        {
            var source = new BitmapImage(new Uri(@"D:\ブログ用\チェック用2\NEC_2820_2018_05_06_午後わてん.jpg"));
            //エラーになる、別スレッドでBitmapSourceを使った処理はできない 
            int i = await Task.Run(() => source.PixelWidth * 2);
            MyTextBlock.Text = i.ToString();
        }
        //BitmapSourceのPixelWidthを入れたint型変数
        private async void MyButtonBitmap2_Click(object sender, RoutedEventArgs e)
        {
            var source = new BitmapImage(new Uri(@"D:\ブログ用\チェック用2\NEC_2820_2018_05_06_午後わてん.jpg"));
            int w = source.PixelWidth;
            //これならOK
            int i = await Task.Run(() => w * 2);
            MyTextBlock.Text = i.ToString();
        }
    }
}
 
これでBitmapSourceやBitmapSourceのプロパティを直接渡すとエラーになるけど
変数に入れて渡せばエラーにならず別スレッドで処理できそうなので
できたのが最初の画像処理アプリ
 
デザイン画面

f:id:gogowaten:20191212154137p:plain

 
C#コード
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Threading;

namespace _20180507_別スレッドで画像処理
{
    public partial class MainWindow : Window
    {
        BitmapSource OriginBitmap;//元画像用
        CancellationTokenSource MyCancelSource;//キャンセル用
        public MainWindow()
        {
            InitializeComponent();
            OriginBitmap = GetBitmapSourceWithChangePixelFormat2(
                @"D:\ブログ用\チェック用2\NEC_2773_2018_05_05_午後わてん_.jpg", PixelFormats.Pbgra32, 96, 96);
            MyImage.Source = OriginBitmap;

            MyButtonOrigin.Click += MyButtonOrigin_Click;
            MyButton1.Click += MyButton1_Click;
            MyButtonCancel.Click += MyButtonCancel_Click;
            MyButtonCancel.IsEnabled = false;
        }
        private void MyButtonCancel_Click(object sender, RoutedEventArgs e)
        {
            MyCancelSource.Cancel();//キャンセルを発行
        }
        //処理実行ボタン押した時
        private async void MyButton1_Click(object sender, RoutedEventArgs e)
        {
            MyButtonボタンの有効設定(true);
            var wb = new WriteableBitmap(OriginBitmap);
            int w = wb.PixelWidth;
            int h = wb.PixelHeight;
            int stride = wb.BackBufferStride;
            byte[] pixels = new byte[h * stride];
            wb.CopyPixels(pixels, stride, 0);
            MyCancelSource = new CancellationTokenSource();
            CancellationToken token = MyCancelSource.Token;
            Progress<int> progress = new Progress<int>(MyProgressUpdate);
            await Task.Run(() => ColorReverce(pixels, w, h, stride, token, progress));
            wb.WritePixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);
            MyImage.Source = wb;
            MyButtonボタンの有効設定(false);
        }
        //元の画像に戻す
        private void MyButtonOrigin_Click(object sender, RoutedEventArgs e)
        {
            MyImage.Source = OriginBitmap;
        }
        //プログレスバーの表示更新
        private void MyProgressUpdate(int i)
        {
            MyProgressBar.Value = i;
        }
        private void MyButtonボタンの有効設定(bool is処理中)
        {
            if (is処理中 == true)
            {
                MyButtonCancel.IsEnabled = true;
                MyButtonOrigin.IsEnabled = false;
                MyButton1.IsEnabled = false;
            }
            else
            {
                MyButtonCancel.IsEnabled = false;
                MyButtonOrigin.IsEnabled = true;
                MyButton1.IsEnabled = true;
            }
        }
        //色を反転
        private bool ColorReverce(byte[] pixels, int w, int h, int stride,
            CancellationToken token, IProgress<int> progress)
        {
            long p;
            for (int y = 0; y < h; ++y)
            {
                if (token.IsCancellationRequested == true)
                {
                    return false;
                }

                for (int x = 0; x < w; ++x)
                {
                    p = y * stride + x * 4;
                    //RGBを反転、アルファ値はそのまま
                    pixels[p] = (byte)(255 - pixels[p]);
                    pixels[p + 1] = (byte)(255 - pixels[p + 1]);
                    pixels[p + 2] = (byte)(255 - pixels[p + 2]);
                }
                progress.Report(y * 100 / (h - 1));//プログレスバー更新
                Thread.Sleep(10);//0.01秒待機
            }
            return true;
        }

        //指定PixelFormatでBitmapSource取得
        private BitmapSource GetBitmapSourceWithChangePixelFormat2(
        string filePath, PixelFormat pixelFormat, double dpiX = 0, double dpiY = 0)
        {
            BitmapSource source = null;
            try
            {
                using (System.IO.FileStream fs = new System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read))
                {
                    var bf = BitmapFrame.Create(fs);
                    var convertedBitmap = new FormatConvertedBitmap(bf, pixelFormat, null, 0);
                    int w = convertedBitmap.PixelWidth;
                    int h = convertedBitmap.PixelHeight;
                    int stride = (w * pixelFormat.BitsPerPixel + 7) / 8;
                    byte[] pixels = new byte[h * stride];
                    convertedBitmap.CopyPixels(pixels, stride, 0);
                    //dpi指定がなければ元の画像と同じdpiにする
                    if (dpiX == 0) { dpiX = bf.DpiX; }
                    if (dpiY == 0) { dpiY = bf.DpiY; }
                    //dpiを指定してBitmapSource作成
                    source = BitmapSource.Create(
                        w, h, dpiX, dpiY,
                        convertedBitmap.Format,
                        convertedBitmap.Palette, pixels, stride);
                };
            }
            catch (Exception) { }
            return source;
        }

    }
}
 
イメージ 7
画像の色を反転する画像処理
今まではBitmapSourceを受け取って処理していたけど、別スレッドで処理する場合はそれだとエラーになるのでbyte型配列とint型だけ受け取るように変更(80行目)
100行目でウェイトをかけているのは、色の反転処理はあっという間に終わってキャンセルボタンを押せないから
 

f:id:gogowaten:20191212154231p:plain

処理開始ボタンのクリックイベント
36~41行目と47,48行目は以前では画像処理で行っていたところ
46行目で画像処理を別スレッドで行っている
それ以外は前回と同じ
 
 

 
やっとできたけど
 
BitmapSourceはButtonやTextBlockと同じUIってことなのかねえ
そもそもUIってのがイマイチ理解できていない
 
それに別スレッドで行うメソッドに引数を渡す時に
BitmapSource.Widthそのままだとエラーになるけど
int w = BitmapSource.Width
って変数に入れて渡せばエラーにならないってのも、そうなのかあって感じ
 
参照すること自体がUIスレッドにアクセスするってことだから別スレッドエラーで
int型は参照型じゃなくて値型だからint型変数に入れれば参照じゃないからOK
ってことかなあ
 
それでも今回ので画像処理中の進捗率表示と処理キャンセルっていう目的は達成できた
 
 
コード
アプリ
 
 

*1:) => 100 * 2); //OK

B: int i = await Task.Run(() => ("test文字数").Count(