C#

【C#】ListViewの仮想化をしたけど、ファイル一覧表示時にアイコン表示が重かったので改善メモ

前回は、ListViewの仮想化を行い、大量データでも快適に動作することを確認しました。

それを活かして、エクスプローラのようなファイルの一覧表示を作成し、それを仮想化してみました。

しかし、ファイルが多くなるほど処理が重くなり、表示が遅くなってしまいました。どうやら、ファイル名称の前に表示するアイコン取得の処理が原因のようです。

ListView表示データを作成する部分でアイコンの取得処理も行っていますが、ここで全てのファイルのアイコン取得処理をまとめて行っているため、時間がかかっているようです。

アイコン取得処理を別スレッドで行ったり、処理を分散したりする方法を検討する必要がありそうです。

今回は、処理を分散するという考え方で、ListView仮想化時のデータ表示イベントである「RetrieveVirtualItem」イベントにて、ファイルのアイコン取得を行ってみました。

結果としては、かなり改善したと思われます。

プログラム作成

エクスプローラのようなファイル一覧表示処理を ListViewの仮想化で作成していきます。

実装内容

実装したい処理は、以下になります。

  • 指定したフォルダパス配下のファイルを一覧表示する。
  • ファイル名称とアイコンを表示する。

プログラム作成

最初に、Windowsフォームアプリで、以下の画面を作成します。

画面には、以下の部品を配置しています。

  • ListView : listView1
  • TextBox : textBox1
  • Button : button1

以下のプログラムを記述します。ちょっと長いので、分割して記述しています。

