虚拟表格显示控件
虚拟表格显示控件
摘要:一个虚拟的大表格数据显示控件(MFC)
关键字:虚拟控件 表格显示
前一段时间写了一个表格显示控件(说是控件,其实是个视图,这里就不加区分了),可以显示巨型表格数据。为了能够处理巨量的数据,采用了"虚拟"的技术。
如果你用过虚拟列表控件(Virtual List Controls),就知道所谓的"虚拟"是什么意思。让我解释一下,毕竟这是这篇文章的关键点。一般情况下,控件在内部保存它管理的所有数据,比如编辑框(edit box)就是这样,它自身"拥有"全部的数据。任何时候,要查询或改变控件数据,控件都通过操作自己内部的数据来进行。但有时候,如果控件要显示巨量的数据,这样做就不合适了。首先,分配这么大的内存可能不现实。其次,即使有这么多内存,将数据读出并拷贝到控件自身的内存中去,可能会是一个漫长的过程。为了解决这个问题,就提出了"虚拟"控件的概念。虚拟的意思是,控件看起来好像有全部的数据,但实际上没有,虚拟的而已。
在这种情况下,控件只保存很少的一部分数据,例如说一个屏幕能显示的数据量。每次它要显示或操作数据的时候,如果对应的数据项不在它保存的数据子集里面,它就向程序发出请求,说需要某个数据项。这通常是向控件的父窗口发生一个消息,程序在响应消息的时候将数据传递给控件。有人可能会问,既然这样,控件干脆一个数据也不保存不是更好,每个数据都向程序要好了。嗯,这是一个平衡的问题。如果根本不保存任何数据,可以减少控件的内存消耗,但会大大降低程序的运行效率。比如,控件上方弹出一个对话框,或者任何其它窗口需要重绘的时候,控件都必须去请求数据。如果读数据是个费时的操作比如读数据库,那么程序基本上是玩不下去了,太慢了。所以适当的平衡内存和速度是有必要的。这里的平衡点就是,只要显示的数据不发生改变,就不应该请求数据。一般情况下,控件应该保存界面上一次最多能显示的数据量。
好了,原理就是这样。具体实现如下:
A.计算数据的时机
对一个滚动视图来说,可能改变数据的情况有两种:改变窗口大小,滚动窗口。考虑到滚动可能是滚动条和鼠标滚轮引发的,在OnScrollBy函数中进行数据更新是最合适不过了。至于窗口大小变化,当然是响应WM_SIZE消息了。
B.计算需要哪些新数据
在更新数据的时候,比较需要的数据和已经有的数据之间的差别,看看缺少哪些项,然后只请求缺少的数据。具体的算法很简单,想象一个很大的矩形区域(代表了整个表格),上面有一个小矩形A(代表了当前的窗口显示区域),现在经过某些操作后,矩形A可能缩放了,平移了,变成了矩形B(代表了新的显示区域)。那么在矩形B中且不在矩形A中的数据就是缺少的数据项。
很明显,给定每行的高度,每列的宽度,滚动位置,窗口大小,我们可以计算出窗口应该显示表格的那些行列(用矩形A表示)。窗口变化后(指缩放和滚动),又可以计算出新的窗口应该显示表格的哪些行列(用矩形B表示)。A,B的交集就是仍然有效的数据,B中其它的元素就必须请求数据。
下面是更新数据函数的主要代码:(m_rcRowCol是矩形A,rcNew是矩形B,其单位都是表格的行列)
void CDataView::UpdataActiveData()
{
RECT rcNew;
RegionToRowCol(m_rcRegion,rcNew);//计算要显示的数据行列.
if(::EqualRect(&m_rcRowCol,&rcNew))
{
return;
}
//原来有 m_rcRowCol 的数据,现在需要 rcNew 的数据.
RECT rcInersect;
::IntersectRect(&rcInersect,&m_rcRowCol,&rcNew);
if(::IsRectEmpty(&rcInersect))//没有交集,数据全部重新取得.
{
//释放原来所有数据.
...
//取得新数据.
...
m_rcRowCol = rcNew;
return ;
}
...
for(i=y1; i<=y2; ++i)
for(j=x1; j<=x2; ++j) //把原来数据中仍然要显示的复制到temp中.
{
temp[i-y1][j-x1] = m_active[i][j];
}
for(i=0;i<50;++i)
for(j=0;j<50;++j)//释放原来数据中不再需要的部分.
{
if( (j<x1 || j>x2 || i<y1 || i>y2) //在相交矩形之外的数据.
&& m_active[i][j])
{
m_malloc.free(m_active[i][j]);
}
m_active[i][j] = 0;
}
...
for(i=0; i<=h; ++i)
for(j=0; j<=w; ++j)
{
if(j>=x1&&j<=x2&&i>=y1&&i<=y2)//已经有该数据.
{
m_active[i][j] = temp[i-y1][j-x1];
}
else//取得新数据.
{
m_active[i][j] = GetData(i+y0,j+x0);
}
}
m_rcRowCol = rcNew;
}
请求数据是调用了一个函数char *CDataView::GetData(int row,int col),而不是象虚拟列表控件控件那样采用消息的方法。该函数不是虚函数,它简单的调用一个用户指定的回调函数来取得表格数据项.这里表格数据项是一个字符串指针。
这个控件的主要工作就是这样,不过还有一些其它的问题需要考虑。
首先,你如果仔细分析代码,会发现内存的分配和释放相当频繁。这是没有办法的,在控件的用户程序给出表格数据之前,是无法预料到数据的长度,只能动态分配。不过我们仍然有机会做一些优化。比如,如果你的表格数据是整数,可能绝大多数情况下表格数据不超过4个字节。不管表格数据是什么样子,其数据长度都很可能集中在很小的范围内。这种情况下,我们预先分配一定数量的固定大小的块,比如分配1000个4字节的块。以后需要分配表格项数据内存的时候,如果其长度不大于4字节,则从缓冲区取一个块,否则正常分配内存。如果你的表格项数据大部分都不超过4字节,则这种策略可以大大减少内存分配的次数。这个缓冲分配的功能已经在CMalloc类中实现了,该类使用了std::set来跟踪每个块的使用情况。
其次,大表格很可能涉及到滚动坐标超过16位的情况。因此,必须改写CScrollView与滚动坐标相关的函数。具体说就是重载CScrollView::OnScroll,在其中检测到THUMB pos相关值时,使用GetScrollInfo取得滚动位置值,而不使用滚动消息参数中的滚动位置值。
好了,文章到这里就结束了。但是容我再说几句多余的话,以免本文对新人造成误导。是的,我知道系统的list控件实现了同样的功能,我也知道C运行库的内存分配函数本身是带缓冲的,那么写这个控件还有什么意义吗?有的。首先我的实现和他们的不同。其次,对一个喜欢编程的人来说,自己实现些东西总是有意义的(当然在做项目的时候还是要使用标准的东西)。
最后祝编程愉快!
(附件中有完整的VC6工程代码,在XP下编译通过)