C#リストビューで画像ファイルのサムネイル表示4「サムネイルをファイルに保存」

コンピュータ

大きなファイルのサムネイルを表示しようとすると描画まで時間がかかるので、サムネイル画像をファイルとして保存して2回目以降はそちら使うようにして高速化してみます。

プロジェクトの作成

mkdir プロジェクト名
cd プロジェクト名
dotnet new winforms

ソースコード

ファイル名:Form1.cs

using System.Diagnostics;
using System.Linq;
using System.Text.RegularExpressions;

namespace ThumSample4;

public partial class Form1 : Form
{
    string _path = @"C:\Users\karet\Pictures";
    List<ListViewItem> _listitem = new();

    ListView _listView = new()
    {
        Dock = DockStyle.Fill,
        OwnerDraw = true,
        VirtualMode = true,
        LargeImageList = new ImageList
        {
            ImageSize = new Size(Thumbnail1.Width, Thumbnail1.Height),
        },
    };
    MenuStrip _menuStrip = new();
    ToolStripMenuItem _menuItem = new()
    {
        Text = "実行",
    };
    public Form1()
    {
        InitializeComponent();

        _menuStrip.Items.Add(_menuItem);
        Controls.AddRange(new Control[] {_menuStrip, _listView});

        _menuItem.Click += _menuItem_Click;
        _listView.RetrieveVirtualItem += _listView_RetrieveVirtualItem;
        _listView.DrawItem += _listView_DrawItem;

        Size = new Size(1980, 1080);
    }
    void _menuItem_Click(Object? sender, EventArgs e)
    {
        _menuItem.Enabled = false;

        // 画像ファイルの一覧を取得、拡張子で絞り込み、サムネイルファイルを除く
        IEnumerable<string> imgFiles = Directory.EnumerateFiles(_path, "*.*", SearchOption.TopDirectoryOnly)
            .Where(x => Regex.IsMatch(x, @"\.(png|jp(e)?g|gif|bmp)$", RegexOptions.IgnoreCase))
            .Where(x => !Regex.IsMatch(x, @"_thumbnail\.", RegexOptions.IgnoreCase));

        int w = _listView.Size.Width / Thumbnail1.Width;
        int h = _listView.Size.Height / Thumbnail1.Height;


        // リストビューの更新開始
        _listView.BeginUpdate();

        if (_listitem.Count > 0) _listitem.Clear();
        foreach(var file in imgFiles)
        {
            _listitem.Add(new ListViewItem(file));
        }
        _listView.VirtualListSize = _listitem.Count;

        _menuItem.Enabled = true;

        // リストビューの更新終了
        _listView.EndUpdate();
    }
    void _listView_RetrieveVirtualItem(Object? sender, RetrieveVirtualItemEventArgs e)
    {
        if (e.Item == null) e.Item = _listitem[e.ItemIndex];
    }
    void _listView_DrawItem(Object? sender, DrawListViewItemEventArgs e)
    {
        string filename = e.Item.Text;
        Image? thumbnail = Thumbnail1.LoadImage(filename);
        if (thumbnail == null)
            return;

        Rectangle imagerect = new Rectangle(new Point(e.Bounds.X + ((e.Bounds.Width - thumbnail.Width) / 2), e.Bounds.Y), new Size(thumbnail.Width, thumbnail.Height));

        e.DrawDefault = false;
        e.DrawBackground();
        e.Graphics.DrawImage(thumbnail, imagerect);

        var stringFormat = new StringFormat() {
            Alignment = StringAlignment.Center,
            LineAlignment = StringAlignment.Center,
        };
        e.Graphics.DrawString(e.Item.Text, _listView.Font, Brushes.Black, new RectangleF(e.Bounds.X, e.Bounds.Y + imagerect.Height + 5, e.Bounds.Width, e.Bounds.Height - imagerect.Height - 5), stringFormat);
        e.DrawFocusRectangle();
    }
}//class

ファイル名:Thumbnail1.cs

using System.Diagnostics;
public class Thumbnail1
{
    static public readonly int Width = 384;
    static public readonly int Height = 384;

    static Object _lockObj = new();
    static Dictionary<string, Image> _thumbnailCache = new();

    static public Bitmap CreateThumbnail(string filename)
    {
        // サムネイルファイルの作成
        string thumbnailPath = "";
        string? dir = Path.GetDirectoryName(filename);
        string? bn = Path.GetFileNameWithoutExtension(filename);
        string? ext = Path.GetExtension(filename);
        if (dir is not null && bn is not null && ext is not null) {
            thumbnailPath = Path.Join(dir, bn+"_thumbnail"+ext);
            if (File.Exists(thumbnailPath)) {
                // 更新日時の確認
                var ttime = File.GetLastWriteTime(thumbnailPath);
                var otime = File.GetLastWriteTime(filename);
                if (ttime > otime) {
                    // 既に作成されたサムネイルファイルを読み込んで戻す
                    using var tfs = new FileStream(thumbnailPath, FileMode.Open, FileAccess.Read);
                    return (Bitmap)Bitmap.FromStream(tfs);
                } else {
                    // サムネイルの更新日>元画像の更新日の場合サムネイルファイルを削除
                    File.Delete(thumbnailPath);
                }

            }
        }


        using var fs = new FileStream(filename, FileMode.Open, FileAccess.Read);

        Bitmap canvas = new Bitmap(Width, Height);
        using var original = Bitmap.FromStream(fs);
        using var g = Graphics.FromImage(canvas);
        g.Clear(Color.White);

        double fw = (double)Width / (double)original.Width;
        double fh = (double)Height / (double)original.Height;

        double scale = Math.Min(fw, fh);

        int w2 = (int)(original.Width * scale);
        int h2 = (int)(original.Height * scale);

        g.DrawImage(original, (Width-w2)/2, (Height-h2)/2, w2, h2);

        // サムネイルを保存
        canvas.Save(thumbnailPath);
        var attr = File.GetAttributes(thumbnailPath);
        // サムネイルファイルを隠しファイルにに変更
        File.SetAttributes(thumbnailPath, attr | FileAttributes.Hidden);
        
        return canvas;
    }

    public static Image? LoadFile(string filename)
    {
        if (!File.Exists(filename)) return null;
        var thumbnail = CreateThumbnail(filename);
        lock(_lockObj)
        {
            if (!_thumbnailCache.ContainsKey(filename))
                _thumbnailCache.Add(filename, thumbnail);
        }
        return thumbnail;
    }

    public static Image? LoadImage(string filename)
    {
        if (_thumbnailCache.ContainsKey(filename))
        {
            return _thumbnailCache[filename];
        }
        return LoadFile(filename);
    }
}

実行

dotnet run

画像ファイルと同じディレクトリに_thumbnailというサフィックスが付いたファイル名でサムネイル画像がファイルとして保存されます。
サムネイルファイルは隠しファイルとしています。目には見えませんがストレージの容量を消費します。
元画像が削除や移動されてもサムネイル画像はそのままにですので、サムネイルファイルがゴミファイルと化す仕様になっています。

コードの内容は前回の記事からサムネイルをキャッシュとして先読みする機能を排除し、サムネイル用の縮小画像をを作った際ファイルとして書き出す機能と、サムネイルファイルの有無と更新日付を確認しサムネイル画像を再作成するか判断するロジックを追加しています。

コメント