2012年2月3日金曜日

wpf : 画像ファイルをロックせずにBitmapSourceを読み込む

wpfでローカルの画像ファイルを読み込むときの話。 BitmapFrameを使うと次のようになります。

try
{
    // xamlでImageを記述 → imgLock
    imgLock.Source = BitmapFrame.Create(new Uri(path, UriKind.Absolute));
}
catch (Exception exc)
{
    // thisはMainWindow
    MessageBox.Show(this, "[" + exc.Message + "]\n" + exc.StackTrace, this.Title);
}

このやり方だと画像ファイルはロックされてしまい、更新、移動、削除ができません。 ロックされないようにするには次のように書きます。

try
{
    using (Stream stream = new FileStream(
        path,
        FileMode.Open,
        FileAccess.Read,
        FileShare.ReadWrite | FileShare.Delete
    ))
    {
        // ロックしないように指定したstreamを使用する。
        BitmapDecoder decoder = BitmapDecoder.Create(
            stream,
            BitmapCreateOptions.None, // この辺のオプションは適宜
            BitmapCacheOption.Default // これも
        );
        BitmapSource bmp = new WriteableBitmap(decoder.Frames[0]);
        bmp.Freeze();

        // xamlでImageを記述 → imgSync
        imgSync.Source = bmp;
    }
}
catch (Exception exc)
{
    MessageBox.Show(this, "[" + exc.Message + "]\n" + exc.StackTrace, this.Title);
}

まずロックしないような設定でstreamを作成。 そしてWriteableBitmapで読み込みます。 WriteableBitmapを使うのは同期処理ですぐに読み込まれるから。 BitmapFrame.Create(stream)だと非同期読み込みなのでusing中に読み込めません。 usingを抜けてstreamが閉じられた後に実際の読み込み処理が走りますが、失敗します。

同期処理って事で読み込みに時間がかかる場合は注意が必要ですが、短いコーディングで済みます。 以降、WriteableBitmapの編集をしない場合はお好みでFreezeさせましょう。

とりあえず今のところ、BitmapSourceを即読みさせる方法が「WriteableBitmapのコンストラクタ」しか見つかってないのでこんな感じになりました。 なんか「イベントドリブンじゃないのは気になる」って方にはこんなやり方もあります。

class なんたら
{
    private BufferedStream _imgLoadStream;

    public void LoadBitmapAsync(string path)
    {
        try
        {
            _imgLoadStream = new BufferedStream(
                new FileStream(
                    path,
                    FileMode.Open,
                    FileAccess.Read,
                    FileShare.ReadWrite | FileShare.Delete
                )
            );
            
            BitmapFrame bmpFrame = BitmapFrame.Create(
                _imgLoadStream,
                BitmapCreateOptions.None,  // この辺のオプションは適宜
                BitmapCacheOption.OnLoad); // OnLoad以外のオプションはダメかも
            imgAsync.Source = bmpFrame;
            imgAsync.SizeChanged += (sender, e) =>
            {
                MessageBox.Show(this, "SizeChanged", this.Title);
                closeImageLoadStream();
            };
        }
        catch (Exception exc)
        {
            MessageBox.Show(this, "[" + exc.Message + "]\n" + exc.StackTrace, this.Title);
            closeImageLoadStream();
        }
    }

    private void closeImageLoadStream()
    {
        try
        {
            if (_imgLoadStream != null)
            {
                _imgLoadStream.Close();
                _imgLoadStream = null;
            }
        }
        catch (Exception exc)
        {
            MessageBox.Show(this, "[" + exc.Message + "]\n" + exc.StackTrace, this.Title);
        }
    }
}

読み込み用のstreamをメンバー変数で取っておいて、読み込み終わったら後で閉じてます。 なんか普通っぽいコードです。

が、ちょっと問題があります。 それは私の検索不足ですが、「画像が読み込み終わった瞬間をとらえるイベントが分かっていない」ということ。 このコードではなんとなく使っているSizeChangedイベントは全てのケースで使えるか未確認です。 その他のそれっぽいイベントはいくつか試したけど不適当でした。

  • BitmapSource.DownloadCompleted ... ローカルのファイルを読んだときは発行されない? BitmapFrameで使おうとしたら「InvalidOperationException:Frozenだからダメ」と言われた。
  • Image.ImageFailed ... 壊れた画像などを渡しても発行を確認できず。 その場合は非同期ではなく、その場で例外発生。
  • Image.Loaded ... 「コントロールが使えるようになりました」というイベントであり、画像のロードとは無関係。 例えば、Loaded済のImageコントロールに後からBitmapSourceを設定したときなどなんの音沙汰も無い。

