Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Control Positioning and Sizing using a C++ Helper Class

0.00/5 (No votes)
20 Sep 2005 1  
Add layout management of controls to a CWnd or CDialog using a C++ helper class.

Introduction

In this article, I present a C++ helper class (CLayoutHelper) that can be used to manage child control positioning and sizing in a CWnd or CDialog. With this approach, no change in your inheritance hierarchy is required as there is no dialog base class that you need to derive from. Typical usage involves creating an instance of the helper class in your CWnd or CDialog, choosing a layout algorithm (style), and then adding child controls to be managed by the helper class. You can call a single method to add all immediate children of your window, or add just the subset of controls that you wish to be managed. You can specify layout options to control the layout behavior on a per-control basis, and update these options dynamically as well. Some interesting effects can be achieved using the supported layout options, such as centering a control around a location in the dialog, anchoring to a specific point, or forcing a control to maintain a fixed aspect ratio (which might be useful for a control displaying a photo or video).

Scrolling

In some ways, scrolling can be thought of as the opposite of layout management. As you resize a dialog or window smaller and smaller, eventually you get to a point where continuing to adjust the position or size of child controls is undesirable. Controls may start to overlap or become too small to be usable. A simple solution is to stop layout management at some minimum window size. Often, a more useful solution is to invoke scrollbars once that minimum size is reached, so that the user can still access the entire dialog or window surface. In a previous article, I presented a similar helper class for adding scrolling support to a CWnd or CDialog. The attached demo application illustrates how both the helper classes (CScrollHelper and CLayoutHelper) can be used to achieve the desired minimum size behavior. The two classes are completely independent but can be easily integrated into the same dialog or window.

Control positioning and sizing

To enable resizing (and scrolling) for a dialog, I typically make sure the following properties are set first using the Visual Studio resource editor:

  1. Border = "Resizing" if you have a popup dialog. If your dialog is a child window embedded within a container parent, you can choose another style such as "None" (the demo project has an example of this).
  2. Clip Children = "True". This setting can help to minimize display flickering as the dialog is being resized.
  3. Horizontal Scrollbar = "True". This is equivalent to adding the window style, WS_HSCROLL.
  4. Style = "Child" if your dialog is a child window embedded within a container parent.
  5. Vertical Scrollbar = "True". This is equivalent to adding the window style, WS_VSCROLL.
  6. Visible = "True". Visual Studio defaults to "False" in some cases, so you want to check this setting.

When a dialog or window is resized, WM_SIZE messages are generated. To handle these messages, I add an ON_WM_SIZE() entry to my window's message map:

BEGIN_MESSAGE_MAP(CMyDlg, CDialog)
    //{{AFX_MSG_MAP(CMyDlg)

    ...
    ON_WM_SIZE()
    //}}AFX_MSG_MAP

END_MESSAGE_MAP()

Then, control repositioning and resizing can be performed in the OnSize() handler as needed:

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.

    ...
}

A single child control can be repositioned and resized at the same time using SetWindowPos(). I usually call it like this:

UINT flags = SWP_NOZORDER | SWP_NOACTIVATE;
::SetWindowPos(pControl->m_hWnd, 0, newX, newY, 
                       newWidth, newHeight, flags);

This is really the crux of layout management. The job of a layout manager is to perform the calculation of parameters such as newX and newWidth based on inputs about the desired layout behavior, and then actually move and resize the child control using SetWindowPos().

Layout algorithms and options

The CLayoutHelper implementation provides two layout algorithms. The default algorithm is a proportional resizer that supports a number of layout constraints for refining positioning and sizing behavior on a per-control basis. The second algorithm simply centers controls within your window's client area without resizing controls or changing relative control positioning. I don't claim that these algorithms can handle every kind of desired layout behavior. Rather, I've found the code more useful myself in cases where I need to manage the layout of a dialog that is embedded within a container CWnd (as shown in the demo application), or embedded within an ActiveX control. Although the helper class can be used in a mostly automatic way, to achieve complex layout behavior the class user needs to be comfortable with defining offsets, anchor points, and positions in pixel values. While the helper class does handle the calculations and actual control movement and resizing, you need to provide it with enough information regarding the desired layout behavior.

