65.9K
CodeProject 正在变化。 阅读更多。
Home

使用虚拟列表

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.87/5 (64投票s)

2004年8月5日

Ms-RL

9分钟阅读

viewsIcon

489839

downloadIcon

8084

本文将解释如何使用虚拟列表,这是一种非常快速的列表,适用于显示大量项目。

Sample Image - virtuallist.gif

目录

简介

假设您的程序中有一个大型数据库,您想将其展示给用户。您可以使用带有几个列的 CListCtrl 并填充数千甚至数百万个元素。运行时,您会发现它有点(或非常)慢。如果我们不必添加所有元素到列表中,而是让列表显示它们,那该多好?这听起来是否愚蠢而荒谬?虚拟列表就是这样工作的。

虚拟列表

虚拟列表没有任何数据,它只知道它应该有多少个数据项。但它如何知道显示什么数据呢?秘诀在于列表会向父窗口请求它所需的信息。假设您有一个包含 100 个元素的列表,并且元素 10-20 是可见的。当列表重绘时,它首先询问父窗口关于元素 10 的信息。当父窗口响应后,列表重绘元素 10,然后继续下一个元素。

虚拟列表会向父窗口发送三种不同的消息。当列表需要信息时会发送 LVN_GETDISPINFO 消息。这是最重要的消息。当用户尝试通过在列表中键入来查找项目时,会发送 LVN_ODFINDITEM 消息。LVN_ODCACHEHINT 消息会在有机会缓存数据时发送。您可能根本不关心此消息。

好的,模糊的理论就到此为止,我们来看一些代码。

创建虚拟列表

创建虚拟列表并不比创建普通的 CListCtrl 难多少。像平常一样在资源编辑器中添加一个列表控件。然后勾选“Owner data”样式,然后为此控件添加一个 CListCtrl 变量。与普通 CListCtrl 唯一的区别是“Owner data”(LVS_OWNERDATA)样式。

在使用虚拟列表时,其使用方式与非虚拟列表基本相同。添加列、选择项目、添加图像列表等等,所有操作都完全一样。

向列表中添加项目

假设 m_list 是列表的控件变量。通常,您会像这样向列表中添加数据

m_list.InsertItem(0, _T("Hello world"));

但在虚拟列表中,这将不起作用。相反,您需要负责处理数据。与其添加,不如更改列表显示的元素数量

//"Add" 100 elements
m_list.SetItemCount(100);

将项目计数设置为 100 或 1,000,000 都没有关系,运行此命令所需的时间仍然几乎为零。在非虚拟列表中,添加一百万个元素可能需要数小时。

处理 LVN_GETDISPINFO 消息

正如我之前所说,列表在需要信息时会向父窗口请求。列表通过发送 LVN_GETDISPINFO 消息来执行此操作。这是处理虚拟列表时最重要的消息。典型的函数如下所示

void CVirtualListDlg::OnGetdispinfoList(NMHDR* pNMHDR, LRESULT* pResult) 
{
    LV_DISPINFO* pDispInfo = (LV_DISPINFO*)pNMHDR;

    //Create a pointer to the item
    LV_ITEM* pItem= &(pDispInfo)->item;

    //Which item number?
    int itemid = pItem->iItem;

    //Do the list need text information?
    if (pItem->mask & LVIF_TEXT)
    {
        CString text;

        //Which column?
        if(pItem->iSubItem == 0)
        {
            //Text is name
            text = m_database[itemid].m_name;
        }
        else if (pItem->iSubItem == 1)
        {
            //Text is slogan
            text = m_database[itemid].m_slogan;
        }

        //Copy the text to the LV_ITEM structure
        //Maximum number of characters is in pItem->cchTextMax
        lstrcpyn(pItem->pszText, text, pItem->cchTextMax);
    }

    //Do the list need image information?
    if( pItem->mask & LVIF_IMAGE) 
    {
        //Set which image to use
        pItem->iImage=m_database[itemid].m_image;
        
        //Show check box?
        if(IsCheckBoxesVisible())
        {
            //To enable check box, we have to enable state mask...
            pItem->mask |= LVIF_STATE;
            pItem->stateMask = LVIS_STATEIMAGEMASK;

            if(m_database[itemid].m_checked)
            {
                //Turn check box on
                pItem->state = INDEXTOSTATEIMAGEMASK(2);
            }
            else
            {
                //Turn check box off
                pItem->state = INDEXTOSTATEIMAGEMASK(1);
            }
        }
    }

    *pResult = 0;
}

首先,我们创建一个 LV_DISPINFO 指针,然后创建一个指向项目的指针。在 itemid 中,我们保存要处理的项目。然后我们检查 pItem 中的掩码。掩码告诉我们列表需要哪种信息。首先,我们检查是否需要文本信息。如果需要,我们首先确定文本属于哪个列。第一列用于名称,第二列用于标语(在报表视图中会显示多个列,在其他视图中只使用第一列)。

