リストビューを仮想リストビューに変更する

Visual C++プログラミングのメモ

【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」を参照のこと