作画資料管理ツールを作りたい #05 フォルダ内ファイルのサムネイル一覧【高速化の検討②】ListViewの仮想モード
はじめに
作りたいもの
作画資料管理ツール
- 画像ファイルをタグで分類できる(pixivやpinterestみたいに)
- サムネイル一覧できる
- 選択画像のプレビューができる
↑ここまでで一旦完成
- 動画も扱う
- 簡単な画像処理もできる(動画のフレームやトリミングもできたらいいな)
- LLM?とかで類似画像の検索ができる
↑これは発展
シリーズ記事一覧
前回
サムネイル一覧表示を高速化する。
- Parallel.For
今回
サムネイル一覧表示を高速化する。
- 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. ソースコード
イベントハンドラ
-
Form1_Load
TreeViewの最上位にドライブ一覧を構成する。 -
treeViewDirectory_BeforeExpand
展開ドライブまたはフォルダの配下のフォルダ一覧を構成する。 -
treeViewDirectory_NodeMouseClick
選択フォルダ内の画像ファイル一覧を作り、サムネイルで表示する。 -
listViewThumbnail_SelectedIndexChanged
選択画像ファイルをプレビューする。 -
listViewThumbnail_RetrieveVirtualItem
\textcolor{red}{←追加}
仮想モードで、Form上のListViewの表示範囲に対して反応するらしい?
lvVirtualItemsは配列ListViewItem[]で作成したもの。
サムネ一覧作成のmakeListThumbs()メソッドでファイル名と画像インデックスを設定している。
private void listViewThumbnail_RetrieveVirtualItem(object sender, RetrieveVirtualItemEventArgs e)
{
e.Item = lvVirtualItems[e.ItemIndex];
}
メソッド
-
addTree_Drive
フォルダツリーの最上位(ドライブ一覧)を構成する。 -
addTree_Child
フォルダツリーの選択フォルダ内のサブフォルダを構成する。 -
resizeImg
画像をリサイズする。 -
makeListThumbs
\textcolor{red}{←変更}
サムネイル一覧を作成する。
ListViewItem配列はローカル変数で扱っていたが、
仮想モードにするためForm1内で使えるようにフィールドに宣言した。
仮想モードに伴い、imageListThumbnail.Images.AddRange(images);が使えなくなったので、
サムネ作成用forループ内でImageListのAdd()メソッドで追加している。
// RetrieveVirtualItemy用
int lvItemsCount; // ListViewItem数
ListViewItem[] lvVirtualItems; //ファイル名と画像インデックス
- Image[] images; //サムネ
//サムネ一覧構成
private void makeListThumbs(string dir)
{
System.Diagnostics.Debug.WriteLine($"-- makeListThumbs --");
//表示対象のファイル形式
string[] extensions = { ".jpg", ".jpeg", ".png", ".tif", ".jfif", ".bmp" };
var itemsFullPath = new List<string>(); //ファイルのフルパス格納用
try
{
itemsFullPath = System.IO.Directory
.GetFiles(dir, "*.*")
.Where(file => extensions.Any(pattern => file.ToLower()
.EndsWith(pattern)
)
).ToList();
}
catch
{
System.Diagnostics.Debug.WriteLine($"例外 -- makeListThumbs ");
return;
}
System.Diagnostics.Debug.WriteLine($"itemsFullPath: {itemsFullPath}");
+ //RetrieveVirtualItem用
+ lvItemsCount = itemsFullPath.Count;
+ //RetrieveVirtualItem用
+ lvVirtualItems = new ListViewItem[lvItemsCount];
//サムネイル一覧の初期化
listViewThumbnail.Items.Clear();
imageListThumbnail.Images.Clear();
//ListViewに追加するItem
var items = new ListViewItem[itemsFullPath.Count];
// サムネイル用配列
- images = new Image[lvItemsCount];
//ImageListに表示するサムネのサイズ
int width = 50, height = 50;
imageListThumbnail.ImageSize = new Size(width, height);
// *** 時間計測用 ************************************
// Stopwatchクラス生成
System.Diagnostics.Stopwatch sw = new();
// 計測開始
sw.Start();
// *** 時間計測用 ************************************
//画像ファイルを1つずつ処理
Parallel.For(0, lvItemsCount, i =>
{
//フルパスからファイル名のみ抽出
string fileName = System.IO.Path.GetFileName(itemsFullPath[i]);
//サムネイル作成→サムネ用配列に格納
- images[i] = resizeImg(itemsFullPath[i], width, height);
+ // 仮想モード
+ imageListThumbnail.Images.Add( resizeImg(itemsFullPath[i], width, height) );
//サムネのファイル名追加
- items[i] = new ListViewItem(fileName, i); //ファイル名, イメージインデックスを指定
+ // 仮想モード
+ lvVirtualItems[i] = new ListViewItem(fileName, i); //ファイル名, イメージインデックスを指定
});
// *** 時間計測用 ************************************
// 計測停止
sw.Stop();
// 結果表示
System.Diagnostics.Debug.WriteLine("*** 計測時間 ***");
TimeSpan ts = sw.Elapsed;
System.Diagnostics.Debug.WriteLine($"{ts.Hours}時間 {ts.Minutes}分 {ts.Seconds}秒 {ts.Milliseconds}ミリ秒");
// *** 時間計測用 ************************************
//サムネが入った配列をImageListに追加→ListViewのLargeImageListに設定
- imageListThumbnail.Images.AddRange(images);
listViewThumbnail.LargeImageList = imageListThumbnail;
+ // 仮想モードON
+ listViewThumbnail.VirtualMode = true;
+ // Item数設定
+ listViewThumbnail.VirtualListSize = lvItemsCount;
}
実行結果
前回、Parallel.Forを使ったときと比較して、ほぼ高速化できていない。
サムネの作成がなければ多分高速になるのだろう(参考サイトのコードを試すと高速化できていた)。
また、マウスカーソルがListView上にある間は頻繁に仮想モードのイベントが発生しているのか、コントロール内の描画がチラつくし、動作も重たい。
今回は、なんなら仮想モードを使わない方が使いやすい。
きっと書き方、使い方が間違っているのかもしれないがよくわからない。
ソースコード全体
(省略)
参考にさせていただきました
おわり
今回は仮想モードを試してみましたが、ImageListとの相性が良くないだろうかと疑っています。
使い方を間違っているだけなら希望が生まれるのですがよくわりません。
task/async/awaitで非同期処理?も試してみたいと思います。
あと、読み込みながらListViewを表示していけるようなものがいいのかなと思います。
それも一度読み込めば再読み込みしなくてもいいような作りにしたいです。
Discussion
高速化のヒケツは「まずは計測」です。
現在は
Stopwatchで計っているだけなので、ざっくりこの辺が重い、は分かりますが重い原因までは分かりません。ですが「重くなる原因」にはCPU負荷、GPU負荷、ファイルやネットワークのI/O、メモリ消費、コードのアルゴリズムなどいろんなものがあります!VisualStudioをお使いのようですので、プロファイリングツールを使って計測することをおすすめします!
またこちらの記事を一読していただくことをおすすめします!
まだまだIDEを使った開発に不慣れなので少しずつ試してみます!参考記事のご紹介ありがとうございます。
サムネイルをUIスレッドでループで一気に作成するとそこで描画が停止してしまうので、画像のロードもRetrieveVirtualItemで行う必要があるでしょうね。表示した分だけ逐次ロードが実行される筈です。
バックグラウンドでロードしたいなら、Task.Runで別Taskを起動してロードしたり一工夫必要になると思います。
下記はAIに出力させたやつですけど、雰囲気としては伝わるでしょうか。
1000枚でもすぐ表示されます。
コメントありがとうございます。非同期処理の方も試していますがスレッドの扱い方とか理解できておらず迷子になっていました。
ひとまず、記載いただいたコードを参考にもう一度仮想モードで試してみます!