VC 虚拟列表.docx
《VC 虚拟列表.docx》由会员分享,可在线阅读,更多相关《VC 虚拟列表.docx(27页珍藏版)》请在冰点文库上搜索。
VC虚拟列表
[VC/MFC]虚拟列表控件
2008-10-0719:
12一、什么是虚拟列表控件
虚拟列表控件是指带有LVS_OWNERDATA风格的列表控件。
。
二、为什么使用虚拟列表控件
我们知道,通常使用列表控件CListCtrl,需要调用InsertItem把要显示的数据插入列表中,之后我们就不必关心数据在哪里了,这是因为控件自己开辟了内存空间来保存这些数据。
现在假设我们要显示一个数据库,里面的信息量很大,有几十万条记录。
通常有两种方法解决这个问题:
1是仅仅在ListCtrl中插入少量的数据,比如100个,然后通过[上一页][下一页]两个按钮进行控制,某一时刻显示的只是从xxx到xxx+100之间的记录。
2是把所有数据全部插入到ListCtrl中,然后让用户通过滚动来查看数据。
无疑,很多用户喜欢采用第二种方式,特别是对于已经排序的数据,用户只需用键盘输入某行的开头字符,就可以快速定位到某一行。
但是,如果这样做,InsertItem插入数据的过程将是很漫长的,而且用户会看到ListCtrl刷新速度也很慢,而且所有数据都位于内存中消耗了大量的内存,当数据多达上万以后几乎是不能忍受的。
为此,mfc特别提供了虚拟列表的支持。
一个虚拟列表看起来和普通的ListCtrl一样,但是不用通过InsertItem来插入数据,它仅仅知道自己应该显示多少数据。
但是它如何知道要显示什么数据呢?
秘密就在于当列表控件需要显示某个数据的时候,它向父窗口要。
假设这个列表控件包含100个元素,第10到20个元素(行)是可见的。
当列表控件重画的时候,它首先请求父窗口给它第10个元素的数据,父窗口收到请求以后,把数据信息填充到列表提供的一个结构中,列表就可以用来显示了,显示第10个数据后,列表会继续请求下一个数据。
在虚拟的样式下,ListCtrl可以支持多达DWORD个数据项。
(缺省的listctrl控件最多支持int个数据项)。
但是,虚拟列表的最大优点不在于此,而是它仅仅需要在内存中保持极少量的数据,从而加快了显示的速度。
所以,在使用列表控件显示一个很大的数据库的情况下,采用虚拟列表最好不过了。
不仅CListCtrl提供虚拟列表的功能,MFC的CListView类也有同样的功能。
三、虚拟列表控件的消息
虚拟列表总共发送三个消息给父窗口:
当它需要数据的时候,它发送LVN_GETDISPINFO消息。
这是最重要的消息。
当用户试图查找某个元素的时候,它发送LVN_ODFINDITEM消息;还有一个消息是LVN_ODCACHEHINT,用来缓冲数据,基本上很少用到这个消息。
虚拟列表控件使用起来非常简单。
它总共只有三个相关的消息,如果你直接使用CListCtrl,应该在对话框中响应这三个消息。
如果你使用CListCtrl派生类,可以在派生类中响应这三个消息的反射消息。
这三个消息分别是:
(1)LVN_GETDISPINFO控件请求某个数据
(2)LVN_ODFINDITEM查找某个数据
(3)LVN_ODCACHEHINT缓冲某一部分数据
我们必须响应的消息是
(1),多数情况下要响应
(2),极少数的情况下需要响应(3)
四、如何使用虚拟列表控件
1、首先要创建控件,创建一个虚拟列表和创建一个正常的CListCtrl差不多。
先在资源编辑器里面添加一个listcontrol资源。
然后选中"Ownerdata"属性,然后给它捆绑一个CListCtrl变量。
添加列,添加imagelist等都和使用正常的listctrl一样。
2、给虚拟列表添加元素。
假设m_list是这个列表的控制变量。
通常的情况下这样添加数据:
m_list.InsertItem(0,_T("Helloworld"));
但是对于虚拟列表,不能这么做。
只需告诉列表有多少个数据:
//总共显示100行
m_list.SetItemCount(100);
3、处理它的通知消息。
五、如何响应虚拟列表的消息
1、处理LVN_GETDISPINFO通知消息
当虚拟列表控件需要某个数据的时候,它给父窗口发送一个LVN_GETDISPINFO通知消息,表示请求某个数据。
因此列表的所有者窗口(或者它自己)必须处理这个消息。
例如派生类的情况(CMyListCtrl是一个虚拟列表类对象):
//这里处理的是反射消息
BEGIN_MESSAGE_MAP(CMyListCtrl,CListCtrl)
//{{AFX_MSG_MAP(CMyListCtrl)
ON_NOTIFY_REFLECT(LVN_GETDISPINFO,OnGetdispinfo)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
在LVN_GETDISPINFO的处理函数中,必须首先检查列表请求的是什么数据,可能的值包括:
(1)LVIF_TEXT必须填充pszText
(2)LVIF_IMAGE必须填充iImage
(3)LVIF_INDENT必须填充iIndent
(4)LVIF_PARAM必须填充lParam
(5)LVIF_STATE必须填充state
根据它的请求,填充所需的数据即可。
//=================例子代码=====================================
下面的给出一个例子,填充的是列表所需的某个数据项的文字以及图像信息:
LV_DISPINFO*pDispInfo=(LV_DISPINFO*)pNMHDR;
LV_ITEM*pItem=&(pDispInfo)->item;
intiItemIndx=pItem->iItem;
if(pItem->mask&LVIF_TEXT)//字符串缓冲区有效
{
switch(pItem->iSubItem){
case0:
//填充数据项的名字
lstrcpy(pItem->pszText,m_Items[iItemIndx].m_strItemText);
break;
case1:
//填充子项1
lstrcpy(pItem->pszText,m_Items[iItemIndx].m_strSubItem1Text);
break;
case2:
//填充子项2
lstrcpy(pItem->pszText,m_Items[iItemIndx].m_strSubItem2Text);
break;
}
}
/*注意,多数情况下要使用lstrcpyn,因为最多复制字符的个数由pItem->cchTextMax给出:
lstrcpyn(pItem->pszText,text,pItem->cchTextMax);
*/
if(pItem->mask&LVIF_IMAGE)//是否请求图像
pItem->iImage=m_Items[iItemIndx].m_iImageIndex;
甚至连某行数据是否选中(当有checkbox的情况下)的信息也需要由用户自己来维护,例如:
//是否显示该行的选择信息?
if(IsCheckBoxesVisible())//自定义函数
{
pItem->mask|=LVIF_STATE;
pItem->stateMask=LVIS_STATEIMAGEMASK;
if(m_database[itemid].m_checked)
{
pItem->state=INDEXTOSTATEIMAGEMASK
(2);
}
else
{
pItem->state=INDEXTOSTATEIMAGEMASK
(1);
}
}
2、处理LVN_ODFINDITEM消息
在资源管理器里面,定位到某个文件夹,会显示很多文件,如果按下键盘的‘A’,则资源管理器会自动找到名字以'A'打头的文件夹或者文件,并选择该文件。
继续按A,如果还有其它名字以'A'打头的文件,则下一个文件被选中。
如果输入"AB",则'AB'打头的文件被选中。
这就是列表控件的自动查找功能。
当虚拟列表收到一个LVM_FINDITEM消息,它也会发送这个消息通知父窗口查找目标元素。
要搜索的信息通过LVFINDINFO结构给出。
它是NMLVFINDITEM结构的一个成员。
当找到要搜索的数据后,应该把该数据的索引(行号)返回,如果没有找到,则返回-1。
以对话框为例,响应函数大致如下:
//=================例子代码=====================================
voidCVirtualListDlg:
:
OnOdfinditemList(NMHDR*pNMHDR,LRESULT*pResult)
{
//pNMHDR里面是要查找的元素的信息
//要选中的目标元素的行号最后要保存在pResult中,这是关键!
NMLVFINDITEM*pFindInfo=(NMLVFINDITEM*)pNMHDR;
/*pFindInfo->iStart是查找的起始位置,一直到最后,然后从头开始,如果没有找到合适的,最终停留在iStart*/
*pResult=-1;
//是否按照文字查找?
if((pFindInfo->lvfi.flags&LVFI_STRING)==0)
{
return;
}
//这是我们要找的字符串
CStringsearchstr=pFindInfo->lvfi.psz;
intstartPos=pFindInfo->iStart;//保存起始位置
//判断是否最后一行
if(startPos>=m_list.GetItemCount())
startPos=0;
intcurrentPos=startPos;
//开始查找
do
{
if(_tcsnicmp(m_database[currentPos].m_name,
searchstr,searchstr.GetLength())==0)
{
//选中这个元素,停止查找
*pResult=currentPos;
break;
}
currentPos++;
//从头开始
if(currentPos>=m_list.GetItemCount())
currentPos=0;
}while(currentPos!
=startPos);
}
显然,如果数据很多,必须实现一个快速查找的方法。
关于pFindInfo->lvfi里面的信息的详细说明可以参考MSDN。
3、处理LVN_ODCACHEHINT消息。
假如我们从数据库或者其它地方读取数据的速度比较慢,则可以利用这个消息,批量读取一些数据,然后根据请求,逐个提供给虚拟列表。
LVN_ODCACHEHINT消息的用途就是给程序一个缓冲数据的机会。
以提高程序的性能。
//=================例子代码=====================================
使用ClassWizard重载OnChildNotify函数,检查是否LVN_ODCACHEHINT消息,然后准备缓冲数据:
NMLVCACHEHINT*pcachehint=NULL;
NMHDR*phdr=(NMHDR*)lParam;
if(phdr->code==LVN_ODCACHEHINT)
{
pcachehint=(NMLVCACHEHINT*)phdr;
//自定义函数,准备指定范围的数据到缓冲区
PrepCache(pcachehint->iFrom,pcachehint->iTo);
}
else...
注意,如果消息不是LVN_ODCACHEHINT,则要传递给基类进行处理。
五、如何修改ListCtrl显示的数据。
由于是程序自己维护数据,所以只需修改数据库中的数据,然后调用CListCtrl:
:
RedrawItems函数进行重画即可。
六、数据的选择状态和选择框
CListCtrl可以显示checkbox选择框。
有些情况下是很有用的。
对于正常的listctrl,用户可以用鼠标来修改某个元素的选择状态,但是对于虚拟列表就不行了。
必须自己处理一些消息,然后自己保存数据的选中状态:
voidCVirtualListDlg:
:
ToggleCheckBox(intitem)
{
m_database[item].m_checked=!
m_database[item].m_checked;
m_list.RedrawItems(item,item);
}
处理LVN_KEYDOWN消息,添加对空格键的响应,用于切换选择状态:
voidCVirtualListDlg:
:
OnKeydownList(NMHDR*pNMHDR,LRESULT*pResult)
{
LV_KEYDOWN*pLVKeyDown=(LV_KEYDOWN*)pNMHDR;
if(pLVKeyDown->wVKey==VK_SPACE)
{
intitem=m_list.GetSelectionMark();
if(item!
=-1)
ToggleCheckBox(item);
}
*pResult=0;
}
然后处理NM_CLICK消息:
voidCVirtualListDlg:
:
OnClickList(NMHDR*pNMHDR,LRESULT*pResult)
{
NMLISTVIEW*pNMListView=(NM_LISTVIEW*)pNMHDR;
LVHITTESTINFOhitinfo;
hitinfo.pt=pNMListView->ptAction;
intitem=m_list.HitTest(&hitinfo);
if(item!
=-1)
{
//看看鼠标是否单击在checkbox上面了?
if((hitinfo.flags&LVHT_ONITEMSTATEICON)!
=0)
{
ToggleCheckBox(item);
}
}
*pResult=0;
}
七、备注:
1、虚拟列表无法进行排序。
2、虚表的一个优点是容易保持和数据库的同步。
修改数据库中的数据,然后重画list十分容易而且高效。
3、虚表的另一个优点是可以根据需要产生数据。
比如在某一列加上行号。
CListCtrl大数据显示
分类:
MFC
2009-03-1212:
211004人阅读评论(0)收藏举报
CListCtrl是个很方便的东西,但是当数据大到一个程度(比如说10万条数据),显示速度就会非常的慢。
解决办法就是用虚拟列表。
CListCtrl显示数据的原理是将需显示的所有数据拷贝在它内部的一块空间里,然后显示出来。
一但数据量过大,拷贝的时间就会延长,显示速度当然也就非常慢了。
而虚拟列表则不需要将显示数据拷贝到内部空间,它的做法是当需要显示某个数据时,才将数据拷入内部空间。
看上去好像和普通CListCtrl的做法相同,实则大不一样。
因为列表一屏的数据,最多也就几百行。
也就是说虚拟列表每次最多也就只需要拷贝几百行的数据,对现在计算机的速度而言,速度的延迟是完全可以忽略的。
好了,原理说到这里。
接下来该说做怎么做了。
[cpp:
showcolumns]viewplain
copy
·········10········20········30········40········50········60········70········80········90········100·······110·······120·······130·······140·······150
classCVirtaulListCtrl:
publicCListCtrl
{
DECLARE_DYNAMIC(CVirtaulListCtrl)
public:
CVirtaulListCtrl();
virtual~CVirtaulListCtrl();
public:
//导入数据
voidImportData(CStringArray*pDataArray)
{
m_pDataArray=pDataArray;
SetItemCount(m_pDataArray->GetSize());
}
protected:
CStringArray*m_pDataArray;
protected:
DECLARE_MESSAGE_MAP()
public:
afx_msgvoidOnLvnGetdispinfo(NMHDR*pNMHDR,LRESULT*pResult)
{
LV_DISPINFO*pDispInfo=(LV_DISPINFO*)pNMHDR;
LV_ITEM*pItem=&(pDispInfo)->item;
intiItemIndex=pItem->iItem;//行号
intcol=pItem->iSubItem;//列号
if(pItem->mask&LVIF_TEXT)
{
if(col==0)
{
//序号
charnumberStr[10];
itoa(iItemIndex+1,numberStr,10);
lstrcpyn(pItem->pszText,numberStr,pItem->cchTextMax);
}
elseif(col==1)
{
lstrcpyn(pItem->pszText,m_pDataArray->GetAt(iItemIndex),pItem->cchTextMax);
}
}
}
};
上面一共只有两个函数。
一个是ImportData,作用是导入需要显示的数据。
里面只有两条语句,第一条为获取数据指针,第二条为设置列表长度(一共有多少条数据)。
这个是自定义函数,可以自定义函数名和导入的数据结构。
第二个函数是OnLvnGetdispinfo。
这个函数是事件函数,当列表中的一个单元格需显示的时候调用。
使用时,还需要将列表控件的“所有者数据”属性设为“true”。
MFC总结之CListCtrl用法及技巧
(一)
本文根据本人在项目中的应用,来谈谈CListCtrl的部分用法及技巧。
当初学习时,查了很多资料,零零碎碎的作了些记录,现在主要是来做个总结,方便以后查阅。
主要包括以下十三点内容:
基本操作、获取选中行的行号、复选框操作、动态设置选中行的字体颜色、设置选中行的背景颜色、禁止拖动表头、让第一列居中显示、设置行高与字体、虚拟列表技术、点击表头时进行归类、向上与向下移动、动态调整大小问题、避免闪烁问题。
分为两篇来进行总结。
本篇重点总结:
基本操作、获取选中行的行号、复选框操作、动态设置选中行的字体颜色、设置选中行的背景颜色
1、基本操作
分别从下面四点来介绍CListCtrl的基本操作:
①设置列表视图显示方式
Ⅰ.CListCtrl有四种样式:
LVS_ICON、LVS_SMALLICON、LVS_LIST、LSV_REPORT,可通过控件属性来设置。
本文所述均为LSV_REPORT属性。
Ⅱ.扩展样式:
常用的扩展样式有三种:
LVS_EX_FULLROWSELECT、LVS_EX_GRIDLINES、LVS_EX_CHECKBOXES,分别对应作用选中某行时使正行高亮、设置网格线、item前生成Ckeckbox控件。
使用SetExtendedStyle(style)函数设置扩展样式,使用GetExtendedStyle()函数获取样式,如:
m_listInfo.SetExtendedStyle(LVS_EX_FULLROWSELECT|LVS_EX_GRIDLINES);
Ⅲ.使用CListView时,需要在PreCreateWindow()函数中添加cs.style|=LVS_REPORT;
来将其设置为LVS_REPORT风格,否则插入无效。
还用另一种方法来设置风格,即在OnInitialUpate()中获取CListCtrl控制权,然后修改风格,如下所示:
CListCtrl&theCtrl=GetListCtrl();
theCtrl.ModifyStyle(0,LVS_REPORT);
②插入操作
先插入列:
intInsertColumn(intnCol,LPCTSTRlpszColumnHeading,intnFormat,intnWidth,intnSubItem)
插入列时,可指明列号、列名称、列名称显示样式,列宽等信息。
对于列号为0的那一列,始终是靠左显示,后面会有修改使其剧中显示的方法,其他列通过设置nFormat属性可以居中显示。
插入行:
intInsertItem(intnItem,LPCTSTRlpszItem)
直接插入一行,nItem指明行号,lpszItem指明该行第0列的信息。
设置信息:
BOOLSetItemText(intnItem,intnSubItem,LPCTSTRlpszText)
设置第nItem行nSubItem列的信息(nItem:
0,1,2,3……;nSubItem:
1,2,3……)
③删除操作
有三个操作函数:
BOOLDeleteAllItems()-------删除所有的行
BOOLDeleteItem(nItem)--------删除某一行
BOOLDeleteColumn(nCol)-----删除某一列
④获取/设置属性函数
有很多函数了,就不一一介绍了。
常用的有
intGetItemCount()--------获取已插入信息的行数
BOOLSetItemState(intiLink,UINTstate,UINTstateMask)---------设置行状态,如高亮显示等
等等
2、获取选中行的行号
获取选中行的行号,然后对该行进行相关处理,这点在编程中用的非常多。
当鼠标单击item时,控件向父窗口发送NM_CLICK消息,其响应函数为OnNMClickXXXX(NMHDR*pNMHDR,LRESULT*pResult),在该函数下来编写代码获取鼠标点击的行号。
有两种方法来获取行号:
第一种是使用GetFirstSelectedItemPosition和GetNextSelectedItem配合来获取;第二种是先获取鼠标位置信息,然后调用HitTest函数来找出行号。
示例分别如下:
第一种方法,该示例截自MSD