关于ListCtrol自绘的技巧

时间:2023-03-08 17:26:02

一、给控件添加排序功能
report风格的list控件很多情况下都需要支持排序功能,而且最好支持按不同列进行排序。CListCtrl的类方法SortItems支持排序功能,但是在排序过程中,两个数据真正的比较过程是通过SortItems的第一个参数指向的回调比较函数来完成的。这个函数通过比较SetItemData函数设置的每个Item对应数值,返回一个代表比较结果的值,SortItems便可以依据这个返回值进行排序实现。可以看出,这种方法虽然简单,
却有一个比较严重的缺陷,即不能支持按照不同列数据进行排序。
对于自绘控件来说,要想支持按不同列排序功能,需要完成以下几件事:
1、list控件响应LNV_COLUMNCLICK消息,得到需要排序的列号,并通知HeaderCtrl在列头上绘制正序或倒序三角标识;
2、写一个比较函数,功能类似上面提到的回调函数,只不过这个函数将由我们自己的排序函数调用。为了支持多种类型的列数值排序,这个函数应该能对不同类型的数值进行比较;
3、写一个list控件行交换函数,排序函数在调用比较函数后,根据返回值会调用此交换函数完成排序效果。实现此函数既可以通过调用DeleteItem和InsertItem两个类功能函数实现,也可以通过GetItem和SetItem两个函数交换两行的数据信息实现;
4、写一个排序函数,这个函数类似上面SortItems函数完成的功能。通过指定列号、排序方式(正序或倒序)以及排序开始和结束的行号,按照某种排序策略(插入排序、快速排序等),通过调用比较函数和行交换函数完成排序效果。如果list中的数据项不是很多的话,用递归调用实现快速排序,效果还是十分令人满意的。

二、添加进度条
给list控件添加进度条也是常见的需求之一,可惜CListCtrl好像并没有提供直接的方法可以解决这个问题。思路有两个:一是创建进度条控件,将其嵌入到list控件相应的位置上,并通过响应消息来处理进度条显示;而对于自绘控件来说,直接绘制自己的进度条可能是一个更简单有效的方法,而且这种思路出来的效果可能给人更多惊喜。
这里介绍一种利用贴图自绘进度条的方法,效果非常不错。首先,需要定义两个CBitmap类型的变量加载进度条背景和前景的位图资源;然后计算进度条所在subitem的区域大小、进度条背景区域大小和前景区域大小;最后,在控件自绘函数DrawItem中需要绘制进度条的位置,调用DC的StretchBlt函数以拉伸方式显示进度条背景和前景位图即可。

三、支持多行文本
如果某个subitem的显示文本太长的话,多行文本便显得很重要。其实多行文本显示很大程度上体现了自绘控件的思路纲领:在需要绘制的时刻,自己计算绘制区域的大小,自己计算显示内容所需区域大小,自己制定合理的策略以正确合理的方式进行显示。
支持多行文本显示,首先计算显示区域的大小;然后通过GetLogFont函数查询当前设置的字体属性,得到字体高度和宽度;之后便可以计算出显示文本内容需要显示几行,每一行显示多少个字。当然实际情况中由于有中英文的区别,不能简单的按照字体宽度计算;可以用DC的GetTextExtent函数得到文本实际需要区域大小,然后进行相应调整。另外,当显示区域填满仍然不能显示所有文本时,可以用“...”表示剩余字符。

四、关于滚动条
list控件的滚动条非常有用,在此我不想多谈滚动条的自绘等内容,那是一个比较复杂的话题。其实MFC的CWnd类是可以设置WS_VSCROLL和WS_HSCROLL风格的,分别代表支持竖直滚动条和水平滚动条,CListCtrl是从CWnd继承而来,自然也不例外。更让我们欣喜的是,CListCtrl基类已经实现了滚动条的
功能和控制,不过这里的滚动条并不是一个ScrollBar控件,而是CListCtrl自己绘制的。
虽然不用我们自己实现滚动条功能,但是关于CListCtrl中滚动条一些属性和特点还是要有概念,因为有的时候就要用到。比如,我们利用DrawItem函数绘制每个Item的显示;那么就要在背景刷新函数OnEraseBkgnd中刷新剩余区域,这时就要根据item的个数和行高计算剩余区域的位置,这时我们就要考虑
滚动条的位置。通过GetScrollInfo函数可以得到滚动条信息,其中SCROLLINFO类型的信息结构体需要说明一下。定义如下:
typedef struct tagSCROLLINFO
{
UINT cbSize;
UINT fMask;
int nMin;
int nMax;
UINT nPage;
int nPos;
int nTrackPos;
} SCROLLINFO, FAR *LPSCROLLINFO;
typedef SCROLLINFO CONST FAR *LPCSCROLLINFO; 
其中nMin,nMax分别表示目前滚动条设置的滚动范围,而nPos则表示当前滚动块在滚动条中的位置。这个位置正是相对于nMin和nMax而言的位置,比如设置的范围为(10,20),如果滚动块位于滚动条的中间,则其值为15。nPage表示滚动条每页的滚动位置数,通俗一点讲,其实就是点击滚动条中滚动块之外的位置时,滚动条就会向上或向下翻页,翻页时滚动的位置数即为nPage的值。由上可知,滚动一个位置对应的像素数,是可以根据滚动窗口大小和设置的滚动范围计算出来的。那么CListCtrl中,根据实际调试可知,竖直滚动条滚动一个位置的像素数其实就是list控件的行高,也就是说,info中nMin为0,nMax为隐藏区域高度可以包含的行数。

