ListViewで大量データを扱った場合に処理が遅くなってしまったので、仮想化を試してみました。
しかし、処理速度がかなり早くなったと喜んでみたものの、ソートをしようとカラムヘッダをクリックしたら異常終了。調べてみたところ、ListViewの仮想化をするとソート処理ができなくなるらしいです・・・。
とりあえず、ListView仮想化時のソート処理を頑張って実装したので、メモとして残しておきます。
プログラム作成
ListView仮想化時のソート処理を作成していきます。
実装内容
実装したい処理は、以下になります。
- ListViewのカラムヘッダをクリックして、ソートを行う。
- ソート処理は、カラムヘッダのクリックイベントから呼び出す。
- 各カラムごとに、文字列、数値、日時、自然検索から選択したソートが行える。
ソートクラス
ソートクラスには、クリックされたカラム(縦列)のデータをソートする処理を実装します。
ソート比較条件は、以下になります。
- 文字列比較
- 整数比較
- 日時比較
- 自然検索
基本的には、文字列を変換しながら比較する感じです。
public class ListViewSort
{
/// <summary>
/// 2 つの Unicode 文字列を比較
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
[System.Runtime.InteropServices.DllImport("shlwapi.dll",
CharSet = System.Runtime.InteropServices.CharSet.Unicode,
ExactSpelling = true)]
private static extern int StrCmpLogicalW(string x, string y);
private int lastSortCol = 0; // 前回ソートカラム
private SortOrder lastSortOrder = SortOrder.Ascending; // 前回ソート向き
private List<CompareType>? lstType = null; // 比較種類リスト
/// <summary>
/// 比較種類
/// </summary>
public enum CompareType
{
typeStr, // 文字列
typeInt, // 整数
typeDateTime, // 日時
typeNature // 自然順
}
/// <summary>
/// 比較種類追加(カラムごと)
/// </summary>
/// <param name="type">比較種類</param>
public void addCompareType(CompareType type)
{
if (lstType == null)
{
lstType = new List<CompareType>();
}
lstType.Add(type);
}
/// <summary>
/// リストソート
/// </summary>
/// <param name="tagList">ListView仮想化 リストデータ</param>
/// <param name="col">選択カラム番号</param>
/// <returns></returns>
public List<ListViewItem> SortList(List<ListViewItem> tagList, int col)
{
if (tagList == null)
{
return new List<ListViewItem>();
}
// 前回ソートカラムと同じカラムが選択された場合に、ソート向きを切り替える
if (lastSortCol == col)
{
if (lastSortOrder == SortOrder.Ascending)
{
lastSortOrder = SortOrder.Descending;
}
else if (lastSortOrder == SortOrder.Descending)
{
lastSortOrder = SortOrder.Ascending;
}
}
lastSortCol = col;
if (lstType != null && lstType.Count > col)
{
switch (lstType[col])
{
case CompareType.typeStr: // 文字列
tagList.Sort((a, b) => CompareStr(a, b, col, lastSortOrder));
break;
case CompareType.typeInt: // 数値
tagList.Sort((a, b) => CompareImt(a, b, col, lastSortOrder));
break;
case CompareType.typeDateTime: // 日時
tagList.Sort((a, b) => CompareDateTime(a, b, col, lastSortOrder));
break;
case CompareType.typeNature: // 自然検索
tagList.Sort((a, b) => CompareNature(a, b, col, lastSortOrder));
break;
default:
break;
}
}
return tagList;
}
/// <summary>
/// 文字列ソート
/// </summary>
/// <param name="x">比較データ</param>
/// <param name="y">比較データ</param>
/// <param name="col">選択カラム番号</param
/// <param name="order">ソート向き</param>
/// <returns></returns>
private int CompareStr(object x, object y, int col, SortOrder order)
{
ListViewItem a = (ListViewItem)x;
ListViewItem b = (ListViewItem)y;
int ret = a.SubItems[col].Text.CompareTo(b.SubItems[col].Text);
if (order == SortOrder.Descending)
{
ret = -ret;
}
return ret;
}
/// <summary>
/// 数値ソート
/// </summary>
/// <param name="x">比較データ</param>
/// <param name="y">比較データ</param>
/// <param name="col">選択カラム番号</param
/// <param name="order">ソート向き</param>
/// <returns></returns>
private int CompareImt(object x, object y, int col, SortOrder order)
{
ListViewItem a = (ListViewItem)x;
ListViewItem b = (ListViewItem)y;
Int32.TryParse(a.SubItems[col].Text, out int numA);
Int32.TryParse(b.SubItems[col].Text, out int numB);
int ret = numA.CompareTo(numB);
if (order == SortOrder.Descending)
{
ret = -ret;
}
return ret;
}
/// <summary>
/// 日時ソート
/// </summary>
/// <param name="x">比較データ</param>
/// <param name="y">比較データ</param>
/// <param name="col">選択カラム番号</param
/// <param name="order">ソート向き</param>
/// <returns></returns>
private int CompareDateTime(object x, object y, int col, SortOrder order)
{
ListViewItem a = (ListViewItem)x;
ListViewItem b = (ListViewItem)y;
DateTime.TryParse(a.SubItems[col].Text, out DateTime dateA);
DateTime.TryParse(b.SubItems[col].Text, out DateTime dateB);
int ret = dateA.CompareTo(dateB);
if (order == SortOrder.Descending)
{
ret = -ret;
}
return ret;
}
/// <summary>
/// 自然ソート
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <param name="col"></param
/// <param name="order"></param>
/// <returns></returns>
private int CompareNature(object x, object y, int col, SortOrder order)
{
ListViewItem a = (ListViewItem)x;
ListViewItem b = (ListViewItem)y;
int ret = StrCmpLogicalW(a.SubItems[col].Text,
b.SubItems[col].Text);
if (order == SortOrder.Descending)
{
ret = -ret;
}
return ret;
}
}
簡単にプログラムの説明を記載します。
9~12行目で、自然検索を行う「StrCmpLogicalW()」関数を使用するために、DLLからの読み込みを行っています。
22~28行目で、今回作成する検索条件を enum で指定しています。小数比較など行いたい場合は、ここに条件を追加してください。
ここから関数ごとの説明を記載していきます。
addCompareType() 関数
【引数】
- CompareType type:比較種類(enum)
【戻り値】
- なし
カラムごとのソート比較条件を追加する関数になります。
第1引数のtypeをリストに追加するだけですが、カラム順に追加する必要があります。
一番最初に追加した条件が、一番目のカラム(ListViewの左端)のソート条件になります。
SortList() 関数
【引数】
- List tagList:ListView仮想化 リストデータ
- int col:選択カラム番号
【戻り値】
- List<ListViewItem>:ListView表示データ
実際にソート処理を行う関数です。
現在ListViewに表示しているデータをソートして、戻してあげる処理になります。
58~68行目で、選択されたカラムが前回選択されたものと同じ場合は、ソートの昇順と降順を入れ替える処理を行っています。
72~91行目で、実際にソートする比較条件ごとの関数の呼び出しを行っています。
Listのソート処理をラムダ式で記述しています。
よく見るラムダ式の書き方は、以下になります。リストデータを昇順にソートする場合の記述になります。
list.Sort((a, b) => a – b);
ラムダ式を作成するには、ラムダ演算子の左辺に入力パラメーターを指定し、右辺に式を指定します(Microsoftより)。
今回は、左辺の引数がListViewのアイテム(object型)になっていて、右辺の式が関数になっています。
CompareStr() 関数
【引数】
- object x:比較データ
- object y:比較データ
- int col:選択カラム番号
- SortOrder order:ソート向き(昇順 or 降順)
【戻り値】
- int:比較結果
文字列比較を行っています。ラムダ式右辺の式にあたる処理になります。
以下の関数を使用して、比較結果を戻します。
a.Compare(b);
【戻り値】
- 0より小さい値 :a の位置が
b
よりも前です(マイナスの値)。 - 0 : a の位置が、並べ替え順序において
b
と同じです。 - 0 より大きい値 :a の位置が
b
よりも後ろです。または、b が null です。
110行目で比較処理を行っています。
112行目~115行目で降順にソートするために、110行目の比較結果を反転しています。結果がマイナスの場合はプラスに、プラスの場合はマイナスにします。
CompareImt() 関数
CompareStr() 関数と処理内容は、ほぼ同じです。
異なるのは、133~134行目で比較対象の値をInt型に変換してから、136行目で比較を行っていることです。
CompareDateTime() 関数
CompareStr() 関数と処理内容は、ほぼ同じです。
異なるのは、159~160行目で比較対象の値をDateTime型に変換してから、162行目で比較を行っていることです。
CompareNature() 関数
CompareStr() 関数と処理内容は、ほぼ同じです。
異なるのは、185行目で StrCmpLogicalW() 関数を呼び出していることです。
使用方法
作成したクラスを実際に使用してみます。
最初に、Windowsフォームアプリで、以下の画面を作成します。
画面には、以下の部品を配置しています。
- ListView : listView1
以下のプログラムを記述します。
public partial class Form1 : Form
{
// リストビューデータ
private List<ListViewItem> listItem = new List<ListViewItem>();
// リストソートクラス
private ListViewSort lvSort;
// テストデータ配列
private string[,] aryTestData = new string[10, 4] {
{ "a001", "100", "2022/01/02 01:02:03", "2"},
{ "a002", "50", "2022/11/03 01:02:03", "10"},
{ "b001", "111", "2022/01/01 01:02:03", "21"},
{ "b002", "33", "2022/03/03 01:02:03", "3"},
{ "b003", "a2", "2021/01/01 01:02:03", "30"},
{ "c001", "3", "2022/02/22 01:02:03", "111"},
{ "c002", "50", "2023/01/01 01:02:03", "1"},
{ "d001", "11", "2020/03/21 01:02:03", "22"},
{ "e002", "1", "2022/12/12 01:02:03", "01"},
{ "f003", "99", "2022/01/01 10:10:10", "05"},
};
/// <summary>
/// コンストラクタ
/// </summary>
public Form1()
{
InitializeComponent();
}
/// <summary>
/// フォームロード
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Form1_Load(object sender, EventArgs e)
{
// リストビューのヘッダー設定
listView1.View = View.Details;
listView1.Clear();
listView1.Columns.Add("文字列", 100, HorizontalAlignment.Left);
listView1.Columns.Add("数値", 100, HorizontalAlignment.Right);
listView1.Columns.Add("日時", 200, HorizontalAlignment.Right);
listView1.Columns.Add("自然検索", 100, HorizontalAlignment.Left);
// リストビューデータ表示イベント設定
listView1.RetrieveVirtualItem += new RetrieveVirtualItemEventHandler(listView1_RetrieveVirtualItem);
// 仮想モード オン
listView1.VirtualMode = true;
listView1.VirtualListSize = 0;
// テストデータ追加
for (int i = 0; i < 10; i++)
{
ListViewItem item = new ListViewItem(aryTestData[i, 0]);
// 各列の内容を設定
item.SubItems.Add(aryTestData[i, 1]);
item.SubItems.Add(aryTestData[i, 2]);
item.SubItems.Add(aryTestData[i, 3]);
listItem.Add(item);
}
listView1.VirtualListSize = listItem.Count;
// リストビュー カラムクリックイベント設定
this.listView1.ColumnClick += new ColumnClickEventHandler(ColumnClick);
// リストソートクラス設定
lvSort = new ListViewSort();
lvSort.addCompareType(ListViewSort.CompareType.typeStr); // 1カラム目 文字列ソート
lvSort.addCompareType(ListViewSort.CompareType.typeInt); // 2カラム目 数値ソート
lvSort.addCompareType(ListViewSort.CompareType.typeDateTime); // 3カラム目 日時ソート
lvSort.addCompareType(ListViewSort.CompareType.typeNature); // 4カラム目 自然検索
}
/// <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();
}
e.Item = listItem[e.ItemIndex];
}
}
/// <summary>
/// リストビューカラムクリックイベント
/// </summary>
/// <param name="o"></param>
/// <param name="e"></param>
private void ColumnClick(object? o, ColumnClickEventArgs e)
{
SortManager(e.Column);
}
/// <summary>
/// ソート管理
/// </summary>
/// <param name="col"></param>
private void SortManager(int col)
{
// リストソート
listItem = lvSort.SortList(listItem, col);
// リフレッシュ
listView1.Refresh();
}
}
簡単にプログラムの説明を記載します。
36~77行目のフォームロードで、ListViewの初期設定とデータ設定を行っています。
39~44行目で、ListViewのヘッダカラムの設定を行っています。
47~51行目で、ListViewを仮想化するための設定を行っています。
54~66行目で、ListViewにテストデータの設定を行っています。
69行目で、ListViewのヘッダカラムをクリックした際のイベント設定を行っています。
72~76行目で、ListViewSortクラスの設定を行っています。
72行目でインスタンスを作成して、73~76行目のaddCompareType()でソートの比較種類を追加しています。ListViewの1番左端のカラムから追加しています。
84~95行目は、ListView仮想化時のデータ表示処理です。
102~105行目のカラムヘッダをクリックした際のイベントでは、SortManager()関数を呼び出しています。引数には、クリックしたカラムヘッダの番号を渡しています。
111~118行目の SortManager()関数では、作成したListViewSortクラスのソート処理「SortList()」を呼び出しています。
カラムヘッダがクリックしたらListViewSortクラスのソート処理を呼び出して、ListViewをリフレッシュしている感じです。
実行結果
上記で作成したプログラムを実行してみます。
テストデータがListViewに表示されました。仮想化が正常に動作しているようです。
それでは、ソートの確認をしてみます。
まず最初に、文字列カラムのヘッダをクリックしてみます。
文字列カラムのデータが降順にソートされました。もう一度クリックすると昇順に戻ります。正常に動作したようです。
次に数値カラムのヘッダをクリックしてみます。
次に日時カラムのヘッダをクリックしてみます。
日時で昇順にソートされました。日時データに変換してから比較できているようですが、文字列比較でも同じ結果になりそうな気もします。
続いて自然検索カラムのヘッダをクリックしてみます。
「1」の次に「11」が並ばず、ちゃんと自然な感じで昇順ソートされています。このソート方式は、ファイル名のソートなどで便利かもしれません。
全体的に、ちゃんとソートできたみたいです。ListView仮想化時のソートがとりあえず動作するようになりましたが、この方法が正しいのかは良くわかりません。
他の方法もありそうな気もします。
とりあえず機能を満たせたようなので、よかったです。