LoginSignup
2
2

More than 3 years have passed since last update.

【WPF】ImageSourceもUIスレッドで作る必要がある

Last updated at Posted at 2020-04-04

TL;DR;

NG

ImageSource thumbnail = await Task.Run(() =>
{
    // ワーカスレッド
    var bitmap = new Bitmap(image));  // Drawing.Image => Bitmap 変換 
    var imageSouce = Imaging.CreateBitmapSourceFromHBitmap(bitmap.GetHbitmap(),
        IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions())); // Bitmap => ImageSource
    return imageSource
});
// UIスレッド
viewModel.Add(new ViewModel(){ Thumbnail = thumbnail, });

System.ArgumentException: 'DependencySource は、DependencyObject と同じ Thread 上で作成する必要があります。'

OK

Bitmap bitmap = await Task.Run(() =>
{
    // ワーカスレッド
    var bmp = new Bitmap(image));  // Drawing.Image => Bitmap 変換
    return bmp;
});
// UIスレッド
var thumbnail = Imaging.CreateBitmapSourceFromHBitmap(bitmap.GetHbitmap(),
IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions())); // Bitmap => ImageSource
viewModel.Add(new ViewModel(){ Thumbnail = thumbnail, });

やりたかったこと

何枚かある画像をDrawing.Imageで入力されます。
それをなるべくUIスレッドを止めずに、非同期でControls.Imageに表示します。

詳細

WPFでは、UI操作はUIスレッドで行わなければなりません。
なので、XAMLでx:Nameでクラス内に持ってきて直接いじる場合はもちろんのこと、
BindされているViewModelのプロパティやコレクションも変更した際にUI操作が刺さったイベントが発火するので、UIスレッドで行う必要があります。
ここまでは自然に思えます。

しかし、ImageSourceの生成はUIスレッドで行わなければならないようです。
名前空間も違いますし(System.Windows.ControlsSystem.Windows.Media)、ImageSourceの生成は結構重いのでできればワーカスレッドに逃がしたいところではありますが。

// PropertyChangedの発火はUIスレッドが期待される
abstract class ViewModelBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;
    protected void RaisePropertyChanged([CallerMemberName]string propertyName = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}

// 適当なViewModel
class ViewModel : ViewModelBase
{
    ImageSource thumbnail;
    public ImageSource Thumbnail {
        get => thumbnail;
        set
        {
            RaisePropertyChanged();
            thumbnail = value;
        }
    }
}

async Task ApplyImageAsync(ObservableCollection<ViewModel> viewModels, Image image)
{
    var bitmap = await Task.Run(() => new Bitmap(image)); // Drawing.Image => Bitmap 変換
    ImageSource thumbnail = Imaging.CreateBitmapSourceFromHBitmap(source.GetHbitmap(),
        IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions());
        // Bitmap => ImageSource
        // ただ変換しているだけのように思えるが、実はImageSourceの生成はUIスレッドで行わなければならない。
        // しかも例外が起こるのは、ImageSource生成時ではなく、BindされたViewModelのOnXXXChangedが飛んで
        // 実際にImageSourceが使われるときになるので注意が必要。
    var viewModel = new ViewModel(){ Thumbnail = thumbnail, };
        // この例では新規インスタンスなのでPropertyChangedは発火しても何も起こらないが、
        // すでにBindされている場合はPropertyChangedの発火でUI操作が走る可能性があるので、
        // ViewModelのプロパティを操作するときはUIスレッドでやる必要がある。
    viewModel.Add(viewModel);
        // ObservableCollection<T>.OnCollectionChangedが呼ばれ
        // その先にはUI操作が刺さっているので、UIスレッドでAddする必要がある。
}

[補足] Async/Awaitについて

私も嘗てはなかなか理解できなかったのですが、次のように考えるとすんなり理解できました。
(※あくまでもイメージです)

asyncメソッドの戻り値はTask/Task<TResult>型と決められています。(voidもありますが例外だと思ってください)
つまり、呼び出し側が戻り値としてもらえるのは、「タスク情報」です。
このタスク情報には、タスク、つまりasyncメソッドが「終了しているか」「asyncメソッドでreturnした値」が取り出せます。