五、防止刷新时的闪烁
这个问题几乎是所有控件和视图刷新都要面对的问题,解决的手段也几乎被全部归算到“双缓冲”技术上。其实有的时候双缓冲不一定能解决问题,有的时候不一定像双缓冲那么复杂。最重要的一点,就是要搞清楚为什么会有闪烁现象,除去显卡性能的因素,闪烁的罪魁祸首就是刷新前后的画面差别太大。知道了这一点,很多现象都可以清楚地分析其缘由了。比如,你明明在刷新函数中完成了双缓冲,但是却遗憾的发现闪烁依然存在。我想很可能是你没有制止MFC自己默认对窗口背景的刷新动作,你需要做的就是覆盖掉基类的OnEraseBkgnd函数,自己绘制背景,并返回TRUE,来告诉Windows不用帮你绘制背景了。
所以我们防止闪烁的手段其实有很多种,我们可以在OnEraseBkgnd中自己处理背景来消除背景差异过大引起的闪烁;我们可以在OnPaint或OnDraw函数中用双缓冲技术来减少由于绘制复杂画面导致频繁刷新引起的闪烁;我们可以在调用引起区域或窗口无效的函数时,设置参数防止背景重绘;我们可以在强制刷新时,尽量细化重绘区域,使重绘区域减到最小...

六、调整列宽引起的麻烦事
当你不经意调整HeaderCtrl的列宽时,对于自绘list控件,你可能突然发现不少问题:
1、你的第一列是一个缩略图,你根本不想这一列的列宽被调整;
2、某一列列宽被你调整到0,你竟然看不到它了,再把它弄出来似乎也很费劲;或者你将某列列宽减小到一定程度时,发现两个列的绘制竟然重合了;
3、你缩小了某一列列宽,发现最后一列向左移动了相应位置,但HeaderCtrl最右边却露出了不同背景颜色的区域。(这一点对于不同版本似乎情况不尽相同,Unicode有这个问题,MultiBytes版本没有,未搞清楚原因)
对于第一个问题,需要支持某列不支持调整列宽;第二个问题,需要设置一个最小列宽;第三个问题,一个有效的解决办法是动态调整最右边一列的宽度,使之总是符合list控件窗口宽度。
1、支持某列列宽固定。
很简单,只要在HeaderCtrl中重写虚函数OnChildNotify,当消息类型为HDN_BEGINTRACKW及HDN_BEGINTRACKA,且列号为需要固定的那一列时,直接给参数pLResult赋值为TRUE,并返回TRUE即可。
这样调整列之间的间隔条的消息就被屏蔽,因此list控件就不会收到对应消息。当然,如果想做的漂亮一点,可以把此时Cursor改变这个动作也屏蔽掉。
2、设置某列最小宽度
也很简单,重写list控件的虚函数OnNotify,当消息类型为HDN_ITEMCHANGINGW或HDN_ITEMCHANGINGA,列号为需要设置最小宽度的那一列,且此时列宽小于设置列宽时,直接给参数pResult赋值为TRUE,并返回TRUE即可。这里需要说明一个问题,当你的list控件设置了ImageList后,则所有的subitem最小宽度和高度为ImageList中Image的大小,因此当你在DrawItem函数中调用GetSubItemRect查询subitem大小时,返回的结果与你看到界面上的结果是不一样的,这一点一定要注意。这也是很引起上面提到的两列绘制内容重合问题的原因。
3、动态设置最右一列宽度为合适大小
在同上函数的位置,处理某列宽度被调整的消息时,调整最右一列的宽度。需要注意的是,由于调用SetColumnWidth函数又会触发这个消息,所以要判断当前调整列是否为最右一列,否则就会不断循环下去,使程序崩溃。
另外,调用SetColumnWidth函数时设置参数为LVSCW_AUTOSIZE_USEHEADER,并不会使宽度立即更新,而是需要设置具体的数值。猜想可能是LVSCW_AUTOSIZE_USEHEADER这个参数不会立即强迫list控件刷新,只有在list控件下次刷新时才起作用。

