😊

作画資料管理ツールを作りたい #07 フォルダ内ファイルのサムネイル一覧【高速化の検討④】BackGroundWorker

に公開

はじめに

作りたいもの

作画資料管理ツール

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

↑ここまでで一旦完成

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

↑これは発展

シリーズ記事一覧

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

前回

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

  • ListViewの仮想モードを改善

今回

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

  • BackGroundWorkerを使った非同期処理

能力

  • 初心者
  • 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
BackgroundWorker (Name) backgroundWorkerThums
WorkerReportsProgress true
WorkerSupportsCancellation true
ProgressBar (Name) progressBar1

選択画像のプレビュー

3. ソースコード

フィールド

次のように変更した。

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

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

private Image[] bgImges; //バックグラウンドでサムネイル作成用

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

private int progressCnt; //プログレスバー用カウンタ

イベントハンドラ

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

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

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

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

  5. listViewThumbnail_RetrieveVirtualItem \textcolor{red}{←変更}
    前回、サムネイル作成とListViewのItem作成する処理をを分け、なんとか仮想モードを使えるようになった。
    しかし、サムネイル作成はこのメソッドの中で毎回おこなっているのでもっさり重たかった。
    そこで今回新たに追加した'backGroundWorkerThums_DoWork'側でサムネイル一覧を作成するようにした。

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)
    {

        if (bgImges[e.ItemIndex] == null)
        {
            //何もしない
        }
        else
        {
            imageListThumbnail.Images.Add(bgImges[e.ItemIndex]);
            data.ImageIndex = imageListThumbnail.Images.Count - 1;
        }
    }

    e.Item = lvVirtualItems[e.ItemIndex];
}
  1. backGroundWorkerThums_DoWork \textcolor{red}{←追加}
    非同期処理を行う。
    サムネイル一覧作成用のメソッドを呼び出す。
backGroundWorkerThums_DoWork
private void backGroundWorkerThums_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
{
    System.Diagnostics.Debug.WriteLine($"■■■backGroundWorker1_DoWork");

    bgLoadImgs();


}
  1. backgroundWorker1_ProgressChanged \textcolor{red}{←追加}
    サムネイル作成の進捗を表示するプログレスバー。
    サムネイル一覧作成用のメソッドの中で値を更新していく。
backgroundWorker1_ProgressChanged
private void backgroundWorker1_ProgressChanged(object sender, System.ComponentModel.ProgressChangedEventArgs e)
{
    progressBar1.Value = e.ProgressPercentage;
}

メソッド

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

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

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

  4. makeListThumbs \textcolor{red}{←変更}
    サムネイル一覧を作成する。
    ファイル名の一覧を別メソッドに分けた。
    バックグラウンド処理を実行するbackgroundWorkerThums.RunWorkerAsync();を追加した。

makeListThumbs
//サムネ一覧構成
private void makeListThumbs(string dir)
{
    System.Diagnostics.Debug.WriteLine($"-- makeListThumbs --");

    try
    {
        itemsFullPath = getNameFullPath(dir);
    }
    catch
    {
        System.Diagnostics.Debug.WriteLine($"例外 -- makeListThumbs ");
        return;
    }


    //RetrieveVirtualItem用
    lvItemsCount = itemsFullPath.Count;

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


    bgImges = new Image[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;

    //サムネ一覧作成のバックグラウンド
    backgroundWorkerThums.RunWorkerAsync();
}
  1. getNameFullPath \textcolor{red}{←追加}
    ファイルパス(フルパス)の一覧を取得する。
getNameFullPath
//画像ファイルパス一覧取得
private List<string> getNameFullPath(string dir) {

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

    List<string> fullpath = System.IO.Directory.GetFiles(dir, "*.*")
        .Where(file => extensions.Any(pattern =>
                                      file.ToLower().EndsWith(pattern))
              ).ToList();

    return fullpath;

}
  1. bgLoadImgs \textcolor{red}{←追加}
    サムネイル一覧を作成する。
    Parallel.Forを使って並列処理していく。
    進捗状況はフォームのスレッドを使うためInvoke()によりプログレスバーを更新する。
bgLoadImgs
private void bgLoadImgs()
{
    progressCnt = 0; //プログレスバー用カウンタ

    Parallel.For(0, itemsFullPath.Count, i =>
    {
    //for (int i = 0; i < itemsFullPath.Count; i++) {

        bgImges[i] = resizeImg(itemsFullPath[i], width, height);


        //プログレスバーの更新
        progressCnt++;
        int progressPerc = (int)((double)progressCnt / (double)itemsFullPath.Count * 100);
        Invoke( () => backgroundWorkerThums.ReportProgress(progressPerc));
    //}
    });

}

実行結果

変更前(前回)
https://youtu.be/KZHP95MWMLM



変更後(今回)
https://youtu.be/kEQSqupNXpo

フォルダ展開直後や多少のもたつきはあるけれど、普通に許せるのではないだろうか。
スクロールバーや矢印キーにも前回よりだいぶ追いついてくれるようになった。
ただ、BackGroundWorkerは現在では非推奨ということらしいので、Task/async/awaitの方法に置き換えた方がよさそう。
あとおまけでプログレスバーを付けてみたが、Parallel.Forにすると処理は完了しても100%にならない。
今は重要ではないのでそのうち改善しよう。

参考にさせていただきました

https://youtu.be/T9ueKIJq_LM

https://youtu.be/H5ojVgZj_8U

https://qiita.com/longlongago_k/items/8f19d84fce6dd677922e

ソースコード全体

(省略)

記事が長くなってしまうしコピペが面倒になってきた。github?を使った方がいいのかな…

おわり

世の中のツールと比べるとまだまだ及びませんが、個人的には使いものになりそうな感じです。
今回の内容をTask/async/awaitの方法に変更できたら、一旦サムネ一覧の高速化は終えて、データベース連携に着手していこうと思います。

Discussion