/// <summary>
/// ファイル情報を取得
/// </summary>
/// <param name="pszPath"></param>
/// <param name="dwFileAttributes"></param>
/// <param name="psfi"></param>
/// <param name="cbFileInfo"></param>
/// <param name="uFlags"></param>
/// <returns></returns>
[DllImport("shell32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SHGetFileInfo(string pszPath, uint dwFileAttributes, out SHFILEINFO psfi, uint cbFileInfo, uint uFlags);

/// <summary>
/// イメージリストを登録
/// </summary>
/// <param name="hWnd"></param>
/// <param name="Msg"></param>
/// <param name="wParam"></param>
/// <param name="lParam"></param>
/// <returns></returns>
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

/// <summary>
/// リソース解放
/// </summary>
/// <param name="hIcon"></param>
/// <returns></returns>
[DllImport("user32.dll", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
public static extern bool DestroyIcon(IntPtr hIcon);

/// <summary>
/// SHGetFileInfo関数で使用する構造体
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
private struct SHFILEINFO
{
	public IntPtr hIcon;
	public int iIcon;
	public uint dwAttributes;
	[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
	public string szDisplayName;
	[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
	public string szTypeName;
};

// SHGetFileInfo関数で使用するフラグ
private const uint SHGFI_ICON = 0x00000100;			// アイコン・リソースの取得
private const uint SHGFI_LARGEICON = 0x00000000;	// 大きいアイコン
private const uint SHGFI_SMALLICON = 0x00000001;	// 小さいアイコン
private const uint SHGFI_SYSICONINDEX = 0x00004000; // システムイメージリストアイコンのインデックス取得
private const uint SHGFI_TYPENAME = 0x000000400;    // ファイルタイプ説明取得

// ListView用
private const uint LVM_SETIMAGELIST = 0x1003;       // リスト ビュー コントロールにイメージ リストを割り当て
private const uint LVSIL_NORMAL = 0x0000;           // 大きいアイコンイメージを持つイメージリスト
private const uint LVSIL_SMALL = 0x0001;            // 小さいアイコンイメージを持つイメージリスト

private const int LIST_SUBITEM_TYPE = 1;			// ListViewサブアイテム(種別)

// リストビューデータ
private List<ListViewItem> listItem = new List<ListViewItem>();

簡単にプログラムの説明を記載します。

この部分は、外部DLLで今回使用する関数の読み込みや、この関数を使用する際に必要になる構造体と定数を定義しています。


		public Form1()
		{
			InitializeComponent();
		}

		/// <summary>
		/// フォームロード
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void Form1_Load(object sender, EventArgs e)
		{
			InitListView();
		}

		/// <summary>
		/// リストビュー初期化
		/// </summary>
		private void InitListView()
		{
			// リストビューのヘッダーを設定
			listView1.View = View.Details;
			listView1.Clear();
			listView1.Columns.Add("名前", 200, HorizontalAlignment.Left);
			listView1.Columns.Add("種類", 150, HorizontalAlignment.Left);
			listView1.Columns.Add("フォルダ", 150, HorizontalAlignment.Left);
			listView1.Columns.Add("更新日時", 180, HorizontalAlignment.Left);
			listView1.Columns.Add("作成日時", 180, HorizontalAlignment.Left);
			listView1.Columns.Add("サイズ", 80, HorizontalAlignment.Right);

			// ListViewに仮想モードでデータを更新するイベントを追加
			listView1.RetrieveVirtualItem += new RetrieveVirtualItemEventHandler(listView1_RetrieveVirtualItem);

			// 仮想モードをオンに
			listView1.VirtualMode = true;
			listView1.VirtualListSize = 0;

			InitImageList();
		}

		/// <summary>
		/// イメージリスト初期化
		/// </summary>
		private void InitImageList()
		{
			// イメージリストの設定
			SHFILEINFO shFileInfo = new SHFILEINFO();
			IntPtr imageHandle= SHGetFileInfo(String.Empty, 0, out shFileInfo, (uint)Marshal.SizeOf(shFileInfo), SHGFI_SMALLICON | SHGFI_SYSICONINDEX);

			// ListView
			SendMessage(listView1.Handle, LVM_SETIMAGELIST, new IntPtr(LVSIL_SMALL), imageHandle);
		}

ListViewの初期化処理になります。

19~39行目の「InitiListView()」関数は、フォームロードで呼び出されて、ListViewの初期化「ヘッダ作成」、「イベントハンドラーの定義」、「仮想モード設定」を行っています。

処理の最後で呼び出している「InitImageList()」関数は、ListViewのイメージリストの初期化を行っています。

48行目の「SHGetFileInfo()」関数 で、下記に示す引数を設定した情報取得をするように、設定を行っています。

  • SHGFI_SMALLICON : 小さいアイコンを取得
  • SHGFI_SYSICONINDEX : システムイメージリストアイコンのインデックス取得

51行目の「SendMessage()」関数で、ListViewコントロールにイメージリスト(imageHandle)を割り当ててる感じです。

/// <summary>
/// リストビュー項目設定
/// </summary>
/// <param name="path"></param>
private void setListItem(String path)
{
	// フォルダ存在チェック
	if (!Directory.Exists(path))
	{
		return;
	}

	List<ListViewItem> list = new List<ListViewItem>();

	// フォルダ一覧
	DirectoryInfo dirList = new DirectoryInfo(path);

	foreach (var di in dirList.GetDirectories())
	{
		// アイテム追加
		ListViewItem item = new ListViewItem(di.Name);

		// 各列の内容を設定
		item.ImageIndex = -1;
		item.SubItems.Add("");
		item.SubItems.Add(di.FullName);
		item.SubItems.Add(String.Format("{0:yyyy/MM/dd HH:mm:ss}", di.LastWriteTime));
		item.SubItems.Add(String.Format("{0:yyyy/MM/dd HH:mm:ss}", di.CreationTime));
		item.SubItems.Add("");

		item.Tag = di.FullName;

		list.Add(item);
	}

	// ファイル一覧
	List<String> files = Directory.GetFiles(path).ToList<String>();

	foreach (var file in files)
	{

		FileInfo info = new FileInfo(file);

		// アイテム追加
		ListViewItem item = new ListViewItem(info.Name);

		// 各列の内容を設定
		item.ImageIndex = -1;
		item.SubItems.Add("");
		item.SubItems.Add(info.DirectoryName);
		item.SubItems.Add(String.Format("{0:yyyy/MM/dd HH:mm:ss}", info.LastWriteTime));
		item.SubItems.Add(String.Format("{0:yyyy/MM/dd HH:mm:ss}", info.CreationTime));
		item.SubItems.Add(info.Length.ToString());

		item.Tag = info.FullName;

		list.Add(item);
	}

	listItem = list;
	listView1.VirtualListSize = listItem.Count;
}

/// <summary>
/// 表示ボタン押下処理
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button1_Click(object sender, EventArgs e)
{
	setListItem(textBox1.Text);
}

ListViewに表示する一覧データを作成する処理になります。

69~72行目のボタンクリックイベントで、「setListItem()」関数が呼ばれて、「textBox1.text」に入力されたフォルダパス配下のデータ一覧を作成します。

16~34行目で、フォルダの一覧データを作成しています。

37~58行目で、ファイルの一覧データを作成しています。

60~61行目で、作成したデータをListView表示データに置き換えています。

最初は、24行目と48行目の「item.ImageIndex = -1」の前で、アイコンを取得して「item.ImageIndex」に設定するような処理になっていましたが、ここでは初期化だけとして、「-1」を設定しています。

ここで、全てのデータのアイコン取得処理を実行すると、かなり時間がかかります。

31行目と55行目では、ファイルのフルパスを設定しています。このパスは、「RetrieveVirtualItem」でアイコンを取得する際に使用しています。必要なデータですが、ListViewに表示させることがないので、隠しデータ扱いとしています。

「item.Tag」は、ListViewに表示するデータ以外を設定できるなど、とても便利です。

/// <summary>
/// リストビューデータ表示
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void listView1_RetrieveVirtualItem(object? sender, RetrieveVirtualItemEventArgs e)
{
	if (e.ItemIndex < listItem.Count)
	{
		if (e.Item == null)
		{
			e.Item = new ListViewItem();
		}

		if (listItem[e.ItemIndex].ImageIndex < 0)
		{
			// フォルダ種類、アイコンの取得
			string path = (string)listItem[e.ItemIndex].Tag;
			string type = "";
			int iconIndex = 0;

			// アイコン取得
			GetIcon(path, out type, out iconIndex);

			listItem[e.ItemIndex].ImageIndex = iconIndex;
			listItem[e.ItemIndex].SubItems[LIST_SUBITEM_TYPE].Text = type;
		}

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

/// <summary>
/// アイコン・ファイル種類取得
/// </summary>
/// <param name="path"></param>
/// <param name="type"></param>
/// <param name="iconIndex"></param>
private void GetIcon(string path, out string type, out int iconIndex)
{
	type = "";
	iconIndex = -1;

	// ファイル情報を取得
	SHFILEINFO shFileInfo = new SHFILEINFO();
	IntPtr hSuccess = SHGetFileInfo(path, 0, out shFileInfo,
						(uint)Marshal.SizeOf(shFileInfo),
						SHGFI_ICON | SHGFI_LARGEICON | SHGFI_SMALLICON | SHGFI_SYSICONINDEX | SHGFI_TYPENAME);
	if (hSuccess != IntPtr.Zero)
	{
		if (shFileInfo.szTypeName != null)
		{
			type = shFileInfo.szTypeName;
		}
		iconIndex = shFileInfo.iIcon;
	}

	// アイコン破棄・メモリ解放
	DestroyIcon(shFileInfo.hIcon);
}

    ListView仮想化時のデータ設定と、アイコン取得処理になります。

    6~31行目の「listView1_RetrieveVirtualItem()」関数で、アイコン取得とListViewのデータ設定を行っています。

    15~27行目の「setListItem()」関数で初期化している「ImageIndex」の値が初期値の「-1」の場合だけ、アイコン取得処理を行います。取得されるとアイコンの値が「ImageIndex」に設定されるので、次回から処理を行わないようになります。

    実際のアイコン取得処理は、39~60行目の「GetIcon()」関数で行っています。

    46行目の「SHGetFileInfo()」が、アイコンとファイル種別を取得する処理になります。

    また、上記処理を行った場合は、必ず「DestroyIcon()」を使用して、メモリの解放を行わないとメモリリークを起こすので、注意してください。

    実行結果

    上記で作成したプログラムを実行してみます。

    テキストボックスにフォルダパスを入力して、表示ボタンを押下します。

    問題なくListViewにアイコン付きのファイル一覧が表示されました。

    表示ボタンを押下してから、ファイルの一覧が表示されるまでの時間が、かなり短縮されました。

    ただ、この方法だとアイコン取得処理の回数が減っただけです。そのため、数万件のデータを表示させようとすると、ファイル一覧のデータ作成に時間がかかってしまい、結局のところ表示が遅くなります。

    大量のデータを高速に作成する処理は、別途検討していきたいと思います。