😊

作画資料管理ツールを作りたい #06 フォルダ内ファイルのサムネイル一覧【高速化の検討③】ListViewの仮想モード(改善)

に公開

はじめに

作りたいもの

作画資料管理ツール

  • 画像ファイルをタグで分類できる(pixivやpinterestみたいに)
  • サムネイル一覧できる
  • 選択画像のプレビューができる

↑ここまでで一旦完成

  • 動画も扱う
  • 簡単な画像処理もできる(動画のフレームやトリミングもできたらいいな)
  • LLM?とかで類似画像の検索ができる

↑これは発展

シリーズ記事一覧

https://zenn.dev/indonegiyan/articles/77062311f8755e

前回

サムネイル一覧表示を高速化する。

  • ListViewの仮想モード
    →失敗

今回

サムネイル一覧表示を高速化する。

  • 前回のListViewの仮想モードを改善

能力

  • 初心者
  • C#とフォームアプリの作り方を勉強する

1. 環境

  • Windows 11 pro
  • Visual Studio Community 2022 (Version 17.14.12)
  • プロジェクトの種類:Windowsフォームアプリ

2. フォームデザイン

主なコントロールと内容

コントロール プロパティ 内容
Form (Name) Form1
TreeView (Name) treeViewDirectory
ListView (Name) listViewThumbnail
View LargeIcon
ImageList (Name) imageListThumbnail
PictureBox (Name) pictureBoxPreview
BackColor Silver

選択画像のプレビュー

3. ソースコード

フィールド

次のように変更した。

フィールド
private string selectedDir = ""; //選択中のディレクトリ

private List<string> itemsFullPath = new (); //ファイルのフルパス格納用
private int width = 200, height = 200; //サムネイルサイズ

// RetrieveVirtualItemy用
private int lvItemsCount; // ListViewItem数
private ListViewItem[] lvVirtualItems; //ファイル名と画像インデックス

そういえば、C#ではクラス内の変数はフィールドって呼ぶのだろうか?プロパティとか呼ぶこともあるようだけど何が違うのかな。後で調べてみよう。

イベントハンドラ

  1. Form1_Load
    TreeViewの最上位にドライブ一覧を構成する。

  2. treeViewDirectory_BeforeExpand
    展開ドライブまたはフォルダの配下のフォルダ一覧を構成する。

  3. treeViewDirectory_NodeMouseClick
    選択フォルダ内の画像ファイル一覧を作り、サムネイルで表示する。

  4. listViewThumbnail_SelectedIndexChanged
    選択画像ファイルをプレビューする。

  5. listViewThumbnail_RetrieveVirtualItem \textcolor{red}{←変更}
    仮想モードで、Form上のListViewの表示範囲に対して反応するらしい?
    前回の失敗例では、サムネ一覧作成用のメソッドを呼び出し、同じforループ内でListViewItem[]とImageListに追加していた。これが同じスレッドの処理?になっているらしく結局一切高速化に繋がっていなかった。
    そこで今回はlistViewThumbnail_RetrieveVirtualItem側でサムネを作成して、一覧表示できるように変更した。
    このあたりは前回記事にいただいたコメントを参考にしました。ありがとうございます。

https://zenn.dev/link/comments/c69e0ffc8dc2e8

listViewThumbnail_RetrieveVirtualItem
private void listViewThumbnail_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
{
    System.Diagnostics.Debug.WriteLine($"■■■listViewThumbnail_RetrieveVirtualItem");

    var data = lvVirtualItems[e.ItemIndex];
    
    // 画像がまだダミーなら読み込み
    if (data.ImageIndex == 0)
    {
        using (var img = resizeImg(itemsFullPath[e.ItemIndex], width, height))
        {
            imageListThumbnail.Images.Add(img);
            data.ImageIndex = imageListThumbnail.Images.Count - 1;
        }
    }

    e.Item = lvVirtualItems[e.ItemIndex];
}

メソッド

  1. addTree_Drive
    フォルダツリーの最上位(ドライブ一覧)を構成する。

  2. addTree_Child
    フォルダツリーの選択フォルダ内のサブフォルダを構成する。

  3. resizeImg
    画像をリサイズする。

  4. makeListThumbs \textcolor{red}{←変更}
    サムネイル一覧を作成する。
    サムネ作成はダミー画像を設定して、正式なサムネ作成及び追加はイベントハンドラ側で行うようにした。

makeListThumbs
private void makeListThumbs(string dir)
{
    System.Diagnostics.Debug.WriteLine($"-- makeListThumbs --");

    //表示対象のファイル形式
    string[] extensions = { ".jpg", ".jpeg", ".png", ".tif", ".jfif", ".bmp" };

    
    try
    {
        itemsFullPath = System.IO.Directory.GetFiles(dir, "*.*")
                        .Where(file => extensions.Any(pattern => 
                                                      file.ToLower().EndsWith(pattern))
                              ).ToList();
    }
    catch
    {
        System.Diagnostics.Debug.WriteLine($"例外 -- makeListThumbs ");
        return;
    }


    //RetrieveVirtualItem用
    lvItemsCount = itemsFullPath.Count;

    //RetrieveVirtualItem用
    lvVirtualItems = new ListViewItem[lvItemsCount];


    //サムネイル一覧の初期化
    listViewThumbnail.Items.Clear();
    imageListThumbnail.Images.Clear();

    imageListThumbnail.ImageSize = new Size(width, height);

    // Index 0: 透明なダミー画像
    Bitmap dummy = new Bitmap(width, height);
    using (Graphics g = Graphics.FromImage(dummy))
    {
        g.Clear(Color.Transparent);
    }
    imageListThumbnail.Images.Add(dummy);



    //画像ファイルを1つずつ処理
    for (int i = 0; i < itemsFullPath.Count; i++)
    //Parallel.For(0, lvItemsCount, i =>
    {
        //フルパスからファイル名のみ抽出
        string fileName = System.IO.Path.GetFileName(itemsFullPath[i]);

        //サムネのファイル名追加
        lvVirtualItems[i] = new ListViewItem(fileName, 0); //ファイル名, イメージインデックスを指定

    //});
    }

    listViewThumbnail.LargeImageList = imageListThumbnail;

    // 仮想モードON
    listViewThumbnail.VirtualMode = true;

    // Item数設定
    listViewThumbnail.VirtualListSize = lvItemsCount;
}

実行結果

3MBのgifに収めたので画質は悪けれど、こんな感じ。
前回から改善して、格段に高速化できた。
しかしながら、実際の使用感としては画面がちらついたり、もっさり重たい。
それに無駄に何度もイベントが呼び出されている。
スクロールや素早い方向キーでの移動をすると描画が追いつかない。
我慢できなくもないが…気持ちよくはない。
もしかすると、サムネイルを追加する際ファイルを読み込みリサイズ処理を毎回おこなっているので多少なり影響しているのかもしれない。
裏でサムネイル作成できるようにしてみよう。
仮想モードや非同期処理についてもっと調べて試してみよう。

ソースコード全体

(省略)

おわり

今回はひとまず一応ギリ使えるんじゃないかレベルまで改善できました。
引き続き高速化の方法を検討していきたいと思います。

Discussion