ListViewへの追加処理の改善

C#

概要

ListView に大量のデータを追加する場合、追加処理に時間がかかり、また追加処理中はフォームが固まり、表示が更新されない状態になります。今回はそれを改善する方法を試します。

ソースコードはこちらで公開しています。
https://github.com/matsushima-terunao/test_cs/tree/main/ListViewMulti

仮想 ListView

ListView.Item.Add で ListView にデータを追加する代わりに、仮想 ListView で自前でデータを持つことで、時間を大幅に短縮できます。

初期化
仮想 ListView では RetrieveVirtualItem イベントハンドラでアイテムの情報を返します。

/// <summary>サムネイル ListViewItem</summary>
private List<ListViewItem> listViewItems = new();
/// <summary>サムネイル ImageList</summary>
private ImageList imageList = new();
/// <summary>タスクキャンセル</summary>
private CancellationTokenSource cancellationTokenSource = null!;

public Form1()
{
	InitializeComponent();

	// ドラッグアンドドロップでアーカイブを開く
	DragDrop += (object? sender, DragEventArgs e) =>
	{
		string[] files = (string[])e.Data?.GetData(DataFormats.FileDrop, false)!;
		ReadFolder(files[0]);
	};
	DragEnter += (object? sender, DragEventArgs e) =>
	{
		e.Effect = ((e.Data?.GetDataPresent(DataFormats.FileDrop)) ?? false) ? DragDropEffects.All : DragDropEffects.None;
	};
	AllowDrop = true;

	// 仮想 ListView
	listView1.RetrieveVirtualItem += (object? sender, RetrieveVirtualItemEventArgs e) =>
	{
		e.Item = listViewItems[e.ItemIndex];
	};
	listView1.VirtualListSize = 0;
	listView1.VirtualMode = true;
}

データ追加
フォルダー配下の画像ファイルのサムネイルを作成して追加する例です。
まずはファイル情報だけを取得してリストに追加します。データ読み込みとサムネイル作成は後で行います。タスクキャンセルと画像読み込み処理については後述します。

/// <summary>
/// フォルダー内のファイル情報を ListView に追加。
/// </summary>
/// <param name="path"></param>
public void ReadFolder(string path)
{
	// タスクキャンセル
	if (null != cancellationTokenSource)
	{
		Debug.WriteLine("cancel");
		cancellationTokenSource.Cancel();
	}

	DateTime dt = DateTime.Now;

	// ListView 更新
	listView1.Items.Clear();
	// サムネイル ImageList
	imageList = new();
	imageList.ImageSize = new Size(100, 100);
	// ダミーイメージ
	var bitmap = new Bitmap(1, 1);
	imageList.Images.Add(bitmap);
	listView1.LargeImageList = imageList;
	// サムネイル ListViewItem
	listViewItems = new();
	foreach (var file in new DirectoryInfo(path).EnumerateFiles("*", SearchOption.AllDirectories))
	{
		var item = new ListViewItem(file.Name);
		item.ImageIndex = 0;
		item.Tag = file;
		listViewItems.Add(item);
	}
	// 仮想 ListView 更新
	listView1.VirtualListSize = listViewItems.Count;
	// 読み込み進捗 ToolStripProgressBar
	toolStripProgressBar1.Minimum = 0;
	toolStripProgressBar1.Maximum = listViewItems.Count;
	toolStripProgressBar1.Value = 0;

	// 画像読み込み処理
	cancellationTokenSource = new();
	var cancelToken = cancellationTokenSource.Token; // 別スレッドからアクセスされるためローカル変数に退避
#if false
	// 画像読み込み処理を実行
	AddThumbnails(cancelToken);
#else
	// 画像読み込み処理を別スレッドで実行
	new Task(() => AddThumbnails(cancelToken), cancelToken).Start();
#endif
	Debug.WriteLine("ReadFolder " + (DateTime.Now - dt).TotalSeconds);
}

データ読み込み処理を別スレッドで行う

データ読み込み処理を別スレッドで行うことで、読み込み処理中にフォームが固まるをの防ぎます。

