一、所有者绘制(owner-draw)和自定义绘制(custom-draw)
一、给控件添加排序功能
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不会被调用。