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

C# コンピュータ
C#
前回作成したプログラムで、サムネイル用の縮小画像を毎回作成するとレスポンスが悪くなるので、一度生成した縮小画像をファイルとして保存し、2回目以降はそれを読み出すようなキャッシュルーチンを組み込んでいました。保存した縮小画像のファイルは不可視属性として基本的に表示されないようにしていました。
ただ、見えないだけで存在しており、エクスプローラーでは表示されなくとも、他のプログラムで顔を出すことがあり、意外と使がってが悪いことに気が付きました。
C#リストビューで画像ファイルのサムネイル表示4「サムネイルをファイルに保存」
大きなファイルのサムネイルを表示しようとすると描画まで時間がかかるので、サムネイル画像をファイルとして保存して2回目以降はそちら使うようにして高速化してみます。プロジェクトの作成mkdir プロジェクト名cd プロジェクト名dotnet n...

ということでサムネイル画像をzipファイルにアーカイブして、目のつかない場所にため込むスタイルにしたいと思います。
そうなりますと、サムネイル画像と元画像を紐づけする必要があり、そちらの管理はSQLiteでテーブルを作成したいと思います。
そういえば昔フォルダに作成した覚えがない「Thumbs.db」というファイルが存在していることがありました。多分これ似たようなものを自作する感じになります。

プロジェクトの作成

dotnet new winforms --name プロジェクト名
cd プロジェクト名
dotnet add package System.Data.SQLite

ソースコード

ファイル名:Form1.cs

namespace ThumSample5;

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

