使用 C++ 帮助类进行控件定位和调整大小
为 CWnd 或 CDialog 添加控件的布局管理,使用 C++ 辅助类。
引言
在本文中,我介绍了一个 C++ 辅助类 (CLayoutHelper
),可用于管理 CWnd
或 CDialog
中子控件的定位和大小。通过这种方法,无需更改继承层次结构,因为不需要从任何对话框基类派生。典型用法包括在 CWnd
或 CDialog
中创建辅助类实例,选择布局算法(样式),然后添加要由辅助类管理的子控件。您可以调用一个方法来添加窗口的所有直接子控件,或仅添加您希望管理的控件子集。您可以指定布局选项以按控件为单位控制布局行为,还可以动态更新这些选项。使用支持的布局选项可以实现一些有趣的效果,例如将控件置于对话框的某个位置居中、固定到特定点,或强制控件保持固定的纵横比(这对于显示照片或视频的控件可能很有用)。
滚动
在某些方面,滚动可以被视为布局管理的对立面。当您将对话框或窗口的尺寸越缩越小时,最终会达到一个点,此时继续调整子控件的位置或大小是不受欢迎的。控件可能会开始重叠,或者变得太小而无法使用。一个简单的解决方案是在达到某个最小窗口大小时停止布局管理。通常,一个更有用的解决方案是在达到最小尺寸时调用滚动条,以便用户仍然可以访问整个对话框或窗口表面。在我之前的 文章中,我介绍了一个类似的辅助类,用于为 CWnd
或 CDialog
添加滚动支持。附带的演示应用程序说明了如何使用这两个辅助类 (CScrollHelper
和 CLayoutHelper
) 来实现所需的最小尺寸行为。这两个类是完全独立的,但可以轻松地集成到同一个对话框或窗口中。
控件定位和大小调整
要启用对话框的调整大小(和滚动)功能,我通常会使用 Visual Studio 资源编辑器首先确保以下属性已设置:
- 边框 = “可调整大小”(如果您使用的是弹出式对话框)。如果您的对话框是嵌入在容器父窗口中的子窗口,您可以选择其他样式,例如“无”(演示项目中有此示例)。
- 剪裁子控件 = “真”。此设置有助于最小化对话框调整大小时的显示闪烁。
- 水平滚动条 = “真”。这等同于添加窗口样式
WS_HSCROLL
。 - 样式 = “子窗口”(如果您的对话框是嵌入在容器父窗口中的子窗口)。
- 垂直滚动条 = “真”。这等同于添加窗口样式
WS_VSCROLL
。 - 可见 = “真”。Visual Studio 在某些情况下默认设置为“假”,因此您需要检查此设置。
当对话框或窗口大小调整时,会生成 WM_SIZE
消息。为了处理这些消息,我会在窗口的消息映射中添加一个 ON_WM_SIZE()
条目。
BEGIN_MESSAGE_MAP(CMyDlg, CDialog) //{{AFX_MSG_MAP(CMyDlg) ... ON_WM_SIZE() //}}AFX_MSG_MAP END_MESSAGE_MAP()
然后,可以根据需要,在 OnSize()
处理程序中执行控件的重新定位和大小调整。
void CMyDlg::OnSize(UINT nType, int cx, int cy) { CDialog::OnSize(nType, cx, cy); // Reposition and resize child controls // to adapt to the new dialog size. ... }
可以使用 SetWindowPos()
同时重新定位和调整单个子控件的大小。我通常这样调用它:
UINT flags = SWP_NOZORDER | SWP_NOACTIVATE;
::SetWindowPos(pControl->m_hWnd, 0, newX, newY,
newWidth, newHeight, flags);
这实际上是布局管理的核心。布局管理器的工作是基于关于所需布局行为的输入,执行 newX
和 newWidth
等参数的计算,然后使用 SetWindowPos()
实际移动和调整子控件的大小。
布局算法和选项
CLayoutHelper
实现提供了两种布局算法。默认算法是一个比例调整器,支持多种布局约束,用于按控件为单位精细调整定位和大小行为。第二种算法只是将控件居中放置在窗口的客户区内,而不调整控件大小或改变控件的相对位置。我并不声称这些算法可以满足所有期望的布局行为。相反,我发现代码在需要管理嵌入在容器 CWnd
中的对话框(如演示应用程序所示)或嵌入在 ActiveX 控件中的对话框的布局时对我自己更有用。虽然辅助类可以以几乎自动的方式使用,但要实现复杂的布局行为,类用户需要熟悉以像素值为单位定义偏移量、锚点和位置。虽然辅助类确实处理了计算和实际的控件移动和调整大小,但您需要提供足够的信息,说明所需的布局行为。
假设您有一个子控件,其原始位置和大小(我称之为参考矩形)由变量表示:x
、y
、width
和 height
。默认布局算法根据对话框的当前大小相对于对话框的原始大小(或我称之为参考大小)计算一个比例因子。然后将此比例因子应用于每个受管理子控件的位置和大小。
newX = (int)(x * scaleX); newY = (int)(y * scaleY); newWidth = (int)(width * scaleX); newHeight = (int)(height * scaleY);
虽然这种默认缩放可能适用于对话框中的某些控件,但对于其他控件则不适用。这就是布局选项(约束)发挥作用的地方。下面是 CLayoutInfo
类的公共接口。这是一个简单的数据类,可用于按控件为单位指定可选的布局约束。布局选项通常在控件首次添加到布局助手时设置。但是,这些选项也可以在初始添加后随时动态调整。
class CLayoutInfo { public: // Layout option types. enum { // Sizing constraints. OT_MIN_WIDTH = 0, OT_MAX_WIDTH = 1, OT_MIN_HEIGHT = 2, OT_MAX_HEIGHT = 3, OT_ASPECT_RATIO = 4, // Positioning constraints. OT_MIN_LEFT = 5, OT_MAX_LEFT = 6, OT_MIN_TOP = 7, OT_MAX_TOP = 8, // Constraints for anchoring to the sides // of the attach wnd (e.g., dialog). OT_LEFT_OFFSET = 9, OT_TOP_OFFSET = 10, OT_RIGHT_OFFSET = 11, OT_BOTTOM_OFFSET = 12, // Options to override anchoring to a side of // the attach wnd. Anchor instead to a moveable // point within the attach wnd. These options // only take effect if the corresponding // OT_xxx_OFFSET option was chosen. OT_LEFT_ANCHOR = 13, OT_TOP_ANCHOR = 14, OT_RIGHT_ANCHOR = 15, OT_BOTTOM_ANCHOR = 16, // Center the control based on X/Y anchor points. OT_CENTER_XPOS = 17, OT_CENTER_YPOS = 18, OT_OPTION_COUNT = 19 }; // Constructor / destructor. CLayoutInfo(); ~CLayoutInfo(); // Number of decimal places for interpreting an // integer option value as a floating point value. void SetPrecision(int precision); int GetPrecision() const; // Manage option values. The Add method can be // used to update an existing option value. bool AddOption(int option, int value); bool RemoveOption(int option); bool HasOption(int option) const; bool GetOption(int option, int& value) const; // Set/get the reference rect for this control. void SetReferenceRect(const CRect& rect); const CRect& GetReferenceRect() const; // Clear options and reset precision and reference rect. void Reset(); };
下表仅供参考,描述了支持的布局选项
选项类型 | 描述 |
OT_MIN_WIDTH |
指定控件的最小宽度(以像素为单位)。 |
OT_MAX_WIDTH |
指定控件的最大宽度(以像素为单位)。要防止控件调整大小,请将此值设置为等于最小宽度值。 |
OT_MIN_HEIGHT |
指定控件的最小高度(以像素为单位)。 |
OT_MAX_HEIGHT |
指定控件的最大高度(以像素为单位)。要防止控件调整大小,请将此值设置为等于最小高度值。 |
OT_ASPECT_RATIO |
以整数值指定控件的固定纵横比。使用默认精度,此值应等于:纵横比 * 1000。 |
OT_MIN_LEFT |
指定控件左上角最小的 x 坐标。 |
OT_MAX_LEFT |
指定控件左上角最大的 x 坐标。要防止控件被移动,请将此值设置为等于最小 x 坐标值。 |
OT_MIN_TOP |
指定控件左上角最小的 y 坐标。 |
OT_MAX_TOP |
指定控件左上角最大的 y 坐标。要防止控件被移动,请将此值设置为等于最小 y 坐标值。 |
OT_LEFT_OFFSET |
将控件的左侧偏移到窗口的左侧,并指定一个偏移量(以像素为单位)。 |
OT_TOP_OFFSET |
将控件的顶部偏移到窗口的顶部,并指定一个偏移量(以像素为单位)。 |
OT_RIGHT_OFFSET |
将控件的右侧偏移到窗口的右侧,并指定一个偏移量(以像素为单位)。 |
OT_BOTTOM_OFFSET |
将控件的底部偏移到窗口的底部,并指定一个偏移量(以像素为单位)。 |
OT_LEFT_ANCHOR |
如果选择了 OT_LEFT_OFFSET ,此选项指定一个 x 坐标进行锚定,而不是锚定到窗口的左侧。此 x 坐标相对于窗口的原始参考矩形。因此,此 x 坐标(锚点)在窗口调整大小时会移动。 |
OT_TOP_ANCHOR |
如果选择了 OT_TOP_OFFSET ,此选项指定一个 y 坐标进行锚定,而不是锚定到窗口的顶部。 |
OT_RIGHT_ANCHOR |
如果选择了 OT_RIGHT_OFFSET ,此选项指定一个 x 坐标进行锚定,而不是锚定到窗口的右侧。 |
OT_BOTTOM_ANCHOR |
如果选择了 OT_BOTTOM_OFFSET ,此选项指定一个 y 坐标进行锚定,而不是锚定到窗口的底部。 |
OT_CENTER_XPOS |
将控件围绕给定的 x 坐标居中。此 x 坐标相对于窗口的原始参考矩形。因此,此 x 坐标(中心点)在窗口调整大小时会移动。 |
OT_CENTER_YPOS |
将控件围绕给定的 y 坐标居中。 |
使用 CLayoutHelper
CLayoutHelper
类实现在两个源文件中:LayoutHelper.h 和 LayoutHelper.cpp。下面为供参考的类的公共接口。
class CLayoutHelper { public: // Layout styles (algorithms). enum { DEFAULT_LAYOUT = 0, CENTERED_LAYOUT = 1 }; // Constructor / destructor. CLayoutHelper(); ~CLayoutHelper(); // Attach/detach a CWnd or CDialog. This is the window // containing the child controls to be repositioned/resized. void AttachWnd(CWnd* pWnd); void DetachWnd(); // Select the layout style (algorithm). void SetLayoutStyle(int layoutStyle); int GetLayoutStyle() const; // Set/get the reference size of the CWnd or CDialog. // This is the virtual size of the client area of the // CWnd or CDialog to be used in all layout calculations. void SetReferenceSize(int width, int height); const CSize& GetReferenceSize() const; // Child control management. The Add methods can be used // to add a new control or update an existing one. bool AddControl(CWnd* pControl); bool AddControl(CWnd* pControl, const CLayoutInfo& info); bool AddChildControls(); bool RemoveControl(CWnd* pControl); bool GetLayoutInfo(CWnd* pControl, CLayoutInfo& info) const; // Optional: This is a threshold size for the client // area of the CWnd or CDialog. Below this size, layout // management will be turned off (so you can turn on // scrolling if you like instead). void SetMinimumSize(int width, int height); const CSize& GetMinimumSize() const; // Optional: Set the step size in order to have the layout // function invoked only at fixed size increments of the // dialog size. This can help to improve resizing performance // by not applying layouts on every OnSize() call. A typical // value for the step size might be 5 or 10 pixels. void SetStepSize(int stepSize); int GetStepSize() const; // Message handling. void OnSize(UINT nType, int cx, int cy); // Perform layout of controls. void LayoutControls(); };
要在派生自 CDialog
的类(例如 CMyDlg
)中添加布局管理,我们首先在对话框的类定义(头文件)中添加一个私有成员:
class CLayoutHelper; // Forward class declaration. class CMyDlg : public CDialog { ... private: CLayoutHelper* m_layoutHelper; };
接下来,我们在类定义(头文件)中重写 OnInitDialog()
虚拟方法并为 OnSize()
添加消息处理程序:
protected: // ClassWizard generated virtual function overrides. //{{AFX_VIRTUAL(CMyDlg) ... virtual BOOL OnInitDialog(); //}}AFX_VIRTUAL // Generated message map functions. //{{AFX_MSG(CMyDlg) ... afx_msg void OnSize(UINT nType, int cx, int cy); //}}AFX_MSG DECLARE_MESSAGE_MAP()
在对话框源文件中,包含 LayoutHelper.h 文件,并为 OnSize()
消息处理程序添加消息映射条目:
#include "LayoutHelper.h" ... BEGIN_MESSAGE_MAP(CMyDlg, CDialog) //{{AFX_MSG_MAP(CMyDlg) ... ON_WM_SIZE() //}}AFX_MSG_MAP END_MESSAGE_MAP()
然后,在对话框构造函数中创建辅助类的实例,并将对话框附加到实例:
CMyDlg::CMyDlg(CWnd* pParent) : CDialog(IDD_MY_DLG, pParent) { // Create the layout helper and attach it to this dialog. m_layoutHelper = new CLayoutHelper; m_layoutHelper->AttachWnd(this); ... }
接下来,我们需要在辅助类中设置“参考大小”。这代表了 CWnd
或 CDialog
的客户区的虚拟大小,用于所有布局计算。通常,参考大小设置为窗口在像素中的客户区的原始或初始大小。
CMyDlg::CMyDlg(CWnd* pParent) : CDialog(IDD_MY_DLG, pParent) { ... // Set the reference size equal to the original // dialog size in pixels. m_layoutHelper->SetReferenceSize(500, 300); }
您还可以选择设置窗口的最小大小。通过设置此选项,它告诉辅助类在窗口小于最小大小时停止布局管理(尽管仍可能强制遵守布局约束)。如我之前所暗示的,此选项可与滚动行为结合使用。典型用法是将最小大小设置为等于参考大小。
CMyDlg::CMyDlg(CWnd* pParent) : CDialog(IDD_MY_DLG, pParent) { ... // Set the minimum size equal to the // reference size. m_layoutHelper->SetMinimumSize(500, 300); }
接下来,选择所需的布局算法:
CMyDlg::CMyDlg(CWnd* pParent)
: CDialog(IDD_MY_DLG, pParent)
{
...
// Select the layout algorithm/style.
m_layoutHelper->SetLayoutStyle(
CLayoutHelper::DEFAULT_LAYOUT);
}
在对话框析构函数中,删除布局助手实例:
CMyDlg::~CMyDlg()
{
delete m_layoutHelper;
}
通过简单地委托给辅助类来实现 OnSize()
消息处理程序:
void CMyDlg::OnSize(UINT nType, int cx, int cy) { CDialog::OnSize(nType, cx, cy); m_layoutHelper->OnSize(nType, cx, cy); }
此时,布局助手几乎准备就绪。我们只需添加要管理的控件及其布局选项。所有这些都可以在 OnInitDialog()
方法中完成:
BOOL CMyDlg::OnInitDialog() { CDialog::OnInitDialog(); // Add all child controls to the layout helper // with default (empty) options. m_layoutHelper->AddChildControls(); // Update layout options for specific controls // in order to override the default layout // behavior. // Restrict edit box to a fixed size: 40 x 23 pixels. CLayoutInfo info; info.AddOption(CLayoutInfo::OT_MIN_WIDTH, 40); info.AddOption(CLayoutInfo::OT_MAX_WIDTH, 40); info.AddOption(CLayoutInfo::OT_MIN_HEIGHT, 23); info.AddOption(CLayoutInfo::OT_MAX_HEIGHT, 23); m_layoutHelper->AddControl(GetDlgItem(IDC_MY_EDIT), info); // Restrict the top-left corner of the // groupbox to (10,10). // Anchor the right side of groupbox to x = 354. // Anchor the bottom of groupbox to the bottom of // the dialog with an offset of 10 pixels. info.Reset(); info.AddOption(CLayoutInfo::OT_MIN_LEFT, 10); info.AddOption(CLayoutInfo::OT_MAX_LEFT, 10); info.AddOption(CLayoutInfo::OT_MIN_TOP, 10); info.AddOption(CLayoutInfo::OT_MAX_TOP, 10); info.AddOption(CLayoutInfo::OT_RIGHT_OFFSET, 10); info.AddOption(CLayoutInfo::OT_RIGHT_ANCHOR, 354); info.AddOption(CLayoutInfo::OT_BOTTOM_OFFSET, 10); m_layoutHelper->AddControl(m_groupBox, info); return TRUE; // return TRUE unless you set // the focus to a control // EXCEPTION: OCX Property Pages // should return FALSE };
TestLayout 应用程序
演示项目 (TestLayout) 说明了辅助类的使用。这是一个 MDI 应用程序,我使用 Visual Studio 从头开始创建的。为了生成项目,我默认了所有 VS 向导选项,除了“文档/视图支持”复选框,我将其取消选中。然后我编写了两个新类,CTestDefaultDlg
和 CTestCenteredDlg
。两者都是创建了 CLayoutHelper
实例并使用它来管理对话框上控件的对话框。
生成的 MDI 应用程序提供了一个名为 CChildView
的类,该类包含在 MDI 子框架窗口内。CChildView
是与我上面两个新类集成的起点。我修改了 CChildView
,使其不提供自己的内容,而是创建 CTestDefaultDlg
或 CTestCenteredDlg
实例来覆盖其整个客户区。
int CChildView::OnCreate(LPCREATESTRUCT lpCreateStruct) { if ( CWnd::OnCreate(lpCreateStruct) == -1 ) return -1; // We either create a CTestDefaultDlg or a // CTestCenteredDlg. We alternate using a counter. static int counter = 0; if ( counter % 2 == 0 ) m_testWin = new CTestDefaultDlg(this); else m_testWin = new CTestCenteredDlg(this); ++counter; return 0; }
要测试演示应用程序,只需使用“文件 | 新建”菜单项打开 MDI 子窗口。第一次将获得 CTestDefaultDlg
实例。第二次将创建一个 CTestCenteredDlg
。每次选择“新建”时,它都会在这两种示例之间交替。
下面的快照显示了默认布局算法的运行情况。为了最小化闪烁,我提供了自己的自定义 Groupbox
控件,而不是使用 MFC 版本(它实际上是一个带有 BS_GROUPBOX
样式的 CButton
)。这只是一个示例,表明支持动态创建的控件。“步长”选项允许您指定一个像素阈值,用于决定是否执行布局。例如,如果将此值设置为 5 像素,则只有当对话框的大小调整至少增大或减小 5 像素时,布局助手才会执行布局。这有助于避免在每次 OnSize()
调用时都调整子控件大小,并可以提高调整大小的性能,但可能会引入“锯齿状”大小调整效果。
下面的快照显示了 CTestCenteredDlg
类使用的居中布局算法。请注意,此特定算法不支持布局约束。
结论
由于布局管理代码只涉及两个类(例如,辅助类和选项数据类),因此可以扩展选项集或添加新的布局算法。每个布局算法都封装在 CLayoutHelper
类中的单个方法调用(私有成员函数)中。我通常执行的基本测试是,将对话框调整得足够小,直到出现滚动条。然后,滚动到对话框的右下角。最后,抓住窗口的右下角并将其放大,直到滚动条消失并生效布局逻辑。
历史
- 2005 年 8 月 14 日
- 初始版本。
- 2005 年 8 月 15 日
- 修复了 VS 2003 下的一些警告,并纠正了
GetLayoutInfo()
中的编译问题 - 使用const_iterator
代替。 - 向
CScrollHelper
和CLayoutHelper
添加了GetClientRectSB()
辅助函数。
- 修复了 VS 2003 下的一些警告,并纠正了
- 2005 年 9 月 8 日
- 根据与 Lars 和 zarchaoz 的讨论,增加了对大小约束的重新检查。