ListView控件在添加大量数据时速度很慢而且会频繁刷新以至于窗口不断闪烁,总之是没法用啦,这种情况把控件改成虚拟列表的形式就能解决这个问题。

虚拟列表不在控件中存储数据,而是向外部“索要”数据,即数据是由我们自己维护的,它唯一保存的数据是控件中项目的数量。虚拟列表通过 LVN_GETDISPINFO、LVN_ODFINDITEM和LVN_ODCACHEHINT这三个消息与外界交互。

创建虚拟列表控件

使用CreateWindows(Ex)创建控件时加上 LVS_OWNERDATA 样式,这样创建的就是一个虚拟列表了。注意,虚拟列表样式只能在创建时指定而无法在创建后添加或移除。

hList = CreateWindowEx(0, WC_LISTVIEW, NULL,
    LVS_REPORT | WS_CHILD | WS_VISIBLE | WS_BORDER | LVS_ALIGNTOP | LVS_OWNERDATA,
    0, 0, CW_USEDEFAULT, CW_USEDEFAULT,
    hDlg, (HMENU)ID_LISTVIEW, (HINSTANCE)GetWindowLong(hDlg, GWL_HINSTANCE), NULL);

向虚拟列表添加数据

使用的时候先准备好表中的数据,同时设置好表项的数量( 使用ListView_SetItemCountEx或直接发LVM_SETITEMCOUNT消息给控件),这样就完成数据的添加了。

处理控件的通知消息

·LVN_GETDISPINFO

ListView控件显示信息时向父窗口发送LVN_GETDISPINFO消息来获取数据。因此父窗口要处理 WM_NOTIFY 消息。而大多数情况下我们只要处理了这个消息就可以享受到虚拟列表的高速显示了,剩下两个不用管(‘∀`)
LVN_GETDISPINFO消息的处理差不多是这个样子:

case WM_NOTIFY:
    if (((NMHDR*)lParam)->idFrom == ID_LISTVIEW)
    {
        switch (((NMHDR*)lParam)->code)
        {
            case LVN_GETDISPINFO:
            {
                NMLVDISPINFO *pdi = reinterpret_cast<NMLVDISPINFO*>(lParam);
                LVITEMW *pItem = &pdi->item;
                
                // 要获取第几行的数据
                int itemid = pItem->iItem;
                
                // 从外部传递数据
                Information *pi = g_Data[itemid];
                if (pItem->mask & LVIF_TEXT)
                {
                    switch (pItem->iSubItem)
                    {
                    case 0: // 获取第一列的数据
                        wcscpy_s(pItem->pszText,
                            pItem->cchTextMax,
                            pi->firstcolumn
                            );
                        break;

                    case 1:
                        wcscpy_s(pItem->pszText,
                            pItem->cchTextMax,
                            pi->secondcolumn
                            );
                        break;
                    // ...
                    }
                }
                break;
            }
            // ...
        }
    }

·LVN_ODFINDITEM

LVN_ODFINDITEM消息用于处理查找特定的项,就像我们在资源管理器中输入exp来搜索exp开头的文件(夹)一样,这个消息就是用于支持这项功能的。当控件接受到键盘按键消息或LVM_FINDITEM消息时会向父控件发送此消息。此时的 lParam 指向一个NMLVFINDITEM结构。NMLVFINDITEM包含一个LVFINDINFO结构的成员变量。
NMLVFINDITEM中的iStart指示搜索的开始位置(从0开始),LVFINDINFO中的psz则储存了要搜索的字符串(同时flags成员中应包含LVFI_STRING),有了这些消息就可以自己搜索匹配的项了,然后找到了则返回项的偏移,找不到返回-1。

·LVN_ODCACHEHINT

最后一个处理缓存的消息。将经常会被显示的项缓存起来以提高显示效率。此时lParam指向NMLVCACHEHINT结构。这个结构中的iFrom和iTo指示了显示范围。
当ListView的显示改变(比如滚动鼠标滚轮)时将会发送此消息,此时应用程序便可以决定是否缓存目标范围内的数据,然后等待LVN_GETDISPINFO消息的到来。
比如把大量数据存在硬盘,然后缓存一部分到内存里,收到消息时便可异步读取数据到内存,然后在LVN_GETDISPINFO中提交给控件显示(‘∀`)。
这个消息必须返回0。

复选框(checkbox)的处理

Checkbox还是个挺有用的东西。

首先创建控件时设置扩展风格LVS_EX_CHECKBOXES,这个不管是不是虚拟列表都要处理的。

ListView_SetExtendedListViewStyleEx(
    hList, 0, LVS_EX_GRIDLINES | LVS_EX_CHECKBOXES | LVS_EX_FULLROWSELECT);

非虚拟列表的话就不用处理其他的直接能看到效果了。因为虚拟列表不存储条目数量以外的数据,所以我们还得继续做点处理。一个是要维护一个列表中项目的选中状态,然后就是当有选中状态变换时及时刷新到界面上。比如使用空格键切换checkbox的选中状态:

case WM_NOTIFY:
    if (((NMHDR*)lParam)->idFrom == ID_LISTVIEW)
    {
        switch (((NMHDR*)lParam)->code)
        {
        case VK_SPACE:
            iClickItem = ListView_GetSelectionMark(hList);
            g_Data[iClickItem].checkstate ^= 0x3;
            SendMessage(hList, LVM_REDRAWITEMS, iClickItem, iClickItem);
            break;
        }
    }

其中 checkstate==1 时代表未选中,2代表选中。更新了选中状态后发送LVM_REDRAWITEMS消息刷新显示。因为是虚拟列表,重刷当然还是在LVN_GETDISPINFO里处理。
因此处理checkbox时还要在LVN_GETDISPINFO中设置以下字段的值:

case LVN_GETDISPINFO:
{
    NMLVDISPINFO *pdi = reinterpret_cast<NMLVDISPINFO*>(lParam);
    LVITEMW *pItem = &pdi->item;                
    int itemid = pItem->iItem;
    pdi->item.state = INDEXTOSTATEIMAGEMASK(g_Data[itemid].checkstate);
    pdi->item.stateMask = LVIS_STATEIMAGEMASK;

    // 处理item内容
}

state字段的12~15位表示item的状态图片,即checkbox的图片,0代表不显示。可用宏INDEXTOSTATEIMAGEMASK(sta)设置状态,这个宏就是将sta左移12位
同时还要设置stateMask,使state中的设置生效,状态图片对应的是LVIS_STATEIMAGEMASK。这样就能将checkbox的状态刷新上去了。其他的属性参考LVITEM

更多内容请参考Virtual List-View Style