SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

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();
            }
        }
    }