Suppose you have a child control with original position and size (what I call the reference rectangle) represented by the variables: x, y, width, and height. The default layout algorithm computes a scale factor based on the current size of the dialog (or window) relative to the original size (or what I call the reference size) of the dialog. This scale factor is then applied to the position and size of each managed child control:

newX      = (int)(x * scaleX);
newY      = (int)(y * scaleY);
newWidth  = (int)(width  * scaleX);
newHeight = (int)(height * scaleY);

While this default scaling might be appropriate for some controls in a dialog, it won't do for others. This is where layout options (constraints) come into play. Below is the public interface for the CLayoutInfo class. This is a simple data class that can be used to specify optional layout constraints on a per-control basis. Layout options are typically set when the control is first added to the layout helper. However, these options can also be dynamically adjusted anytime after the initial add.

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();
};

The table below is for reference purposes and describes the supported layout options:

Option Type Description
OT_MIN_WIDTH Specify a minimum width for the control in pixels.
OT_MAX_WIDTH Specify a maximum width for the control in pixels. To prevent a control from resizing, set this value equal to the minimum width value.
OT_MIN_HEIGHT Specify a minimum height for the control in pixels.
OT_MAX_HEIGHT Specify a maximum height for the control in pixels. To prevent a control from resizing, set this value equal to the minimum height value.
OT_ASPECT_RATIO Specify a fixed aspect ratio for the control as an integer value. Using the default precision, this value should be equal to: aspect_ratio * 1000.
OT_MIN_LEFT Specify the minimum x-position of the top-left corner of the control.
OT_MAX_LEFT Specify the maximum x-position of the top-left corner of the control. To prevent a control from being moved, set this value equal to the minimum x-position value.
OT_MIN_TOP Specify the minimum y-position of the top-left corner of the control.
OT_MAX_TOP Specify the maximum y-position of the top-left corner of the control. To prevent a control from being moved, set this value equal to the minimum y-position value.
OT_LEFT_OFFSET Anchor the left side of the control to the left side of the window with a specified offset in pixels.
OT_TOP_OFFSET Anchor the top of the control to the top of the window with a specified offset in pixels.
OT_RIGHT_OFFSET Anchor the right side of the control to the right side of the window with a specified offset in pixels.
OT_BOTTOM_OFFSET Anchor the bottom of the control to the bottom of the window with a specified offset in pixels.
OT_LEFT_ANCHOR If OT_LEFT_OFFSET was chosen, this option specifies an x-position to anchor to instead of anchoring to the left side of the window. This x-position is relative to the original reference rectangle for the window. Hence, this x-position (anchor point) moves as the window is being resized.
OT_TOP_ANCHOR If OT_TOP_OFFSET was chosen, this option specifies a y-position to anchor to instead of anchoring to the top of the window.
OT_RIGHT_ANCHOR If OT_RIGHT_OFFSET was chosen, this option specifies an x-position to anchor to instead of anchoring to the right side of the window.
OT_BOTTOM_ANCHOR If OT_BOTTOM_OFFSET was chosen, this option specifies a y-position to anchor to instead of anchoring to the bottom of the window.
OT_CENTER_XPOS Center the control about a given x-position. This x-position is relative to the original reference rectangle for the window. Hence, this x-position (center point) moves as the window is being resized.
OT_CENTER_YPOS Center the control about a given y-position.

Using CLayoutHelper

The CLayoutHelper class is implemented in two source files: LayoutHelper.h and LayoutHelper.cpp. The public interface of the class is shown below for reference:

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();
};

To add layout management to a CDialog-derived class (such as CMyDlg), we begin by adding a private member to the dialog's class definition (include file):

class CLayoutHelper;    // Forward class declaration.


class CMyDlg : public CDialog
{
...
    
private:
    CLayoutHelper* m_layoutHelper;
};

Next, we override the OnInitDialog() virtual method and add an OnSize() message handler to the class definition (include file):

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()

In the dialog source file, include the LayoutHelper.h file and also add the message map entry for the OnSize() message handler:

#include "LayoutHelper.h"

...
    
BEGIN_MESSAGE_MAP(CMyDlg, CDialog)
    //{{AFX_MSG_MAP(CMyDlg)

    ...
    ON_WM_SIZE()
    //}}AFX_MSG_MAP

END_MESSAGE_MAP()