既に別の読み込み処理が動いているときはそれをキャンセルします。

		// タスクキャンセル
		if (null != cancellationTokenSource)
		{
			Debug.WriteLine("cancel");
			cancellationTokenSource.Cancel();
		}

Task.Start メソッドで別スレッドで画像読み込み処理を実行します。Task コンストラクタの第1パラメーターは別スレッドで実装するデリゲート、第2パラメータ―にはタスクをキャンセルさせるためのキャンセルトークンを渡します。それとは別に画像読み込み処理中にタスクをキャンセルさせるためのキャンセルトークを渡しています。メンバー変数の cancellationTokenSource は再実行時に上書きされるため、タスク内ではパラメーターで渡されたキャンセルトークンを使用するようにしています。

		// 画像読み込み処理
		cancellationTokenSource = new();
		var cancelToken = cancellationTokenSource.Token; // タスク開始前に退避
#if false
		// 画像読み込み処理を実行
		AddThumbnails(cancelToken);
#else
		// 画像読み込み処理を別スレッドで実行
		new Task(() => AddThumbnails(cancelToken), cancelToken).Start();
#endif

データ読み込み処理を並列で行う

Parallel.For メソッドでループを並列で実行します。

/// <summary>
/// 画像読み込み処理。
/// </summary>
private void AddThumbnails(CancellationToken cancelToken)
{
	DateTime dt = DateTime.Now;
#if false
	// 画像読み込み処理を実行
	for (int fileIdx = 0; fileIdx < listViewItems.Count; ++fileIdx)
	{
		AddThumbnail(fileIdx, cancelToken);
	}
#else
	// 並列処理で画像読み込み処理を実行
	var parallelOptions = new ParallelOptions() { CancellationToken = cancelToken };
	try
	{
		Parallel.For(0, listViewItems.Count, parallelOptions, fileIdx =>
		{
			AddThumbnail(fileIdx, cancelToken);
		});
	}
	catch (Exception ex)
	{
		Debug.WriteLine("catch in parallel");
		Debug.WriteLine(ex);
	}
#endif
	Invoke((Action)(() =>
	{
		toolStripProgressBar1.Value = 0;
		toolStripStatusLabel1.Text = "AddThumbnails " + (DateTime.Now - dt).TotalSeconds;
	}));
	Debug.WriteLine("AddThumbnails " + (DateTime.Now - dt).TotalSeconds);
}

コントロールの更新処理

別スレッド(並列処理含む)からコントロールの操作を行う場合は、Control.Invoke メソッドで指定したデリゲート内で実行します。

/// <summary>
/// 画像読み込み処理。
/// </summary>
/// <param name="fileIdx"></param>
private void AddThumbnail(int fileIdx, CancellationToken cancelToken)
{
	try
	{
		// 画像ファイルのサムネイル作成。
		var fileInfo = (FileInfo)listViewItems[fileIdx].Tag;
		var bitmap = CreateThmbnail(fileInfo, imageList.ImageSize.Width);
		{
			// Form のスレッドで ListViewItem 更新
			Invoke((Action<int, Image>)((_fileIdx, _bitmap) =>
			{
				if (!cancelToken.IsCancellationRequested)
				{
					imageList.Images.Add(_bitmap);
					_bitmap.Dispose();
					listViewItems[_fileIdx].ImageIndex = imageList.Images.Count - 1;
					if (listView1.ClientRectangle.IntersectsWith(listViewItems[_fileIdx].Bounds))
					{
						listView1.RedrawItems(_fileIdx, _fileIdx, true);
					}
					++toolStripProgressBar1.Value;
				}
			}), fileIdx, bitmap);
			//Task.Delay(1).Wait();
		}
	}
	catch (Exception ex)
	{
		// 画像ファイルでない
		Debug.WriteLine(ex);
		Debug.WriteLine(listViewItems[fileIdx].Tag);
		Invoke((Action)(() => {
			++toolStripProgressBar1.Value;
		}));
	}
}

コメント

タイトルとURLをコピーしました