65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 C++ 帮助类进行控件定位和调整大小

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (41投票s)

2005 年 8 月 14 日

CPOL

11分钟阅读

viewsIcon

210144

downloadIcon

4071

为 CWnd 或 CDialog 添加控件的布局管理,使用 C++ 辅助类。

引言

在本文中,我介绍了一个 C++ 辅助类 (CLayoutHelper),可用于管理 CWndCDialog 中子控件的定位和大小。通过这种方法,无需更改继承层次结构,因为不需要从任何对话框基类派生。典型用法包括在 CWndCDialog 中创建辅助类实例,选择布局算法(样式),然后添加要由辅助类管理的子控件。您可以调用一个方法来添加窗口的所有直接子控件,或仅添加您希望管理的控件子集。您可以指定布局选项以按控件为单位控制布局行为,还可以动态更新这些选项。使用支持的布局选项可以实现一些有趣的效果,例如将控件置于对话框的某个位置居中、固定到特定点,或强制控件保持固定的纵横比(这对于显示照片或视频的控件可能很有用)。

滚动

在某些方面,滚动可以被视为布局管理的对立面。当您将对话框或窗口的尺寸越缩越小时,最终会达到一个点,此时继续调整子控件的位置或大小是不受欢迎的。控件可能会开始重叠,或者变得太小而无法使用。一个简单的解决方案是在达到某个最小窗口大小时停止布局管理。通常,一个更有用的解决方案是在达到最小尺寸时调用滚动条,以便用户仍然可以访问整个对话框或窗口表面。在我之前的 文章中,我介绍了一个类似的辅助类,用于为 CWndCDialog 添加滚动支持。附带的演示应用程序说明了如何使用这两个辅助类 (CScrollHelperCLayoutHelper) 来实现所需的最小尺寸行为。这两个类是完全独立的,但可以轻松地集成到同一个对话框或窗口中。

控件定位和大小调整

要启用对话框的调整大小(和滚动)功能,我通常会使用 Visual Studio 资源编辑器首先确保以下属性已设置:

  1. 边框 = “可调整大小”(如果您使用的是弹出式对话框)。如果您的对话框是嵌入在容器父窗口中的子窗口,您可以选择其他样式,例如“无”(演示项目中有此示例)。
  2. 剪裁子控件 = “真”。此设置有助于最小化对话框调整大小时的显示闪烁。
  3. 水平滚动条 = “真”。这等同于添加窗口样式 WS_HSCROLL
  4. 样式 = “子窗口”(如果您的对话框是嵌入在容器父窗口中的子窗口)。
  5. 垂直滚动条 = “真”。这等同于添加窗口样式 WS_VSCROLL
  6. 可见 = “真”。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);

这实际上是布局管理的核心。布局管理器的工作是基于关于所需布局行为的输入,执行 newXnewWidth 等参数的计算,然后使用 SetWindowPos() 实际移动和调整子控件的大小。

布局算法和选项

CLayoutHelper 实现提供了两种布局算法。默认算法是一个比例调整器,支持多种布局约束,用于按控件为单位精细调整定位和大小行为。第二种算法只是将控件居中放置在窗口的客户区内,而不调整控件大小或改变控件的相对位置。我并不声称这些算法可以满足所有期望的布局行为。相反,我发现代码在需要管理嵌入在容器 CWnd 中的对话框(如演示应用程序所示)或嵌入在 ActiveX 控件中的对话框的布局时对我自己更有用。虽然辅助类可以以几乎自动的方式使用,但要实现复杂的布局行为,类用户需要熟悉以像素值为单位定义偏移量、锚点和位置。虽然辅助类确实处理了计算和实际的控件移动和调整大小,但您需要提供足够的信息,说明所需的布局行为。

假设您有一个子控件,其原始位置和大小(我称之为参考矩形)由变量表示:xywidthheight。默认布局算法根据对话框的当前大小相对于对话框的原始大小(或我称之为参考大小)计算一个比例因子。然后将此比例因子应用于每个受管理子控件的位置和大小。

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.hLayoutHelper.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);

    ...
}

接下来,我们需要在辅助类中设置“参考大小”。这代表了 CWndCDialog 的客户区的虚拟大小,用于所有布局计算。通常,参考大小设置为窗口在像素中的客户区的原始或初始大小。

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 向导选项,除了“文档/视图支持”复选框,我将其取消选中。然后我编写了两个新类,CTestDefaultDlgCTestCenteredDlg。两者都是创建了 CLayoutHelper 实例并使用它来管理对话框上控件的对话框。

生成的 MDI 应用程序提供了一个名为 CChildView 的类,该类包含在 MDI 子框架窗口内。CChildView 是与我上面两个新类集成的起点。我修改了 CChildView,使其不提供自己的内容,而是创建 CTestDefaultDlgCTestCenteredDlg 实例来覆盖其整个客户区。

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 代替。
    • CScrollHelperCLayoutHelper 添加了 GetClientRectSB() 辅助函数。
  • 2005 年 9 月 8 日
    • 根据与 Lars 和 zarchaoz 的讨论,增加了对大小约束的重新检查。
© . All rights reserved.