Then, create an instance of the helper class in the dialog constructor and attach the dialog to the instance:

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

    ...
}

Next, we need to set the "reference size" in the helper class. This represents the virtual size of the client area of your CWnd or CDialog to be used in all layout calculations. Typically, the reference size is set to the original or initial size of the client area of your window in pixels:

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);
}

Optionally, you can set a minimum size for the window as well. By setting this option, it tells the helper class to stop layout management when the window becomes smaller than the minimum size (although adherence to layout constraints may still be enforced). As I hinted earlier, this option can be used in conjunction with scrolling behavior. A typical usage is to set the minimum size equal to the reference size:

CMyDlg::CMyDlg(CWnd* pParent)
    : CDialog(IDD_MY_DLG, pParent)
{
    ...

    // Set the minimum size equal to the

    // reference size.

    m_layoutHelper->SetMinimumSize(500, 300);
}

Next, select the desired layout algorithm:

CMyDlg::CMyDlg(CWnd* pParent)
    : CDialog(IDD_MY_DLG, pParent)
{
    ...

    // Select the layout algorithm/style.

    m_layoutHelper->SetLayoutStyle(
               CLayoutHelper::DEFAULT_LAYOUT);
}

In the dialog destructor, delete the layout helper instance:

CMyDlg::~CMyDlg()
{
    delete m_layoutHelper;
}

Implement the OnSize() message handler by simply delegating to the helper class:

void CMyDlg::OnSize(UINT nType, int cx, int cy)
{
    CDialog::OnSize(nType, cx, cy);

    m_layoutHelper->OnSize(nType, cx, cy);
}

At this point, the layout helper is almost ready to go. We just need to add the controls to be managed, along with their layout options. This can all be done in the OnInitDialog() method:

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

};

The TestLayout application

The demo project (TestLayout) illustrates the use of the helper class. It's a MDI application that I created from scratch using Visual Studio. To generate the project, I defaulted all of the VS wizard options except for the Document/View support checkbox, which I unchecked. I then wrote two new classes, CTestDefaultDlg and CTestCenteredDlg. Both are dialogs that create their own instance of CLayoutHelper and use it to manage the controls on the dialog.

The generated MDI application provides a class called CChildView, which is contained within the MDI child frame window. CChildView is the starting point of integration with my two new classes above. Instead of CChildView providing its own content, I modified it to either create a CTestDefaultDlg or a CTestCenteredDlg instance that covers its entire client area.

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;
}

To test the demo application, just use the File | New menu item to open MDI child windows. The first time you will get a CTestDefaultDlg instance. The second time, a CTestCenteredDlg will be created. Each time you select "New", it will alternate between the two types of examples.

The snapshot below shows the default layout algorithm in action. To minimize flickering, I've provided my own custom Groupbox control instead of using the MFC version (which is actually a CButton with style BS_GROUPBOX). This is just an example to show that dynamically created controls are supported. The "step size" option allows you to specify a threshold in pixels for deciding whether to perform a layout or not. For example, if you set this value to 5 pixels, then the layout helper will only perform a layout if the dialog is resized at least 5 pixels larger or smaller. This helps to avoid resizing child controls on every single OnSize() call and can improve resizing performance at the expense of introducing a "notched" sizing effect.

The snapshot below shows the centered layout algorithm as used by the CTestCenteredDlg class. Note that this particular algorithm does not support layout constraints.

Conclusion

Since there are only two classes involved in the layout management code (e.g., the helper class and options data class), it should be possible to extend the set of options or add new layout algorithms. Each layout algorithm is encapsulated by a single method call (private member function) in the CLayoutHelper class. A basic test I usually perform to check my own changes is to resize the dialog small enough so that scrollbars appear. Then, scroll all the way to the bottom-right of the dialog. Finally, grip the bottom-right corner of the window and resize it larger and larger until the scrollbars disappear and the layout logic takes effect.

History

  • August 14th, 2005
    • Initial revision.
  • August 15th, 2005
    • Fixed some warnings under VS 2003 and corrected compile problem in GetLayoutInfo() - use const_iterator instead.
    • Added GetClientRectSB() helper function to CScrollHelper and CLayoutHelper.
  • September 8th, 2005
    • Added re-check of sizing constraints as per discussion with Lars and zarchaoz.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here