一个带自定义滚动条控件的自定义列表视图控件,使用 WTL






4.85/5 (18投票s)
2006 年 12 月 4 日
6分钟阅读

110874

4600
本文旨在展示使用自定义滚动条控件定制列表视图控件(报表视图,单行模式)的一种可能性。
引言
本文旨在展示使用自定义滚动条控件定制列表视图控件(报表视图,单行模式)的一种可能性。我希望本文能帮助某些人享受编程的乐趣,而不是仅仅在网上冲浪。
控件定制的主要思想并非创新。通常,我们需要改变控件的外观,同时保留其内部行为。
行动计划相当简单:动态创建控件(或子类化现有控件),并重写所需的消息处理程序——例如,绘制消息(自绘、自定义绘制或WM_PAINT
)。这取决于控件(如果它支持所需的绘制方案以简化其定制)以及我们希望对其进行多大程度的改变。
让我们从滚动条控件开始。像往常一样,我们创建标准控件 (CScrollBar
) 的继承,并以我们自己的方式处理一些必需的消息。CScrollBar
控件既不支持自绘 (WM_DRAWITEM
) 也不支持自定义绘制 (NM_CUSTOMDRAW
) 规范,因此我们被迫完全自己绘制它(参见 WM_PAINT
处理程序)。显然,我们需要处理标准的鼠标消息 (WM_LBUTTONDOWN
, WM_LBUTTONUP
和 WM_MOUSEMOVE
) 以在必要时更新滑块位置。为了模拟标准滚动条的行为,我们添加了对 WM_VSCROLL
请求全系列的支持(详见 MSDN)。您会注意到标准滚动条能够处理对其元素的单次点击以重复处理它们。为了实现相同的功能,我们只需在鼠标按下事件发生时设置一个计时器。我们也不应忘记鼠标捕获,即使鼠标光标在滚动条控件之外也能获取鼠标消息。
还有一个技巧与SBM_SETSCROLLINFO
消息的处理有关。我发现当控件收到该消息时,它看起来像标准控件,直到调用重绘操作。因此,我不得不处理此通知,并在需要时(取决于fRedraw
标志)调用我们自己的绘制过程。
LRESULT CMyScrollbar::OnSetScrollInfo( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& /*bHandled*/ ) { const BOOL fRedraw = ( BOOL ) wParam; LRESULT res = DefWindowProc( uMsg, ( WPARAM ) FALSE, lParam ); if ( fRedraw ) { Invalidate(); } return res; }
为了使自定义滚动条控件更加方便,提供了 SetBuddy
方法,该方法允许设置一个伴侣窗口来发送 WM_VSCROLL
通知(而不是默认的父窗口)。
最后与滚动条控件相关的是资源。如果您研究资源,您会发现该控件使用了四张位图来显示
- 上箭头 (
IDB_SCROLL_UP
), - 可移动滚动条滑块的背景矩形 (
IDB_SCROLL_BACK
), - 滚动条滑块本身 (
IDB_SCROLL_THUMB
), - 下箭头 (
IDB_SCROLL_DOWN
)。
所以,如你所见,这并不难。
关于列表视图控件出现的问题多说一点。由于我们想使用外部滚动,我们必须使用 LVS_NOSCROLL
样式。但结果是,控件完全停止处理滚动。我们该怎么办?似乎唯一的办法是模拟滚动,通过处理 WM_VSCROLL
消息。为了实现这一点,我决定记住当前第一个可见行的位置,并在需要滚动时改变它(参见 CMyListCtrl
的 m_IndexOffset
成员)。听起来很困难?但这是最大的问题,因为其他问题都可以轻松解决。
为了改变控件的外观,我们处理自定义绘制通知。为了支持通过键盘滚动,我们也处理来自键盘的消息。
关于我为什么在 OnPrePaint
方法中处理绘制,有几句话要说。如你所知,我们可以返回 CDRF_NOTIFYITEMDRAW
值来接收每个需要绘制的项目附加消息。问题在于我们使用原始列表视图控件的方式。由于我们通过 LVS_NOSCROLL
样式拒绝了滚动,我们处理的是前 GetCountPerPage
个项目。而且每次我们收到整个客户区域的 WM_PAINT
消息时,这意味着这些项目应该被绘制。问题出现在选定项目上,因为它们通过它们的绝对索引进行重绘。这意味着每次我们收到 OnItemPrePaint
消息时,我们无法识别哪个项目必须被绘制:是相对索引(参见 LPNMLVCUSTOMDRAW
结构的 dwItemSpec
字段)还是绝对索引。所以,我决定一石二鸟:不关心项目索引,我们只需要绘制并使绘制尽可能简单(这真的很简单,因为我们通过从第一个可见项开始的循环来绘制每个项目)。为了使其更有效,我排除了与当前剪辑区域没有交集的项目 (GetClipBox
)。
如果需要改变列表控件中行的默认高度,请不要忘记使用 WM_MEASUREITEM
消息。
最后,WM_CTLCOLORXXX
消息用于获取控件(滚动条、列表和单选按钮)的画刷。每个控件都使用自己的画刷,因为它们是通过 CreatePatternBrush
系统调用以其特定的矩形创建的。
你会发现有两种不同类型的项目——桌面应用程序版本和 Pocket PC 版本。你可以使用你更需要的版本。由于使用了 WTL,两者的代码是相同的。
我要感谢 Hyungchul Shin,文章 Pocket PC 上带自定义图像背景的透明控件的作者,因为创建自定义背景的技术完全采纳自该文章。
今天就到这里!祝您一切顺利!
典型问题
= 我没有收到 WM_MEASUREITEM 消息,因此无法改变列表行的高度?这可能发生在您获取现有控件实例并进行子类化时。WM_MEASUREITEM
消息仅在控件创建时发送一次。因此,当您手动创建列表视图控件时,您会收到此消息;当它自动创建时,您不会收到。实际上,我有一个建议,通过发送 WM_WINDOWPOSCHANGED
消息来让系统重新发送 WM_MEASUREITEM
消息,但我没有足够的勇气去测试它。您可以尝试一下。
你可能忘记在对话框的消息映射中添加 REFLECT_NOTIFICATIONS
宏了,不是吗?
是的,这确实是真的。系统就是不为自定义滚动条控件发送此消息(这可能是一个错误)。因此,唯一的选择是在 WM_PAINT
或 WM_ERASEBKGND
处理程序中调用此消息来模拟其发送。
请参阅 MSDN 中的 SHInitDialog
函数以及我代码中的示例。
因为 Pocket PC 和桌面版本的代码是相同的。如果您在 WinCE 上尝试调用 GetCursorPos
,您肯定会收到“函数不支持”的错误。因此,我们必须使用自己的方法来知道鼠标光标在哪里。如果您查看代码,您会发现我的想法是在 WM_LBUTTONDOWN
处理程序中记住光标位置,并在 WM_MOUSEMOVE
处理程序中更新它。
碰巧我知道 :)。我曾经发现,在 WinCE 上无法强制 EDIT 控件改变其背景——它总是白色的。我所有改变其行为的尝试都失败了,直到两天无用的诅咒之后,我在一个论坛上读到一条消息。消息说,对于 EDIT 控件,我们必须采取不同的方式——“阅读 MSDN,然后反其道而行之” :)。示例代码如下:
LRESULT AppDlg::OnCtlColor( UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled ) { ... // Make sure you work with edit control if ( ... ) { HBRUSH hBrush = getBkBrush( editCtrl ); ::SetWindowLong( m_hWnd, DWL_MSGRESULT, ( LONG ) hBrush ); res = TRUE; } ... }