public partial class Form1 : Form
{
    // 対象ディレクトリ
    string _path = @"H:\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(800, 600);
    }
    // メニュー実行クリック
    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));

        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.GetThumnailImage(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

Form1.csは前回とほぼ一緒
ファイル名:Thumbnail1.cs

using System.Diagnostics;

using System.Drawing.Imaging;
using System.IO.Compression;

using System.ComponentModel;
using System.Data.Common;
using System.Data.SQLite;
using System.Data.Entity.Core.EntityClient;

/*
 サムネイルテーブルの定義
*/
class ThumbnailTbl
{
    public long id = 0;
    public string? filename;
    public string? lastmodified;
}
/*
 サムネイルクラス
*/
public class Thumbnail1
{
    static public readonly int Width = 256;
    static public readonly int Height = 256;

    // データベースファイル
    const string DB_FILE = @".\thumnail.db";
    // ZIPファイル
    const string ZIP_FILE = @".\thumnail.zip";
    // コネクションオブジェクトの生成
    static SQLiteConnection conn = new();

    /*
     データベースと接続
    */
    static void ConnectDatabase()
    {
        // 接続文字列をセット
        conn.ConnectionString = $"Data Source = {DB_FILE}";
        // データベースを開く
        conn.Open();
        // コマンドオブジェクトを作成
        using var cmd = new SQLiteCommand(conn);
        // テーブルを作成
        cmd.CommandText = "CREATE TABLE IF NOT EXISTS thumbnail (id  INTEGER PRIMARY KEY AUTOINCREMENT, filename TEXT, lastmodified TEXT);";
        cmd.ExecuteNonQuery();
        // インデックスを作成
        cmd.CommandText = "CREATE UNIQUE INDEX IF NOT EXISTS filenameindex ON thumbnail(filename, lastmodified);";
        cmd.ExecuteNonQuery();
    }
    /*
     データベースから切断
    */
    static void DisconnectDatabase()
    {
        conn.Close();
    }
    /*
     thumbnailテーブルからデータを取得
    */
    static bool GetThumbnailOne(ref ThumbnailTbl record)
    {
        bool result = false;
        using var cmd = new SQLiteCommand(conn);

        string? filename = record.filename;
        string? lastmodified = record.lastmodified;
        cmd.CommandText = $"SELECT id, filename, lastmodified FROM thumbnail where filename = '{filename}' and lastmodified = '{lastmodified}'";
        using var rec = cmd.ExecuteReader();
        if (rec.Read()) {
            result = true;
            record.id = (long)rec["id"];
            record.filename = (string)rec["filename"];
            record.lastmodified = (string)rec["lastmodified"];
        }
        return result;
    }
    /*
     thumbnailテーブルへデータを追加・更新
     戻り値:true 新規、false、既存
    */
    static bool UpdateThumbnailOne(ref ThumbnailTbl record)
    {

        // データベースのサムネイル情報を取得
        ThumbnailTbl old = new()
        {
            filename = record.filename,
            lastmodified = record.lastmodified,
        };
        if (GetThumbnailOne(ref old)) {
            // サムネイル情報がある場合
            // idをセットして
            record.id = old.id;
            // リターン
            return false;
        }

        using var cmd = new SQLiteCommand(conn);
        // サムネイル情報がない場合・追加
        cmd.CommandText = $"INSERT INTO thumbnail (filename, lastmodified) values ('{record.filename}','{record.lastmodified}')";
        cmd.ExecuteNonQuery();
        // 更新結果を取得
        return GetThumbnailOne(ref record);
    }

    /*
     画像を指定サイズに縮小する
    */
    static System.Drawing.Bitmap ResizeImage(System.Drawing.Image src, int width=384, int height=384)
    {
        // 戻り値用のBitmapオブジェクトの生成
        var result = new System.Drawing.Bitmap(width, height);
        // グラフィックオブジェクトを取得
        using var g = Graphics.FromImage(result);

        // 縦横の比率から縮小率を計算
        double fw = (double)width / (double)src.Width;
        double fh = (double)height / (double)src.Height;
        double scale = Math.Min(fw, fh);

        // 縮小後の幅と高さを計算
        int w2 = (int)(src.Width * scale);
        int h2 = (int)(src.Height * scale);

        // 縮小画像を中央に描画
        g.DrawImage(src, (width-w2)/2, (height-h2)/2, w2, h2);

        return result;
    }
    
    /*
     zipファイルに画像を追加
    */
    static void AddBitmapToZip(string zipFile, ref Bitmap bmp, string entryName)
    {
        // zipファイルを更新モードで開く
        using var zip = ZipFile.Open(zipFile, ZipArchiveMode.Update);
        // zipファイル内のファイル(エントリー)を作成
        var ns = Path.GetFileNameWithoutExtension(entryName) + ".jpg";
        var entry = zip.CreateEntry(ns, CompressionLevel.NoCompression); // 無圧縮
        // エントリーへ書き込むストリームを作成
        using var fs = entry.Open();
        // ビットマップをzipファイルへ保存
        bmp.Save(fs, ImageFormat.Jpeg);
    }

    /*
     zipファイルから画像を取得
    */
    static Image? GetBitmapFromZip(string zipFile, string entryName)
    {
        // zipファイルを更新モードで開く
        using var zip = ZipFile.Open(zipFile, ZipArchiveMode.Read);
        // zipファイル内のファイル(エントリー)を作成
        var ns = Path.GetFileNameWithoutExtension(entryName) + ".jpg";
        var entry = zip.GetEntry(ns);
        if (entry is null ) return null;
        // エントリーへ書き込むストリームを作成
        using var fs = entry.Open();
        // ビットマップを読み込み
        return Bitmap.FromStream(fs);
    }
    /*
     サムネイル画像を取得
    */
    public static Image? GetThumnailImage(string filename)
    {
        // ファイルの情報
        var fileInfo = new FileInfo(filename);

        // データベースへ接続
        ConnectDatabase();

        ThumbnailTbl tbl = new()
        {
            filename = fileInfo.FullName,
            lastmodified = fileInfo.LastWriteTime.ToString("yyyy-MM-dd HH:mm:ss"),
        };
        // データベースの更新
        if (UpdateThumbnailOne(ref tbl) == false) {
            // 既存
            string entryName = tbl.id.ToString();
            Debug.Print($"既存:{filename} {entryName}");
            var img = GetBitmapFromZip(ZIP_FILE, entryName);
            // 切断
            DisconnectDatabase();
            return img;
        } else {
            // 新規追加
            string entryName = tbl.id.ToString();
            using var bmp = Bitmap.FromFile(filename);
            var smallBmp = Thumbnail1.ResizeImage(bmp, Thumbnail1.Width, Thumbnail1.Height);
            Debug.Print($"新規:{filename} {entryName}");
            AddBitmapToZip(ZIP_FILE, ref smallBmp, entryName);
            // 切断
            DisconnectDatabase();
            return smallBmp;
        }

    }
}

当初は元画像の更新日付が更新されるとテーブルとzipファイルを更新するつもりでいたのですが、面倒なので更新日付が更新された場合新たなファイルとして追加する仕様にしました。元画像の削除された場合もテーブルはそのままの仕様となっています。
ファイルサイズが際限なく大きくなりますので上限を設定するルーチンを組めたら良いなと思っています。

コメント