【Visual C++ 2008】【MFC】
リストビュー(CListCtrlクラス)は便利なクラスであるが、 一度に多くの行数(千とか万の単位)を登録すると、表示が完了するまで非常に時間がかかる。 その原因として、
- データの入れ物と表示を兼用している
- 登録した全行を表示するまで、待機カーソルの状態で制御を返さない
の2点が考えられる。 後者を補足説明すると、10行分の高さのリストビューに1000行のデータを登録した場合、 ユーザーの目には10行のデータとスクロールバーが見える。 なのに、見えない部分も含めて1000行全部表示するまで制御を返さないような挙動を示すので、遅く感じる。 一度に多くの行数を削除した場合も同様である。
それを解決するため、
- データの入れ物と表示を分離する
- リストビューの高さの分だけ、入れ物から取り出して表示する。スクロールバー を操作する度に、表示に必要なデータのみを入れ物から取り出す
ことにより高速化するのが、仮想リストビューである。 よって、入れ物は別途設計する必要がある。
ちなみに、当方のテスト環境にて約12万行*15列のデータを用いて測定した際、 通常のリストビューでは30秒以上かかるところを、仮想リストビューでは5秒以内で表示した。
通常のリストビューを、仮想リストビューに変更する手順は以下の通り。 (通常のリストビューの実装方法は、 「リストビューのサイズをダイアログのサイズ変更に合わせる」 「【C++/MFC】リストビューの基本的な使い方」 を参照のこと)
(1)
リソースビューにて、該当リストビューの[プロパティ]-[表示]-[Owner Data]をTrueにする(デフォルトはFalse)。
もしくは、rcファイルを直接書き換えるなら、該当するリストビューにLVS_OWNERDATAを追加する。
(2)
クラスヘッダのprotectedに、メンバ関数定義を追加。(関数名は任意)
afx_msg void OnGetDispInfo(NMHDR* pNMHDR, LRESULT* pResult);
(3)
メッセージマップに、ON_NOTIFY(LVN_GETDISPINFO, IDC_LIST1, OnGetDispInfo)を追加。
(4)
メンバ関数を記述。
リストビューにメッセージが飛ぶ(高さ分だけデータ表示、カーソル選択、スクロールバー操作等)度に、この関数が呼ばれる。
void CXxxxDlg::OnGetDispInfo(NMHDR* pNMHDR, LRESULT* pResult)
{
NMLVDISPINFO* pDispInfo = (NMLVDISPINFO*)pNMHDR;
LVITEM* pItem = &(pDispInfo)->item;
if (pItem->mask & LVIF_TEXT)
{
// 行のインデックス(ゼロ開始)
int iItem = pDispInfo->item.iItem;
// カラムのインデックス(ゼロ開始)
int iSubItem = pDispInfo->item.iSubItem;
// インデックスに該当するデータを入れ物から取り出して、pItem->pszTextに
// コピーする。
//
//
}
*pResult = 0;
return;
}
(5)
CListCtrlクラスのメンバ関数であるInsertItem、SetItem、DeleteItem、DeleteAllItemsの呼び出しを削除する。
代わりに、それぞれを呼び出すタイミングで該当するデータを入れ物に入れる、または、入れ物から削除する。
(6)
CListCtrlクラスのメンバ関数であるSetItemCountExを用いて、入れ物に登録されているデータ行数をリストビューに設定する。
これにより、リストビューの高さ以上にデータを登録した場合に、スクロールバーの表示が可能となる。
(仮想リストビュー自体は、入れ物に登録されているデータ行数を知らないので)
リストビューの仮想リストビュー化は、ここまで。 以下に、各データをCString、各行をCStringArray、リストをCObArrayで持った場合の入れ物の例を示す。
(7)
クラスヘッダのprivateに、入れ物のメンバ変数定義を追加。
CObArray listData;
(8)
「【C++/MFC】リストビューの基本的な使い方」の「テキストの設定」の関数を、
同じインターフェースで中身を書き直した例。(ヘッダにメンバ関数定義を追加すること)
// iLine 行のインデックス(ゼロ開始)
// iCol 列のインデックス(ゼロ開始)
// lpszCsvData 表示する文字列
void CXxxxDlg::setItemListView(int iLine, int iCol, LPCTSTR lpszCsvData)
{
if (iCol == 0)
{
// 行の挿入と、最初の列にアイテム設定
CStringArray *lineData = new CStringArray();
lineData->Add(lpszCsvData);
listData.Add(lineData);
}
else
{
// 最初以外の列にアイテム設定
((CStringArray*)(listData.GetAt(iLine)))->SetAtGrow(iCol, lpszCsvData);
}
return;
}
(9)
(4)にて、入れ物から取り出して、pItem->pszTextにコピーする。
pItem->pszTextにコピー可能な文字列長は、NULL終端込みでpItem->cchTextMax(デフォルトは260)である。
// データを保持していない箇所を参照しないようにする
if (iSubItem < (((CStringArray*)(listData.GetAt(iItem)))->GetCount()))
{
wcsncpy_s(pItem->pszText, pItem->cchTextMax,
((CStringArray*)(listData.GetAt(iItem)))->GetAt(iSubItem), _TRUNCATE);
}
(10)
全行削除の場合、リストをRemoveAllで削除する前に、リストの各要素(各行)を必ずdeleteすること。
同様に、1行削除の場合でも、deleteしてからRemoveAtする。
((8)にて各要素をnewしているため、deleteしないとメモリリークする)
// 全行削除
for (int i = 0; i < listData.GetCount(); i++)
{
delete listData.GetAt(i);
}
listData.RemoveAll();
(7)~(10)のようなCStringArrayをCObArrayで持つ方法は、実際のデータサイズと比較して、実行時にかなりメモリを消費する。 行数、列数、各データ文字列長のいずれか(あるいは全部)の最大値が要件として決まっている場合、 必要なメモリを確保してからデータを格納するような、もっと効率のよい方法を検討すべきであろう。
(メモ)
- 約12万件のテストデータは、郵便局のウェブにて公開されている全国郵便番号を使用した
- newした後は必ずdelete、new[]した後は必ずdelete[]
- MSDNライブラリの「CListCtrl」「LVN_GETDISPINFO」「NMLVDISPINFO」「LVITEM」「CObArray」「CStringArray」「wcsncpy_s」を参照のこと