まぁ、streamがどっかで閉じればいいのでSizeChangedイベントでもその他の適当なイベントでいいのかもしれません。 stream開きっぱなしで放っておくのは行儀が悪いので、大量の画像をキャッシュするプログラムとかに使っちゃダメなコードなのは理解しておいてください。 そうじゃなくて「確実に表示される数少ない画像であり、読み込み後に確実に発行されるイベントも分かっている」ってならこの方法でもいい ... のか?

大量の画像を扱うときはWriteableBitmapを使った方のやり方で自前の読み込みスレッドを作った方がいいのかな? それだとstreamを閉じるタイミングで悩むことは無いですから。 byte配列に読んでからnew MemoryStream(byte配列)してBitmapFrame.Create(stream)に渡すっていう手もありますが、その場合もstreamを閉じるタイミングを探すって点では同じですしねぇ。

う~ん、用途と違うWriteableBitmapを使っている時点で間違っている気がする...

「中途半端なネタで書くなよ」と言っているそこの人、抱えて黙っているよりいいんです。 多分。

...

以下、サンプルコードの全体。 まずはMainWindow.xaml。

<Window x:Class="BitmapSourceReadTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" PreviewMouseDown="Window_PreviewMouseDown" >
    <StackPanel>
        <Image Name="imgLock" Height="100" />
        <Image Name="imgSync" Height="100" />
        <Image Name="imgAsync" Height="100" />
    </StackPanel>
</Window>

そしてMainWindow.xaml.cs。

using System;
using System.IO;
using System.Windows;
using System.Windows.Media.Imaging;

namespace BitmapSourceReadTest
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_PreviewMouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            FileInfo fileInfo = new FileInfo(Environment.GetCommandLineArgs()[0]);
            string path = fileInfo.Directory.FullName + "\\test.jpg";

            LoadBitmapDefault(path);
            LoadBitmapSync(path);
            LoadBitmapAsync(path);
        }

        public void LoadBitmapDefault(string path)
        {
            try
            {
                imgLock.Source = BitmapFrame.Create(new Uri(path, UriKind.Absolute));
            }
            catch (Exception exc)
            {
                MessageBox.Show(this, "[" + exc.Message + "]\n" + exc.StackTrace, this.Title);
            }
        }

        public void LoadBitmapSync(string path)
        {
            try
            {
                using (Stream stream = new FileStream(
                    path,
                    FileMode.Open,
                    FileAccess.Read,
                    FileShare.ReadWrite | FileShare.Delete
                ))
                {
                    // ロックしないように指定したstreamを使用する。
                    BitmapDecoder decoder = BitmapDecoder.Create(
                        stream,
                        BitmapCreateOptions.None, // この辺のオプションは適宜
                        BitmapCacheOption.Default // これも
                    );
                    BitmapSource bmp = new WriteableBitmap(decoder.Frames[0]);
                    bmp.Freeze();
                    imgSync.Source = bmp;
                }
            }
            catch (Exception exc)
            {
                MessageBox.Show(this, "[" + exc.Message + "]\n" + exc.StackTrace, this.Title);
            }
        }

        private BufferedStream _imgLoadStream;

        public void LoadBitmapAsync(string path)
        {
            try
            {
                _imgLoadStream = new BufferedStream(
                    new FileStream(
                        path,
                        FileMode.Open,
                        FileAccess.Read,
                        FileShare.ReadWrite | FileShare.Delete
                    )
                );
                
                BitmapFrame bmpFrame = BitmapFrame.Create(
                    _imgLoadStream,
                    BitmapCreateOptions.None,  // この辺のオプションは適宜
                    BitmapCacheOption.OnLoad); // OnLoad以外のオプションはダメかも
                imgAsync.Source = bmpFrame;
                imgAsync.SizeChanged += (sender, e) =>
                {
                    MessageBox.Show(this, "SizeChanged", this.Title);
                    closeImageLoadStream();
                };
            }
            catch (Exception exc)
            {
                MessageBox.Show(this, "[" + exc.Message + "]\n" + exc.StackTrace, this.Title);
                closeImageLoadStream();
            }
        }

        private void closeImageLoadStream()
        {
            try
            {
                if (_imgLoadStream != null)
                {
                    _imgLoadStream.Close();
                    _imgLoadStream = null;
                }
            }
            catch (Exception exc)
            {
                MessageBox.Show(this, "[" + exc.Message + "]\n" + exc.StackTrace, this.Title);
            }
        }
    }
}