我们还检查是否需要图像信息。如果需要,我们将要使用的图像数量保存在 pItem->iImage 中(这是与列表关联的图像列表中的编号)。

如果复选框可见,当请求图像信息时,我们也应该发送关于它的信息。首先,我们更改 pItem->mask 以告知我们正在发送状态信息。我们还更改 pItem->stateMask 以告知我们正在发送何种信息。然后我们写入复选框是打开还是关闭。

掩码还可以包含标志 LVIF_INDENTLVIF_NORECOMPUTELVIF_PARAMLVIF_DI_SETITEM。但我从未用过任何一个,所以我想它们不重要 :-).

处理 LVN_GETDISPINFO 消息并不比这更难。复选框可能是最难的,但您可能永远不会使用比我在此示例中更复杂的代码。编写此函数后,您的列表将几乎像普通列表一样工作。但是,实现 LVN_ODFINDITEM 也是个好主意。

处理 LVN_ODFINDITEM 消息

首先,一些基础教育:启动 Explorer 并转到您有很多文件的文件夹。按 A。发生了什么?如果您有一个以“A”开头的文件或文件夹,那么该文件应该被选中。再次按 A。如果您有多个以“A”开头的文件,则应选择第二个文件。键入“AB”。如果存在任何以“AB”开头的文件,则已选中。这就是每个普通列表控件的行为方式。列表控件不是很酷吗? :-).

让我们看看列表控件通常如何搜索

名称
Anders
Anna
Annica
Bob
Emma
Emmanuel

Anna 被选中。当我们键入任何内容时,列表将向下搜索以找到最佳匹配。如果到达末尾,它将从顶部重新开始,搜索直到回到起始项目(Anna)。如果键入“A”,应选择 Annika。如果键入“AND”,应选择 Anders。如果键入“ANNK”,选择应保留在 Anna。如果键入“E”,应选择 Emma。

不幸的是,这在虚拟列表中不起作用。虚拟列表根本不会尝试查找任何项目,除非您处理 LVN_ODFINDITEM 消息。我通常会像这样实现此消息

void CVirtualListDlg::OnOdfinditemList(NMHDR* pNMHDR, LRESULT* pResult) 
{
    // pNMHDR has information about the item we should find
    // In pResult we should save which item that should be selected
    NMLVFINDITEM* pFindInfo = (NMLVFINDITEM*)pNMHDR;

    /* pFindInfo->iStart is from which item we should search.
       We search to bottom, and then restart at top and will stop
       at pFindInfo->iStart, unless we find an item that match
     */

    // Set the default return value to -1
    // That means we didn't find any match.
    *pResult = -1;

    //Is search NOT based on string?
    if( (pFindInfo->lvfi.flags & LVFI_STRING) == 0 )
    {
        //This will probably never happend...
        return;
    }

    //This is the string we search for
    CString searchstr = pFindInfo->lvfi.psz;

    int startPos = pFindInfo->iStart;
    //Is startPos outside the list (happens if last item is selected)
    if(startPos >= m_list.GetItemCount())
        startPos = 0;

    int currentPos=startPos;
    
    //Let's search...
    do
    {        
        //Do this word begins with all characters in searchstr?
        if( _tcsnicmp(m_database[currentPos].m_name, 
                 searchstr, searchstr.GetLength()) == 0)
        {
            //Select this item and stop search.
            *pResult = currentPos;
            break;
        }

        //Go to next item
        currentPos++;

        //Need to restart at top?
        if(currentPos >= m_list.GetItemCount())
            currentPos = 0;

    //Stop if back to start
    }while(currentPos != startPos);        
}

乍一看可能不清楚这是如何工作的,但仔细阅读后您就会明白。或者,您可以直接跳过它,复制代码并进行必要的更改——这取决于您 :-)。如果列表非常大,您可能需要制作一个更快的函数版本,或者根本不实现它。

pFindInfo->lvfi 包含有关如何搜索的信息(例如“到达底部时从顶部重新开始”或搜索方向)。我从不关心这些,如果您关心,应该查阅 MSDN 以获取更多信息。

处理 LVN_ODCACHEHINT 消息

发送 LVN_ODCACHEHINT 是为了给您一个缓存数据的机会。如果您正在处理位于网络中另一台计算机上的数据库,这可能很有用,但我还没有在我的任何程序中使用过此消息。处理此消息的函数可能看起来像这样

