WPFで非同期&キャンセル可能な画像の読み込みを行う
昨日書いた、WPFでの画像読み込み処理をキャンセルする方法を、使いやすいようにクラスにしてみました。
非同期での読み込みと、CancellationTokenを用いた処理のキャンセルに対応したメソッドを作ってBitmapFrameExというクラスにしています。
使い方
BitmapFrameExというクラスに、CreateAsyncというstaticな非同期メソッドを作りました。
以下のように非同期メソッドとして呼び出しできます。
ここでCancellationTokenを引数に渡しておくと、トークンを使って読み込み処理のキャンセルができるようになります。
cts = new CancellationTokenSource(); this.Image = await BitmapFrameEx.CreateAsync(path, BitmapCreateOptions.None, cts.Token);
そして、Cancel()メソッドを呼び出すことで、BitmapFrameの読み込み処理をキャンセルできます。
cts.Cancel();
ついでなんで、読み込み時に指定したピクセル数(maxPixel)にリサイズしてから返すバージョンも作ってます。
コードは以下の通り。
BitmapFrameEx.cs
public static class BitmapFrameEx { #region 画像ファイルの元サイズのまま読み込むバージョン // 各種引数違いのオーバーロード版を用意 public static Task<BitmapSource> CreateAsync(string path) { return CreateAsync(path, BitmapCreateOptions.None); } public static Task<BitmapSource> CreateAsync(string path, BitmapCreateOptions createOptions) { return CreateAsync(path, createOptions, CancellationToken.None); } public static Task<BitmapSource> CreateAsync(string path, BitmapCreateOptions createOptions, CancellationToken cancellationToken) { return Task.Run(() => { return Create(path, createOptions, cancellationToken); }); } private static BitmapSource Create(string path, BitmapCreateOptions createOptions, CancellationToken cancellationToken) { using (var stream = File.Open(path, FileMode.Open)) { // キャンセルされたら、streamをDisposeする。 cancellationToken.Register(() => stream.Dispose()); try { var img = BitmapFrame.Create(stream, createOptions, BitmapCacheOption.OnLoad); img.Freeze(); return (BitmapSource)img; } catch (Exception ex) { System.Diagnostics.Trace.WriteLine(ex.Message); return null; } } } #endregion #region 最大のピクセル数を指定して、画像を縮小してから返すバージョン // 各種引数違いのオーバーロード版を用意 public static Task<BitmapSource> CreateAsync(string path, int maxPixel) { return CreateAsync(path, maxPixel, BitmapCreateOptions.None); } public static Task<BitmapSource> CreateAsync(string path, int maxPixel, BitmapCreateOptions createOptions) { return CreateAsync(path, maxPixel, createOptions, CancellationToken.None); } public static Task<BitmapSource> CreateAsync(string path, int maxPixel, BitmapCreateOptions createOptions, CancellationToken cancellationToken) { return Task.Run(() => { return Create(path, maxPixel, createOptions, cancellationToken); }); } private static BitmapSource Create(string path, int maxPixel, BitmapCreateOptions createOptions, CancellationToken cancellationToken) { using (var stream = File.Open(path, FileMode.Open)) { // キャンセルされたら、streamをDisposeする。 cancellationToken.Register(() => stream.Dispose()); try { var img = BitmapFrame.Create(stream, createOptions, BitmapCacheOption.None); var longSide = Math.Max(img.PixelWidth, img.PixelHeight); var scale = (double)maxPixel / longSide; var thumbnail = new TransformedBitmap(img, new ScaleTransform(scale, scale)); var cache = new CachedBitmap(thumbnail, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); cache.Freeze(); return (BitmapSource)cache; } catch (Exception ex) { System.Diagnostics.Trace.WriteLine(ex.Message); return null; } } } #endregion }
サンプルコード
前回と同様の内容を、BitmapFrameExを使って書いてみました。
前回との違いは、CreateAsyncメソッドを呼んでる部分と、キャンセル処理にCancellationTokenSourceを使ってることくらい。
MainWindow.xaml
<Window x:Class="WpfBaseTemplate1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfBaseTemplate1" Title="MainWindow" Width="450" Height="320"> <Window.DataContext> <local:MainWindowViewModel /> </Window.DataContext> <Grid> <StackPanel Margin="10"> <Image Width="300" Height="200" Margin="5" Source="{Binding Image}" /> <TextBlock Margin="5" Text="{Binding Message}" /> <StackPanel Orientation="Horizontal"> <Button Width="75" Margin="0,0,5,0" Command="{Binding LoadCommand}" Content="Load" /> <Button Width="75" Command="{Binding CancelCommand}" Content="Cancel" /> </StackPanel> </StackPanel> </Grid> </Window>
MainWindowViewModel.cs
class MainWindowViewModel : BindableBase { private BitmapSource image; public BitmapSource Image { get { return image; } set { this.SetProperty(ref this.image, value); } } private string message; public string Message { get { return message; } set { this.SetProperty(ref this.message, value); } } // 読み込みキャンセル用のトークン private CancellationTokenSource cts; // Loadボタンから呼び出すコマンド private RelayCommand loadCommand; public RelayCommand LoadCommand { get { return loadCommand = loadCommand ?? new RelayCommand(Load); } } // お行儀悪いかもしれないけど、async voidなメソッドをコマンドから実行する private async void Load() { // 一度画像をクリアする this.Image = null; // 処理時間を計測する var sw = Stopwatch.StartNew(); this.Message = "読み込み中..."; var path = @"H:\Test\Large.png"; cts = new CancellationTokenSource(); this.Image = await BitmapFrameEx.CreateAsync(path, BitmapCreateOptions.None, cts.Token); this.Message = string.Format("経過時間: {0}ms", sw.ElapsedMilliseconds); } // Cancelボタンから呼び出すコマンド private RelayCommand cancelCommand; public RelayCommand CancelCommand { get { return cancelCommand = cancelCommand ?? new RelayCommand(Cancel); } } private void Cancel() { if (cts != null) { cts.Cancel(); } } }