使用 C++ 辅助类向 CWnd 或 CDialog 添加滚动
一篇关于使用 C++ 辅助类向 CWnd 或 CDialog 添加滚动功能的文章。
引言
为自定义的 CWnd
或 CDialog
派生类实现标准的滚动行为,使用 MFC 来说相当繁琐。至少需要编写 3 到 4 个消息处理程序,并且每个滚动条都有一些滚动参数,这些参数需要在窗口或对话框大小调整时进行调整。在本文中,我将介绍一个 C++ 辅助类,该类可用于为任何 CWnd
或 CDialog
类添加水平和/或垂直滚动功能。与其他实现不同的是,您不需要派生自某个可滚动对话框基类。使用辅助类 (CScrollHelper
) 不需要更改您的继承层次结构。
背景
在详细介绍如何使用辅助类之前,我想引用几篇很好的解释滚动实现基础的文章。第一篇是 MSDN 文章 ID 262954,“如何在 Visual C++ 中创建带滚动条的可调整大小的对话框”。这篇文章提供了一个为 CDialog
类添加垂直滚动条支持的示例。第二篇是来自 Jeff Prosise 的著作《MFC 编程,第二版》(Microsoft Press)。本书第 2 章详细解释了在 CWnd
派生类中实现滚动的细节,并提供了一个支持水平和垂直滚动的简单电子表格应用程序的示例代码。
对于 CWnd
派生类,实现滚动的第一个步骤是确保窗口在创建时具有窗口样式 WS_HSCROLL
(如果您想要水平滚动条)和/或 WS_VSCROLL
(如果您想要垂直的、靠右的滚动条)。
Create(NULL, "CScrollWnd", WS_CHILD | WS_VISIBLE | WS_HSCROLL | WS_VSCROLL, CRect(0,0,0,0), parentWnd, 0, NULL);
对于对话框,窗口样式通常通过资源编辑器设置。在 VS 2003 中,我通常会确保任何我想要可滚动的对话框都设置了以下属性:
- 如果您的对话框是弹出式对话框,则
Border = "Resizing"
。如果您的对话框是嵌入在容器父窗口中的子窗口,您可以选择其他样式,例如“None”(演示项目中有此示例)。 Clip Children = "True"
。此设置有助于在调整对话框大小时最小化显示闪烁。Horizontal Scrollbar = "True"
。这等同于添加窗口样式WS_HSCROLL
。- 如果您的对话框是嵌入在容器父窗口中的子窗口,则
Style = "Child"
。 Vertical Scrollbar = "True"
。这等同于添加窗口样式WS_VSCROLL
。Visible = "True"
。在某些情况下,Visual Studio 默认为“False
”,因此您需要检查此设置。
使用代码
CScrollHelper
类实现在两个源文件中:*ScrollHelper.h* 和 *ScrollHelper.cpp*。该类的公共接口如下所示:
class CScrollHelper { public: CScrollHelper(); ~CScrollHelper(); // Attach/detach a CWnd or CDialog. void AttachWnd(CWnd* pWnd); void DetachWnd(); // Set/get the virtual display size. // When the dialog or window // size is smaller than the display // size, then that is when // scrollbars will appear. Set either // the display width or display // height to zero if you don't want to // enable the scrollbar in the // corresponding direction. void SetDisplaySize(int displayWidth, int displayHeight); const CSize& GetDisplaySize() const; // Get current scroll position. // This is needed if you are scrolling // a custom CWnd which implements its // own drawing in OnPaint(). const CSize& GetScrollPos() const; // Get current page size. Useful // for debugging purposes. const CSize& GetPageSize() const; // Scroll back to top, left, or // top-left corner of the window. void ScrollToOrigin(bool scrollLeft, bool scrollTop); // Message handling. void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt); void OnSize(UINT nType, int cx, int cy); };
为了给您的 CDialog
派生类(例如 CScrollDlg
)添加滚动支持,我们首先在对话框的类定义(头文件)中添加一个 private
成员:
class CScrollHelper; // Forward class declaration. class CScrollDlg : public CDialog { ... private: CScrollHelper* m_scrollHelper; };
接下来,我们在类定义(头文件)中添加四个与滚动相关的消息处理程序:
// Generated message map functions. //{{AFX_MSG(CScrollDlg) ... afx_msg void OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); afx_msg void OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar); afx_msg BOOL OnMouseWheel(UINT nFlags, short zDelta, CPoint pt); afx_msg void OnSize(UINT nType, int cx, int cy); //}}AFX_MSG DECLARE_MESSAGE_MAP()
在对话框源文件中,包含 ScrollHelper.h 文件,并添加这四个消息处理程序的映射条目:
#include "ScrollHelper.h" ... BEGIN_MESSAGE_MAP(CScrollDlg, CDialog) //{{AFX_MSG_MAP(CScrollDlg) ... ON_WM_HSCROLL() ON_WM_VSCROLL() ON_WM_MOUSEWHEEL() ON_WM_SIZE() //}}AFX_MSG_MAP END_MESSAGE_MAP()
然后,在对话框构造函数中创建一个辅助类实例,并将对话框附加到该实例:
CScrollDlg::CScrollDlg(CWnd* pParent) : CDialog(IDD_SCROLL_DLG, pParent) { // Create the scroll helper // and attach it to this dialog. m_scrollHelper = new CScrollHelper; m_scrollHelper->AttachWnd(this); }
请记住在对话框析构函数中删除辅助类实例:
CScrollDlg::~CScrollDlg()
{
delete m_scrollHelper;
}
接下来,通过简单地委托给辅助类来实现在这四个消息处理程序,辅助类拥有与消息处理程序完全相同的签名的方法:
void CScrollDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { m_scrollHelper->OnHScroll(nSBCode, nPos, pScrollBar); } void CScrollDlg::OnVScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar) { m_scrollHelper->OnVScroll(nSBCode, nPos, pScrollBar); } BOOL CScrollDlg::OnMouseWheel(UINT nFlags, short zDelta, CPoint pt) { BOOL wasScrolled = m_scrollHelper->OnMouseWheel(nFlags, zDelta, pt); return wasScrolled; } void CScrollDlg::OnSize(UINT nType, int cx, int cy) { CDialog::OnSize(nType, cx, cy); m_scrollHelper->OnSize(nType, cx, cy); }
完成滚动支持的最后一步是设置辅助类中的“显示大小”。这代表了对话框的虚拟显示区域,您可以预先确定并在代码中固定该值,或者在运行时动态计算。如果您熟悉 C# 中的 Windows Forms 编程,显示大小的概念类似于可滚动控件中的 DisplayRectangle
属性。显示大小代表滚动条出现或消失的确切阈值或点。例如,如果用户调整对话框大小,直到它小于显示大小时,滚动条就会出现,允许您滚动对话框以访问整个虚拟显示表面。如果用户将对话框调整得比显示大小大,则滚动条会消失。显示大小通常是在第一次设置后固定的值:
CScrollDlg::CScrollDlg(CWnd* pParent) : CDialog(IDD_SCROLL_DLG, pParent) { // Create the scroll helper and // attach it to this dialog. m_scrollHelper = new CScrollHelper; m_scrollHelper->AttachWnd(this); // We also set the display size // equal to the dialog size. // This is the size of the dialog // in pixels as laid out // in the resource editor. m_scrollHelper->SetDisplaySize(701, 375); // Create the dialog. Create(IDD_SCROLL_DLG, pParent); }
在 CScrollHelper
类的接口中,您可能已经注意到了 GetPageSize()
方法。页面大小是辅助类内部管理的另一个重要滚动参数。基本上,页面大小与对话框大小相同(或者更确切地说,是对话框客户区的范围)。这类似于 Win Forms 中的 ClientRectangle
属性。页面大小与显示大小的比例由 Windows 用来确定滚动条“拇指”部分的大小。拇指的大小让您大致了解您正在查看的虚拟显示表面的多少(拇指的位置告诉您您正在查看虚拟显示表面的哪个部分)。例如,假设您在 Microsoft Word 中查看一个非常长的文档(100 页)。您会发现垂直滚动条的拇指非常小。另一方面,如果您正在编辑一个比单页略长的文档,您会发现滚动条上的拇指接近其最大尺寸。
TestScroll 应用程序
演示项目 (TestScroll) 说明了辅助类的使用。这是一个 MDI 应用程序,我使用 Visual Studio 从头开始创建的。为了生成项目,我默认了所有 VS 向导选项,除了“文档/视图支持”复选框,我将其取消选中。然后我编写了两个新类:CScrollDlg
和 CScrollWnd
。CScrollDlg
是一个使用辅助类来实现滚动的对话框类(如上面代码部分所示)。它只显示四个按钮,分别位于虚拟显示区域的四个角落,还有一个 CListBox
,用于显示对话框调整大小时当前的滚动参数。CScrollWnd
是一个自定义的 CWnd
派生类,它展示了如何为非对话框类添加滚动支持。这里实现的一个有趣之处在于,该类会绘制一个代表固定显示大小的矩形。因此,在调整窗口大小时,您可以清楚地看到滚动条何时出现或消失。
生成的 MDI 应用程序提供了一个名为 CChildView
的类,它包含在 MDI 子框架窗口中。CChildView
实际上是我上面两个新类的集成起点。我修改了 CChildView
,使其不再提供自己的内容,而是创建覆盖其整个客户区的 CScrollDlg
或 CScrollWnd
实例。
int CChildView::OnCreate(LPCREATESTRUCT lpCreateStruct) { if ( CWnd::OnCreate(lpCreateStruct) == -1 ) return -1; // We either create a CScrollWnd or a CScrollDlg. // We alternate using a counter. static int counter = 0; if ( counter % 2 == 0 ) m_scrollWin = new CScrollWnd(this); else m_scrollWin = new CScrollDlg(this); ++counter; return 0; }
要测试演示应用程序,只需使用“文件 | 新建”菜单项打开 MDI 子窗口。第一次打开时,您会得到一个 CScrollWnd
实例。第二次,将创建一个 CScrollDlg
。每次选择“新建”时,都会在这两种示例之间交替。
下面的截图显示了 CWnd
派生类中的滚动。顶部的 MDI 子窗口显示滚动条,因为窗口大小小于显示大小。底部的 MDI 子窗口没有滚动条,您可以清楚地看到窗口大小大于固定的显示大小(由蓝色矩形表示)。另外,在底部的 MDI 子窗口中,请注意页面大小显示为 0 x 0。这就是辅助类如何隐藏滚动条的方法——通过将滚动位置和页面大小等内部滚动参数设置为零值。
下面的截图显示了 CDialog
中滚动的示例。请注意,底部 MDI 子窗口的标题栏显示了当前的滚动位置,并且对话框已滚动到最右下方。当滚动到最大位置时,一个值得注意的有趣之处是,滚动位置 (222, 230) 加上页面大小 (479, 145) 等于显示大小 (701, 375)。
总而言之,CScrollHelper
类简化了为 CWnd
或 CDialog
类添加滚动支持的过程,因为它负责实现所有必需的消息处理程序。使用辅助类的关键在于能够正确设置显示大小。对于 CWnd
类,您可能还需要进一步研究 GDI 映射模式以及逻辑坐标和设备坐标之间的转换。
历史
- 2005 年 7 月 5 日
- 初始版本。
- 2005 年 7 月 6 日
- 根据 MSDN 文章 ID 152252,“如何在滚动消息期间获取 32 位滚动位置”,添加了
Get32BitScrollPos()
函数。 - 在
OnHScroll
/OnVScroll
中处理SB_THUMBPOSITION
。 - 添加了 VC++ 6.0 演示项目。(感谢 PJ Arends 提出的修复建议。)
- 根据 MSDN 文章 ID 152252,“如何在滚动消息期间获取 32 位滚动位置”,添加了
- 2005 年 9 月 8 日
- 添加了
GetClientRectSB()
辅助函数。 - 在演示项目 (
CScrollWnd
类) 中使用内存 DC 绘图,以避免调整大小时闪烁。
- 添加了