所有者绘制(owner-draw)和自定义绘制(custom-draw)
这两种方法应该是控件自绘中最常用到的普遍方法。(当然如果只是改变控件颜色只需要处理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不会被调用。

给控件添加排序功能

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, FARLPSCROLLINFO;
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、拖拽、组功能、虚拟列表等,希望以后有机会的话能够进行总结。

先说一下实现自绘滚动条的思路。首先要隐藏CListCtrl自带的滚动条,如果你简单的认为加上LVS_NOSCROLL风格就能搞定的话,你一定会大失所望,具体我们后面会讲到。然后就是要封装自己的滚动条控件了,考虑到有竖直滚动条和水平滚动条两种,我们的封装可能还要费一番周折。除了要响应消息自绘外,滚动条控件还要向parent控件(即CListCtrl)发送相应的WM_VSCROLL和WM_HSCROLL消息及消息参数。
最后,CListCtrl控件要响应这两个消息并进行相应处理,这个过程需要清楚地理解SCROLLINFO结构体中各个变量含义并进行应用,另外就是处理一些UI绘制和刷新上的麻烦。

好了,下面我们就重点总结一下上面过程中会遇到的难题,并给出解决方法。

一、隐藏CListCtrl自带滚动条
CWnd类有一系列的关于Scroll的函数,感觉好像有很多方法可以实现我们的目的。
1、调用EnableScrollBarCtrl直接将不需要的类型的滚动条disable掉;
2、调用GetScrollBarCtrl得到滚动条指针,然后用ShowWindow(SW_HIDE)将其隐藏掉;
3、调用ShowScrollBar直接隐藏。
遗憾的是,对于report类型的CListCtrl来说,上面的方法全部无效。(好像对于其他非report类型的CListCtrl控件或者CListView控件有些方法是有效的,没有进行具体验证。)其实用spy++查看一下就知道,CListCtrl中的滚动条根本不是窗口,也就是说滚动条不是scrollbar控件,只是CListCtrl自行绘制上去的假滚动条。因此上面这些对于scrollbar操作的函数都无效也就可以理解了,大家可以留意一下GetScrollBarCtrl函数返回的全是NULL。

那么我们能不能通过修改CListCtrl的窗口风格来实现呢?
涉及到滚动条的风格有两个,一个是通用的窗口风格WS_HSCROLL和WS_VSCROLL,另一个是列表控件专有风格LVS_NOSCROLL。
先说LVS_NOSCROLL。如果你给CListCtrl设置该风格的话,你会发现list中关于scroll的窗口过程都被屏蔽了,也就是说,CListCtrl将只显示设定的列表窗口区域,对于区域以外的包括UI显示和消息处理在内的这些过程,将全部忽略。这样的话,隐藏原有滚动条并加载自己的滚动条这个思路将行不通。而且根据MSKBID为137520文章所说,对于report类型的列表控件,设置LVS_NOSCROLL风格还要面临header控件不能正常显示的问题。
再来看WS_HSCROLL和WS_VSCROLL。如果只是简单的通过调用ModifyStyle去掉窗口WS_HSCROLL或WS_VSCROLL风格是没作用的,我们需要在每次重新计算窗口的客户区域的时候都调用ModifyStyle设置scroll风格才行,这样CListCtrl将没有机会自动为窗口加上滚动条,代码如下:

void CXXListCtrl::OnNcCalcSize(BOOL bCalcValidRects,NCCALCSIZE_PARAMS FAR*lpncsp)
{
ModifyStyle( WS_HSCROLL | WS_VSCROLL, 0 ); //去掉水平滚动条和竖直滚动条
CListCtrl::OnNcCalcSize(bCalcValidRects,lpncsp);
}

麻烦还没完,在XP窗口风格下上面的代码没有任何问题。但是当我们链接Windows经典窗口风格的代码库时,上面的代码就不能实现隐藏滚动条的功能了。具体原因不是很清楚,我猜想可能是Windows的不同窗口风格库的内部窗口过程不同引起的。
还有解决的办法吗?只要我们想,总会有办法!那就是调用Windows的API函FlatSB_EnableScrollBar。你只要在你的自绘listctrl类的合适的位置调用以下代码,控件的滚动条将完美地被隐藏起来,而且只需要调用一次就可以:

InitializeFlatSB(this->m_hWnd);
FlatSB_EnableScrollBar(this->m_hWnd, SB_BOTH,ESB_DISABLE_BOTH);

具体关于该函数的细节大家可以去查阅MSDN,但是我并没有想明白为什么这个API函数能做到EnableScrollBarCtrl成员函数做不到的功能,没有源码无从考证:(

二、结构体SCROLLINFO
在这里我并不想过多讨论关于自绘滚动条控件的实现问题,因为看上去思路很简单,并且实现起来也只是细节和完善性的工作。我认为这一块的问题在于一些概念的理解和把握,其中最重要的就是结构体SCROLLINFO。
首先说一下GetScrollInfo和SetScrollInfo这两个函数。CListCtrl类和CScrollBar类都有这两个成员函数,其中CListCtrl类的成员函数是从CWnd继承而来。对于CScrollBar类来说,GetScrollInfo用来得到滚动条的滚动信息,也就是结构体SCROLLINFO中包含的内容;SetScrollInfo用来设置滚动信息使滚动条进行相应的滚动。我们可以理解为CScrollBar类通过这两个函数完成滚动条正确的位置滚动和绘制。前面提到,CListCtrl控件自带的滚动条并不是CScrollBar控件,而是自己画上去的假窗口,因此实际上是通过CLIstCtrl的GetScrollInfo和SetScrollInfo函数完成了滚动条位置信息的查询和设置。而且在CListCtrl中,这些信息是有其特殊性的,后面讲SCROLLINFO的时候会详细说明。
那么当我们把CListCtrl的滚动条隐藏了,甚至用API将其disable了,GetScrollInfo和SetScrollInfo还会正常工作吗?回答是肯定的。用CListCtrl自带的滚动条和用自己封装的滚动条的区别在于:
1、用CListCtrl的滚动条,可通过GetScrollInfo查询滚动信息,通过SetScrollInfo可使滚动条调整位置,且list客户区会自动滚动;用户通过操作滚动条可以滚动list客户区,且改变list的滚动信息;
2、用自己的滚动条,调用CListCtrl的GetScrollInfo可查询滚动信息,通过SetScrollInfo可设置滚动信息但滚动条控件不会自动调整位置,但list客户区可以自动滚动;用户操作滚动条,需要通过调用CScrollBar的SetScrollInfo进行设置才能影响滚动条绘制,而list客户区不会自动滚动,此时list的滚动信息不会被自动修改。
因此,加自己的滚动条实际上是将list和滚动条的滚动信息一致化的问题。或者你也可以完全抛弃CListCtrl中保存的SCROLLINFO信息,完全用scrollbar的信息。无论哪种方法,都必须对结构体SCROLLINFO中的参数意义有比较深刻的理解。

SCROLLINFO的定义如下:
typedefstruct tagSCROLLINFO
{
UINT cbSize;
UINT fMask;
int nMin;
int nMax;
UINT nPage;
int nPos;
int nTrackPos;
} SCROLLINFO, FARLPSCROLLINFO;
typedefSCROLLINFO CONST
LPCSCROLLINFO;

cbSize是该结构体的字节数,每次应用之前设置为sizeof(SCROLLINFO);
fMask指明下面的几个参数哪些是有效值,具体请参考MSDN;
nMin和nMax指明了滚动条最小和最大滚动位置。字面上很好理解,但是这个位置以什么为单位呢?MSDN上并没有说明。其实单位并没有规定和限制,只要在这个结构体的参数中统一就可以。比如在CListCtrl中就以控件中的行为单位,nMin为0,nMax一般为list的总行数。
nPage为页大小,也就是你点击滚动条上滑块以外的区域时,滚动条会滚动一页,而页的大小就是由该值指定;同时,滚动条绘制滑块的大小时也会依据此值;
nPos为滑块当前的位置,也就是滚动位置;
nTrackPos是用户拖动滑块时的即时位置,该值一般用于响应滚动条消息时与SB_THUMBTRACK消息一起作为消息参数,而不能用于设置滚动条信息。

OK,现在可以完善我们的思路了。加载自绘滚动条的步骤如下:
1、隐藏CListCtrl原有滚动条;
2、封装继承于CScrollBar的自绘滚动条控件,根据SCROLLINFO完成滚动条绘制,并将用户操作封装成WM_HSCROLL或WM_VSCROLL消息传递给父类CListCtrl。以竖直滚动条为例,我们要将WM_VSCROLL消息的消息参数分为以下几类:SB_LINEDOWN,SB_LINEUP,SB_PAGEDOWN,SB_PAGEUP,SB_THUMBTRACK和SB_THUMBPOSITION,其意义分别为向下移动一个位置,向上移动一个位置,向下移动一页,向上移动一页,滑块当前正在被拖动的位置和滑块被结束拖动到某位置。CListCtrl接收到消息以后需要根据类型调用滚动条的SetScrollInfo设置滚动信息,使滚动条绘制滚动位置,并调用Scroll函数滚动list的客户区;
3、ClistCtrl子类负责初始化滚动条控件,设置控件尺寸及位置,并响应OnSize函数随时改变控件大小位置。还要响应WM_MOUSEWHEEL消息使滚动条控件进行正确滚动。当然还有刚才提到的接收滚动条控件的消息,并进行相应处理。

看上去应该没什么问题了,但是你还是会遇到一些麻烦。

三、SB_THUMBTRACK和SB_THUMBPOSITION
滚动条控件的消息码是在OnVScroll或OnHScroll响应函数中作为nSBCode参数被传递的,指明了当前的操作类型。但是对于CListCtrl来说,以上两个消息码却比较特殊,因为在CScrollBar中给CListCtrl发送带有这两个消息码的消息,响应函数并不会做出响应。也就是说,CListCtrl内部自动处理了该消息,并没有调用OnVScroll或OnHScroll做响应。具体为什么这样设计,可能是出于某种考虑;但是如果我们要对这两个操作自己特有的处理,或者我们应用的SCROLLINFO中的单位与CListCtrl默认的不统一的话,list的默认响应将满足不了我们的需要。
要解决这个问题,可以自己定义新的消息码来代替。

四、滚动条绘制问题
当你按照上面步骤写好代码,操作滚动条的时候,你可能失望的看到你的滚动条并不能正确绘制,而且现象很特别,好像是绘制函数出了问题。
如果要深入的理解这个问题的原因,可以看一下我前面写的一篇博文——“Windows窗口相关的一些概念解释”。简单一点说,就是CListCtrl的窗口风格没有设置正确。我们自己的滚动条肯定是要时刻保持在list窗口的上方,因此我们需要在CListCtrl中写上这样的代码:

scrollCtrl.Create(WS_CHILD|WS_VISIBLE|WS_GROUP|SBS_VERT,scrollRect, this, ID_SCROLL_XX);
scrollCtrl.SetWindowPos(&wndTop, 0, 0, 0, 0,SWP_NOMOVE | SWP_NOSIZE |SWP_SHOWWINDOW);

可以看到,滚动条控件是作为list窗口的子窗口被创建的,而且被放在窗口的z-order的最上方,因为它一定在list窗口及其子窗口的最上面显示。如果这个时候你的list窗口是自绘控件,或者说上面放置了其他控件作为子窗口,那么滚动条的刷新一定会出问题,原因是因为你没有给滚动条控件设置WS_CLIPSIBLINGS风格。
这样可以了吗?此时你还要为CListCtrl子类加上WS_CLIPCHILDREN风格,原因是你的滚动条控件是顶层窗口,系统会认为在刷新list窗口时它是不用刷新的,这样滚动条和list窗口重合的部分在刷新上会出现问题;当我们给list窗口设置了WS_CLIPCHILDREN风格之后,系统就会在list窗口刷新时告诉重合区域不用重画,从而保证滚动条绘制正确。