CListCtrl控件是MFC控件中功能最丰富的控件之一,能总结和学习的很多,其他可以研究和丰富的功能还有ToolTip、自绘滚动条、编辑subitem、拖拽、组功能、虚拟列表等

这两种方法应该是控件自绘中最常用到的普遍方法。(当然如果只是改变控件颜色只需要处理WM_CTLCOLOR消息就可以了。)但是对于这两者的区别,可能很多开发人员并不是很清楚。如果你做过控件自绘,可能对owner-draw已经很熟悉了。一般只要设置控件的自绘风格属性,并实现owner-draw的消息(WM_DRAWITEM)响应虚函数(DrawItem)就可以了。可以应用这种方法的控件包括拥有自绘风格的Button、ComboBox、ListCtrl、Menu、StatusBar、HeaderCtrl、TabCtrl等大部分控件,MFC在控件需要重绘的时候调用绘制函数,并传递DC及控件位置、大小等信息,我们需要做的就是利用这些信息来绘制自己需求的控件外观。但是这种方式不能用于EditCtrl,也不能用于非report风格的ListCtrl。
custom-draw方式是响应的NM_CUSTOMDRAW消息,与WM_DRAWITEM消息不同,它是被包含在WM_NOTIFY消息中被发送的,需要在类实现中加入消息映射。与owner-draw方式比较,这种绘制方式最大的优势是对绘制的阶段进行了严格控制,可以在不同的响应阶段进行不同的绘制策略,比如既可以进行默认绘制,也可以重载函数进行特殊绘制,还可以只改变一些变量的值让MFC自己去按照要求重绘。我们知道owner-draw方式的绘制函数中,对于所有的绘制细节都需要进行GDI或GDI+的代码控制,而custom-draw方式中,我们可能仅仅改变几个变量值(比如控件颜色)就完成了需求。custom-draw方式支持的控件包括ListView、ToolBar、ToolTip、TreeView等,其中对于ListCtrl支持所有的样式。关于custom-draw的详细信息,可以参考这篇文章http://msdn.microsoft.com/zh-cn/library/ms364048(VS.80).aspx。

二、加载缩略图
这个其实很简单,自己创建一个CImageList类型的对象,并自定义图像的大小及像素类型,然后调用CListCtrl的SetImageList函数设置就可以了。需要注意的一点是,normal和small两种type中,small类型必须设置。

三、自定义表头
需要写一个继承CHeaderCtrl的子类,实现DrawItem函数,在其中进行表头背景和字体、文本颜色等设置并进行绘制;如果要改变表头的高度,可以映射HDM_LAYOUT消息响应函数,在其中设置控件布局。之后在自己的ListCtrl类中声明一个自定义的HeaderCtrl类型变量,并在PreSubclassWindow函数中调用HeaderCtrl的SubclassWindow函数使其子类化,然后在初始化的时候使其各Item的format具有HDF_OWNERDRAW风格就可以了。

四、调整CListCtrl的背景、字体、文本颜色和行高
实现思路与上述表头的方法基本相同。当然要设置list的自绘风格,并选择自绘的方式。另外对于调整行高,如果加载了缩略图的话,行高就会随之调整了。另一个简单的办法就是设置字体的大小来实现,与缩略图是一个道理。如果想自己精确定义行高,则比较麻烦一点。首先设置list的自绘风格,然后重载MeasureItem函数,在其中设置结构体中的item高度变量的值,再在消息映射中添加ON_WM_MEASUREITEM_REFLECT(),就可以让list在合适的时候响应来改变行高。需要注意的两点是:
1、MeasureItem与WM_MEASUREITEM消息响应函数OnMeasureItem是不同的;
2、触发MeasureItem函数调用的WM_MEASUREITEM消息是在一定的情况下才被发送,比较简单的方法是send一个WM_WINDOWPOSCHANGED消息来触发。
3、设置LVS_OWNERDRAWFIXED风格需要在Create或者PreSubclassWindow函数中进行,否则MeasureItem不会被调用。