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:
- 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).
- Clip Children = "True". This setting can help to minimize display flickering as the dialog is being resized.
- Horizontal Scrollbar = "True". This is equivalent to adding the window style,
WS_HSCROLL
.
- Style = "Child" if your dialog is a child window embedded within a container parent.
- Vertical Scrollbar = "True". This is equivalent to adding the window style,
WS_VSCROLL
.
- 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)
...
ON_WM_SIZE()
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);
...
}
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:
enum
{
OT_MIN_WIDTH = 0,
OT_MAX_WIDTH = 1,
OT_MIN_HEIGHT = 2,
OT_MAX_HEIGHT = 3,
OT_ASPECT_RATIO = 4,
OT_MIN_LEFT = 5,
OT_MAX_LEFT = 6,
OT_MIN_TOP = 7,
OT_MAX_TOP = 8,
OT_LEFT_OFFSET = 9,
OT_TOP_OFFSET = 10,
OT_RIGHT_OFFSET = 11,
OT_BOTTOM_OFFSET = 12,
OT_LEFT_ANCHOR = 13,
OT_TOP_ANCHOR = 14,
OT_RIGHT_ANCHOR = 15,
OT_BOTTOM_ANCHOR = 16,
OT_CENTER_XPOS = 17,
OT_CENTER_YPOS = 18,
OT_OPTION_COUNT = 19
};
CLayoutInfo();
~CLayoutInfo();
void SetPrecision(int precision);
int GetPrecision() const;
bool AddOption(int option, int value);
bool RemoveOption(int option);
bool HasOption(int option) const;
bool GetOption(int option, int& value) const;
void SetReferenceRect(const CRect& rect);
const CRect& GetReferenceRect() const;
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:
enum
{
DEFAULT_LAYOUT = 0,
CENTERED_LAYOUT = 1
};
CLayoutHelper();
~CLayoutHelper();
void AttachWnd(CWnd* pWnd);
void DetachWnd();
void SetLayoutStyle(int layoutStyle);
int GetLayoutStyle() const;
void SetReferenceSize(int width, int height);
const CSize& GetReferenceSize() const;
bool AddControl(CWnd* pControl);
bool AddControl(CWnd* pControl, const CLayoutInfo& info);
bool AddChildControls();
bool RemoveControl(CWnd* pControl);
bool GetLayoutInfo(CWnd* pControl, CLayoutInfo& info) const;
void SetMinimumSize(int width, int height);
const CSize& GetMinimumSize() const;
void SetStepSize(int stepSize);
int GetStepSize() const;
void OnSize(UINT nType, int cx, int cy);
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;
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:
...
virtual BOOL OnInitDialog();
...
afx_msg void OnSize(UINT nType, int cx, int cy);
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)
...
ON_WM_SIZE()
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)
{
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)
{
...
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)
{
...
m_layoutHelper->SetMinimumSize(500, 300);
}
Next, select the desired layout algorithm:
CMyDlg::CMyDlg(CWnd* pParent)
: CDialog(IDD_MY_DLG, pParent)
{
...
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();
m_layoutHelper->AddChildControls();
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);
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;
};
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;
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
- 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.