C#でMemoryCacheクラスでWPFのBitmapSourceをキャッシュしてみる。

C# コンピュータ
C#

ストレージ内の画像ファイルとZIPファイル内の画像ファイルをキャッシュするプログラムを作成してみました。

プロジェクトの作成

PowerShellで実行。要dotnet.exe

mkdir BitmapImageLoad01
cd BitmapImageLoad01
dotnet new console
dotnet add package System.Runtime.Caching
code .

ソースコード

using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Runtime.Caching;

namespace BitmapImageLoad01;

/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    MemoryCache _cache = MemoryCache.Default;
    CacheItemPolicy _policy = new ()
    {
        SlidingExpiration = new TimeSpan(0, 5, 0),  // 有効期限を5分にセット
        RemovedCallback = arg =>
        {
            Debug.Print("key:{0}が削除された。", arg.CacheItem.Key);
        },
    };
    static async Task<BitmapSource> LoadImageFormStream(Stream s)
    {
        const int dpi = 96;

        MemoryStream ms = new();
        await s.CopyToAsync(ms);
        ms.Seek(0, SeekOrigin.Begin);

        BitmapSource bs = await Task.Run(()=>
        {
            BitmapSource bs = BitmapFrame.Create(ms);

            int stride = (bs.PixelWidth * bs.Format.BitsPerPixel + 7) / 8;
            byte[] pixels = new byte[stride * bs.PixelHeight];
            bs.CopyPixels(pixels, stride, 0);

            bs = BitmapImage.Create(bs.PixelWidth, bs.PixelHeight, dpi, dpi, PixelFormats.Bgra32, null, pixels, stride);
            bs.Freeze();
            return bs;
        });
        
        ms.SetLength(0);
        ms.Close();

        return bs;
    }
    static public bool IsValidExtension(string path)
    {
        string pattern = @"\.(jpeg|jpg|bmp|tif|bmp|png|gif)$";
        return System.Text.RegularExpressions.Regex.IsMatch(path, pattern);
    }
    public async Task<BitmapSource?> LoadImage(string location, string name)
    {
        string key = System.IO.Path.Join(location, name);
        BitmapSource? bs = null;

        if (_cache.Contains(key))
        {
            bs = _cache[key] as BitmapSource;
            return bs;
        }

        string ext = System.IO.Path.GetExtension(location).ToUpper();
        
        if (ext == ".ZIP")
        {
            string zipFile = location;
            string entryName = name;
            ZipArchiveEntry? entry = ZipFile.OpenRead(zipFile).GetEntry(entryName);
            if (entry is null) return null;

            using Stream stream = entry.Open();
            bs = await LoadImageFormStream(stream);
        } else {
            string fullPath = key;
            FileStream fs = new (fullPath, FileMode.Open, FileAccess.Read);

            using Stream stream = fs;
            bs = await LoadImageFormStream(stream);
        }

        _cache.Set(key, bs, _policy);
        return bs;
    }

    public MainWindow()
    {
        InitializeComponent();

        Loaded += async (s, e) =>
        {

            string zipFile = @"F:\csharp\dotnet8\wpf\BitmapImageLoad01\sample.zip";

            ZipArchive zip = ZipFile.OpenRead(zipFile);

            if (zip.Entries.Any())
            {
                string location = zipFile;

                foreach (var entry in zip.Entries.Where(x=>IsValidExtension(x.FullName)))
                {
                    var _ = await LoadImage(location, entry.FullName);
                }

                string name = zip.Entries.Where(x=>IsValidExtension(x.FullName)).First().FullName;
                Image1.Source = await LoadImage(location, name);
            }
            Debug.Print("Loaded ID:{0}", Environment.CurrentManagedThreadId);
        };
        Closed += (s, e) =>
        {
            Debug.Print("Closed ID:{0}", Environment.CurrentManagedThreadId);
            _cache?.Dispose();
        };
    }
}
<Window x:Class="BitmapImageLoad01.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:BitmapImageLoad01"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Image x:Name="Image1" />
    </Grid>
</Window>

説明

変数zipFileにセットしたzipファイル内の最初の画像が表示されます。

フォームを閉じる際_cacheオブジェクトをDispose()するようにしたところキャッシュされた要素が削除されました。

Loaded ID:1
Closed ID:1
key:F:\csharp\dotnet8\wpf\BitmapImageLoad01\sample.zip\2024-09-08090028.pngが削除された。
key:F:\csharp\dotnet8\wpf\BitmapImageLoad01\sample.zip\2024-09-05193316.pngが削除された。
key:F:\csharp\dotnet8\wpf\BitmapImageLoad01\sample.zip\スクリーンショット 2024-09-08 090021.pngが削除された。
key:F:\csharp\dotnet8\wpf\BitmapImageLoad01\sample.zip\2024-09-08090058.pngが削除された。
key:F:\csharp\dotnet8\wpf\BitmapImageLoad01\sample.zip\スクリーンショット 2024-09-05 193312.pngが削除された。
key:F:\csharp\dotnet8\wpf\BitmapImageLoad01\sample.zip\スクリーンショット 2024-09-05 193305.pngが削除された。

キャッシュされることを確認する為にzipファイルを先読みしていますが、待ち時間が発生しますので(async,awaitでUIはフリーズしていませんが)このあたりを何とかしたいと思います。

LoadImageFormStreamでストリームで指定した画像がいるを読み込んでBitmapSouceを返しています。
表示や加工をすることを想定してPixelFormatをBgra32にDPIを96になるように変換処理を挟んでいます。

前回記事の別スレッドでループを回すプログラムを使って、表示予定の画像ファイルを先読みさせる方法を考えています。
C#で別スレッドでループを回して処理を行う。
別スレッドを起動しその中でループを回し、ループ内で処理を行うプログラムがあります。基本的にサーバーなどのサービスのリクエストの受付などの処理を待つプログラムで使われるコードです。 bool _loopFlag = true; public ...

ただ、このたぐいのスレッドを使うプログラムは実際動かしてみないと上手く動作するかがわからないが難しいところです。
今回のサンプルプログラムも動作を確認するだけであればconsoleプロジェクトの方が都合が良いのですが、WPFに組み込む予定ですのでWPFで試しています。

コメント