使用虚拟列表
本文将解释如何使用虚拟列表,这是一种非常快速的列表,适用于显示大量项目。
目录
- 引言
- 虚拟列表
- 创建虚拟列表
- 向列表中添加项目
- 处理 LVN_GETDISPINFO 消息
- 处理 LVN_ODFINDITEM 消息
- 处理 LVN_ODCACHEHINT 消息
- 更改项目
- 复选框
- 注释
- 历史
简介
假设您的程序中有一个大型数据库,您想将其展示给用户。您可以使用带有几个列的 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_INDENT
、LVIF_NORECOMPUTE
、LVIF_PARAM
和 LVIF_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 日 - 初始版本。