Task<int> task = GetICountAsync();
while(!task.IsComplete)
{
   // ビジーループで待ち受け
}
int count = task.Result;

というように書くこともできます。
ただし、ビジーループではUIスレッドが止まってしまい、フリーズしたように見えてしまうのでよくありません。
そこで、awaitが登場します。
awaitには二つの機能があります。

一つ目は、タスク情報から真の戻り値を取り出すことです。先ほどのビジーループの例では、GetCountAsync()は、Task<int>型の変数で受け取っていました。
しかし、awaitを使うと、int count = await GetCountAsync();というように受ける変数の型が変わります。これにより単純にコードが短くなりハッピーになります。次のようなTask<TResult> => TResultへの変換機能だと思ってください。

TResult Await(Task<TResult> task) => task.Result;
int count = Await(GetCountAsync());

二つ目は、Task<TResult>(A)を受け取ったら即座にそのメソッドを中断します。その代わりにタスク情報(B)を返します。
メソッドの残りの処理については受け取ったタスク情報(A)が終わったときのイベントに「元のスレッドのタスク処理待ちに積む」処理を刺します。(元のスレッドというのは、正確にはSynchronizationContext.Currentです。)
積む処理にはタスク情報の真の戻り値を渡します。これが一つ目の機能ですね。

なので、残っていた処理はちゃんとUIスレッドで実行されるというわけです。そして、元スレッドの処理待ちに積まれた処理が完了すると、注目していたawaitを含むasyncメソッドが終了します。
そして、awaitが中断した代わりに呼び出し元に返していたタスク情報(B)も完了済みになります。
呼び出し元もawaitで呼び出していたとしたら呼び出し元の残っていた処理が消化され、呼び出し元メソッドも終了します。
そして呼び出し元の呼び出し元が…というように遡っていきます。

じゃあ、始端はどうなるのだというと、Task<Result>で受け取ったまま特に何もしていなければ、なにもされないだけです。

{
    Task<int> task = GetICountAsync(); // GetICountAsyncがタスク情報を返してきた時点で制御が戻る
}
var hoge = Hoge(); // taskが完了したかなど関係なく普通に進んでいく
...

ということでまとまるとawaitは

  1. タスク情報(A)受け取り
  2. メソッドを中断
  3. Aが終わったときに「Aの真の戻り値で残りの処理を元のスレッドの処理待ちに積む」処理がされるようにAに仕込む
  4. 呼び出し元にタスク情報(B)を返す

ということを「await」と書くだけでやってくれるものです。

まだ一つ、不明な点があります。始端ががあれば終端があるはずです。awaitはタスク情報を受け取りタスク情報を返します。最初にタスク情報を返してくるのは何なのかといった問題です。
もちろん、return new Task<Result>()でもいいのですが、これを使うことはまずありません。
実用では、大体二つの方法を使います。
一つ目は、組み込みのTaskを返すTask.DelayFile.ReadTextAllAsyncなどのメソッドです。内部的にnew Task<TResult>()と思って下さい。
二つ目は、ある意味一つ目に含まれますが、Task.Run(Func<TResult>)です。これはFunc<TResult>をワーカスレッドに逃がしてTask
を返してくれます。自作で時間のかかるメソッドを作ったときにはTask.Runに突っ込んでタスク情報を返すようにしてあげるようにするといいと思います。(このメソッドはワーカスレッドなのでUIを触ってはいけません!)

まとめると、
本来戻り値のバケツリレーであるメソッド呼び出しをタスク情報のバケツリレーに置き換え、真の戻り値は終わったらやっといてねー。あ、UIスレッドで」といったところでしょうか。

余談

職場で「Async/Await解禁!」となって喜んでいたのですが、ちゃんと他人にも説明できるようにと思ってついでにまとめてみました。
のですが…補足の方が長くなってしまいました…もはやタイトル違いですね。
保身ですが、Async/Awaitを受け止められることを目的とした説明なので、厳密にはちょっと違うし暗黙でシチュエーションが限定されていることを言訳しておきます。

ちなみに前にもAsync/Awaitについて書いてます。

async/await で書いたコードと実行されるスレッド(Qiita)

参考

WPFで画像の読み込みを非同期で行いたい。async/awaitでのやりかた。(中の技術日誌ブログ)

2
2
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
2
2