C++学习笔记 2010-04-01 10:51:11 阅读151 评论0 字号:大中小 订阅
转自http://blog.sina.com.cn/s/blog_48f93b530100eg3z.html
前面写过两篇CListCtrl控件的开发总结,最近在开发和应用当中又发现了几个比较有趣的问题,主要是关于自绘滚动条的。从我的感觉来说,做CListCtrl控件(我们一直在讨论report风格)的自绘滚动条是比较复杂的,因为里边有很多特殊的地方,很可能会让你备受挫折。
先说一下实现自绘滚动条的思路。首先要隐藏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中的滚动条根本不是窗口,也就是说滚动条不是scroll bar控件,只是CListCtrl自行绘制上去的假滚动条。因此上面这些对于scroll bar操作的函数都无效也就可以理解了,大家可以留意一下GetScrollBarCtrl函数返回的全是NULL。
那么我们能不能通过修改CListCtrl的窗口风格来实现呢?
涉及到滚动条的风格有两个,一个是通用的窗口风格WS_HSCROLL和WS_VSCROLL,另一个是列表控件专有风格LVS_NOSCROLL。
先说LVS_NOSCROLL。如果你给CListCtrl设置该风格的话,你会发现list中关于scroll的窗口过程都被屏蔽了,也就是说,CListCtrl将只显示设定的列表窗口区域,对于区域以外的包括UI显示和消息处理在内的这些过程,将全部忽略。这样的话,隐藏原有滚动条并加载自己的滚动条这个思路将行不通。而且根据MSKB ID为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信息,完全用scroll bar的信息。无论哪种方法,都必须对结构体SCROLLINFO中的参数意义有比较深刻的理解。
SCROLLINFO的定义如下:
typedef struct tagSCROLLINFO
{
UINT cbSize;
UINT fMask;
int nMin;
int nMax;
UINT nPage;
int nPos;
int nTrackPos;
} SCROLLINFO, FAR *LPSCROLLINFO;
typedef SCROLLINFO 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窗口刷新时告诉重合区域不用重画,从而保证滚动条绘制正确。
好了,几个比较关键的问题都讲完了,这篇就写到这吧。在公司写效率确实高一些,呵呵。如果有写的不正确的地方,希望大家告诉我,谢谢。