void CVirtualListDlg::OnOdcachehintList(NMHDR* pNMHDR, LRESULT* pResult) 
{
    NMLVCACHEHINT* pCacheHint = (NMLVCACHEHINT*)pNMHDR;

    // ... Cache the data pCacheHint->iFrom to pCacheHint->iTo ...

    *pResult = 0;
}

如您所见,这相当简单。但一如既往,简单的事情并不总是能奏效 :-)。根据 MSDN,您应该覆盖 OnChildNotify 并在该函数中添加处理程序。但我不会深入研究,如果您需要更多关于此的信息,请阅读 MSDN。

更改项目

您应该怎么做来更改数据?这真的很简单。您不更改列表中的数据,而是更改数据库中的数据。要重绘列表项,请调用 CListCtrl::RedrawItems

复选框

复选框很有用,但在使用虚拟列表时实现它们却相当棘手。在普通的非虚拟列表中,复选框在单击它们或按空格键时会切换。但在虚拟列表中,什么都不会发生。因此,您必须自己实现这些事件。我们从一个简单的切换复选框函数开始

void CVirtualListDlg::ToggleCheckBox(int item)
{
    //Change check box
    m_database[item].m_checked = !m_database[item].m_checked;

    //And redraw
    m_list.RedrawItems(item, item);
}

当我们要更改项目时,我们会调用此函数。该函数会切换复选框的值(在数据库中!)并强制列表重绘该项目。很简单。按空格键切换复选框也相当简单。为此消息添加一个 LVN_KEYDOWN 消息处理程序。函数应该如下所示

void CVirtualListDlg::OnKeydownList(NMHDR* pNMHDR, LRESULT* pResult) 
{
    LV_KEYDOWN* pLVKeyDown = (LV_KEYDOWN*)pNMHDR;

    //If user press space, toggle flag on selected item
    if( pLVKeyDown->wVKey == VK_SPACE )
    {
        //Toggle if some item is selected
        if(m_list.GetSelectionMark() != -1)
            ToggleCheckBox( m_list.GetSelectionMark() );
    }

    *pResult = 0;
}

我们检查是否按下了空格键以及是否有任何项目被选中,然后才切换复选框。要通过单击切换复选框,我们需要做一个更复杂的函数。为此消息添加一个 NM_CLICK 消息处理程序。我的函数看起来像这样

void CVirtualListDlg::OnClickList(NMHDR* pNMHDR, LRESULT* pResult) 
{
    NMLISTVIEW* pNMListView = (NM_LISTVIEW*)pNMHDR;

    LVHITTESTINFO hitinfo;
    //Copy click point
    hitinfo.pt = pNMListView->ptAction;

    //Make the hit test...
    int item = m_list.HitTest(&hitinfo); 

    if(item != -1)
    {
        //We hit one item... did we hit state image (check box)?
        //This test only works if we are in list or report mode.
        if( (hitinfo.flags & LVHT_ONITEMSTATEICON) != 0)
        {
            ToggleCheckBox(item);
        }
    }
    
    *pResult = 0;
}

我们使用 CListCtrl::HitTest 来查看我们是否单击了某个项目。如果单击了,函数会返回单击的项目。然后我们使用 hitinfo.flags 来查看我们单击的位置。如果 LVHT_ONITEMSTATEICON 标志为 ON,那么我们就知道用户单击了复选框(状态图像)。

不幸的是,如果列表视图处于“图标”或“小图标”模式,“CListCtrl::HitTest”不起作用。在这些视图中,hitinfo.flags & LVHT_ONITEMSTATEICON 始终为 0。我还没有找到解决方案,如果您找到了,请告诉我。但是,在这些模式下使用复选框可能相当不寻常,所以这并不是一个大问题。

注释

除非您在制作非常特殊的东西,否则您不需要比本文档更多的信息来使用虚拟列表。但是,虚拟列表和非虚拟列表之间存在一些小的兼容性问题。例如,虚拟列表无法对数据进行排序。但这相当明显,不是吗? :-)? 您可以在 MSDN 中找到有关这些问题的更多信息。

何时应该使用虚拟列表,何时不应该使用?对于大型列表,我个人更喜欢虚拟列表。但是非虚拟列表通常更容易编程(消息处理永远不是一件有趣的事),所以对于小型列表,我使用普通的列表。

虚拟列表的一个非常好的特点是它们很容易与数据库保持同步。您只需更改数据库并在必要时重绘列表。因此,如果您处理一个用户应该更改数据库中数据的列表,即使项目数量很少,虚拟列表也可能很有用。

另一个不错的优点是,有时您可以按需生成数据。如果您想在一列中显示行号,这非常容易做到,并且几乎不占用任何内存。在非虚拟列表中,您必须将此数据添加到所有项目中。

历史

  • 2004 年 8 月 5 日 - 初始版本。
© . All rights reserved.