Introduction

In MFC, resizing or repositioning controls could be quite bothersome. If you are familiar with the .NET platform, things are much more simplified with the use of the Anchor and Dock properties of the Control class and design-time support for adding child controls to container controls. I tried to mimic some of these features of .NET, but the C++ way.

Background

There are other solutions available online (also on CodeProject) for this purpose. I think my solution stands out on its design, simplicity, and feature richness.

This solution allows you to do the following:

  • Anchor a dialog item using the Anchor property.
  • Create panels which contain other panels or UI controls.
  • Restrict the size of UI controls or a panel by using minimum size and maximum size properties.
  • Restrict the size of the dialog by using minimum size and maximum size properties.
  • Create a horizontal or vertical split container.
  • Freeze the splitter of split container so that users cannot use mouse to resize the panels.
  • Set a panel to be fixed of a split container so that when the parent resizes only one of the non-fixed panel resizes.
  • Set the parent of a UI control or a panel (not related to Win32 SetParent API).
  • Show the resize grip on the lower right corner of the dialog using Visual Style (if the application supports it).

Let's take a brief look at how .NET does its resizing of child controls:

  • Anchor - It allows a child control to be anchored to the left, top, right, or bottom edge or any combination of these four options.
  • Dock - It allows a child control to be docked to the left, top, right, and bottom edges.
  • Visual Studio Designer Support - If you place a frame control on a Windows Form and then drag a button control on top of the frame, the button becomes the actual child of the frame and grandchild of the Windows Form. But in the resource editor of MFC, if you place the frame on the dialog template and then drag a button on top of the frame control, the button is actually an immediate child of the dialog template and not the frame. This means if you move the frame, the button will not move.
  • SplitContainer - Creating splitter windows has never been this simple since the invention of this control. It has two panels which can host other controls inside.

So in .NET, all controls are children or grandchildren of the WindowForm; this creates a hierarchical structure of controls. When a parent is resized or repositioned, all its children are resized or repositioned according to their Anchor or Dock property settings.

In my solution, I have implemented the Anchor, Panel, and SplitContainer concept. But I have not implemented the "Dock" feature yet (may be in the future).

There are several classes in this solution, but CWndResizer is the only class that a developer will work with.

Typically, you will design your dialog template in the Visual Studio Resource Editor, and then in the dialog class implementation, you will have a member variable like this:

private:
  CWndResizer m_resizer;

The samples included with this article uses CDialog class to demonstrate many features of this class. But this class can be used with any class derived from CWnd (CDialog, CPropertyPage, CPropertySheet, CFrmWnd, CFormView)

Before this class can do anything, you must call the Hook method like this:

BOOL CExample1Dlg::OnInitDialog()
{
  CDialog::OnInitDialog();
  BOOL bOk = m_resizer.Hook(this);
  ASSERT(bOk == TRUE);
}

In this article, I will refer to this window (that is passed to the Hook method) as "hooked-window".

By calling this method, it places a window procedure hook in the WndProc chain.

When you call the Hook method, it stores the client area of the hooked-window in a structure called CPanel. A panel is mainly a rectangle area given in client coordinate of the hooked-window. A panel can have zero or more panels as children. During creation of a panel, you assign a unique name for the panel. The name is used to refer to the panel or find a panel. The client area of the hooked-window is the root of the hierarchy and it is named "_root".

Each panel has Anchor, MinSize, MaxSize properties (along with some other properties). But you cannot directly set or get the properties of a panel; instead, you will use member methods of the CWndResizer class.

The idea is that when a CPanel is resized or repositioned, all its children are also resized and reposition relatively.

Now, let's look at some code snippets.

The following code will anchor the OK and Cancel buttons of your dialog to the bottom-right corner:

BOOL CExample1Dlg::OnInitDialog()
{
  CDialog::OnInitDialog();
  BOOL bOk = m_resizer.Hook(this);
  ASSERT(bOk == TRUE);

  bOk = m_resizer.SetAnchor(IDOK, ANCHOR_RIGHT | ANCHOR_BOTTOM);
  ASSERT(bOk == TRUE);

  bOk = m_resizer.SetAnchor(IDCANCEL, ANCHOR_RIGHT | ANCHOR_BOTTOM);
  ASSERT(bOk == TRUE);
}

If you want to create a panel and set its Anchor property to ANCHOR_HORIZONTALLY, you will do this:

BOOL CExample1Dlg::OnInitDialog()
{
  CDialog::OnInitDialog();
  BOOL bOk = m_resizer.Hook(this);
  ASSERT(bOk == TRUE);
  CRect rc(40, 40, 240, 240);
  bOk = m_resizer.CreatePanel(_T("MyNewPanel"), &rc);

  bOk = m_resizer.SetAnchor(_T("MyNewPanel"), ANCHOR_HORIZONTALLY);
  ASSERT(bOk == TRUE);

  ASSERT(bOk == TRUE);
}

Using the Code

The sample application demonstrates many features of this class.

The CWndResizer class has the following members:

Method
BOOL CreatePanel(LPCTSTR panelName, const CRect * prcPanel);
Description Creates a panel in the client area of the hooked-window.
Parameters panelName An unique name for this panel. This name is used to refer to this panel later on.
prcPanel A rectangle given in hooked-window's client coordinate.
Return values TRUE Succeeded.
FALSE Failed.

CWndResizer::Hook method hasn't been called.

-or-

panelName is not unique or is NULL.

Remarks Initially, the parent of this panel would be the "_root", which is the hooked window. You can change the parent by calling the CWndResizer::SetParent method. You can also set its Anchor property by calling CWndResier::SetAnchor. Initially, the Anchor value is ANCHOR_LEFT | ANCHOR_TOP.
BOOL CExample1Dlg::OnInitDialog()
{
  CDialog::OnInitDialog();
  BOOL bOk = m_resizer.Hook(this);
  ASSERT(bOk == TRUE);
  CRect rc(40, 40, 240, 240);
  bOk = m_resizer.CreatePanel(_T("MyNewPanel"), &rc);
  ASSERT(bOk == TRUE);

  // Subsequently you can use the name to refer to it
  bOk = m_resizer.SetAnchor(_T("MyNewPanel"), ANCHOR_HORIZONTALLY);
  ASSERT(bOk == TRUE);
}
Method
BOOL CreateSplitContainer(LPCTSTR splitContainerName, 
	LPCTSTR panelNameA, LPCTSTR panelNameB);
BOOL CreateSplitContainer(LPCTSTR splitContainerName, 
	LPCTSTR panelNameA, UINT panelIDB);
BOOL CreateSplitContainer(LPCTSTR splitContainerName, 
	UINT panelIDA, LPCTSTR panelNameB);
BOOL CreateSplitContainer(LPCTSTR splitContainerName, 
	UINT panelIDA, UINT panelIDB);
Description Creates a split container. (.NET analogy).
Parameters splitContainerName An unique name for this split container. This name is used to refer to this panel later on.
panelNameA Name of a panel that was created by a prior call to CWndResizer::CreatePanel or CWndResizer::CreateSplitContainer.

If the resulting panel is a horizontal split container, this refers to the left panel of the split container. Otherwise, it is the top panel of the split container.

panelIDA An ID of a child control (window) of the hooked-window.

If the resulting panel is a horizontal split container, this refers to the left panel of the split container. Otherwise, it is the top panel of the split container.

panelNameB Name of a panel that was created by a prior call to CWndResizer::CreatePanel or CWndResizer::CreateSplitContainer.

If the resulting panel is a horizontal split container, this refers to the right panel of the split container. Otherwise, it is the bottom panel of the split container.

panelIDB An ID of a child control (window) of the hooked-window.

If the resulting panel is a horizontal split container, this refers to the right panel of the split container. Otherwise, it is the bottom panel of the split container

Return values TRUE Succeeded.
FALSE Failed.

CWndResizer::Hook method hasn't been called.

-or-

splitContainerName, panelNameA, panelIDA, panelNameB, or panelIDB is not unique or is NULL.

-or-

The specified panels (rectangle) overlapped.

-or-

One or all of the specified panels have already been used to create split container.

Remarks Initially, the parent of this panel would be the "_root", which is the hooked window. You can change the parent by calling the CWndResizer::SetParent function. You can also set its Anchor property by calling CWndResier::SetAnchor. Initially, its Anchor value is ANCHOR_LEFT | ANCHOR_TOP.

CWndResizer determines whether the split container should be horizontal or vertical.

If the right member of the first rectangle (the second parameter) is less than the left member of the second rectangle (the third parameter), this method creates a horizontal split container. Otherwise, if the bottom member of the first rectangle (the second parameter) is less than the top member of the second rectangle (the third parameter), this method creates a vertical split container.

If you move your mouse over the space between the two panels that created the splitter, the mouse pointer will change. Then you can press down the left mouse button and drag to resize the panels. If the parent of the panel is resized, the two panels of the splitter panel will resize proportionately.

Method
BOOL GetAnchor(LPCTSTR panelName, UINT & anchor);
BOOL GetAnchor(UINT panelID, UINT & anchor);
Description Gets the anchor of the specified panel. (.NET analogy).
Parameters panelName Name of a panel that was created by a prior call to CWndResizer::CreatePanel.
panelID An ID of a child control (window) of the hooked-window.
anchor Receive the anchor value.
Return values TRUE Succeeded. anchor contains valid value.
FALSE Failed. anchor value should be ignored.

CWndResizer::Hook method hasn't been called.

-or-

panelName, or panelID is not valid.

Remarks Retrieves the anchor value of a panel. See SetAnchor for details.
Method
BOOL GetFixedPanel(LPCTSTR splitContainer, short & panel);
Description Gets the name of the panel that is fixed (if any). (.NET analogy).
Parameters splitContainerName Name of split container that was created by a prior call to CWndResizer::CreateSplitContainer.
panel Receives the ID of the fixed panel. If this contains 1, then the left panel (or the top panel if splitContainer is a vertical split container) is fixed, or if it is 2 then the right panel (or the bottom panel if splitContainer is a vertical split container). Otherwise, splitContainer has no fixed panel.
Return values TRUE Succeeded. panel contains valid value.
FALSE Failed. panel value should be ignored.

CWndResizer::Hook method hasn't been called.

-or-

splitContainerName is not valid.

Remarks Retrieves the fixed panel ID of a a split container. See SetSetFixedPanel for details.
Method
BOOL GetIsSplitterFixed(LPCTSTR splitContainerName , BOOL &fixed);
Description Gets whether or not splitter is set to fixed for a split container. (.NET analogy).
Parameters splitContainerName Name of split container that was created by a prior call to CWndResizer::CreateSplitContainer.
fixed Receives a boolean value. If this is TRUE, then the splitter is fixed, otherwise the splitter is free to be moved by mouse.
Return values TRUE Succeeded. fixed contains valid value.
FALSE Failed. fixed value should be ignored.

CWndResizer::Hook method hasn't been called.

-or-

splitContainerName is not valid.

Remarks Retrieves the a flag that indicates whether or not the splitter is free to be moved my mouse for a split container. See SetIsSplitterFixed for details.
Method
BOOL GetMaximumSize(LPCTSTR panelName, CSize & size) ;
BOOL GetMaximumSize(UINT panelID, CSize & size) ;
Description Gets the maximum size of the specified panel. (.NET analogy).
Parameters panelName Name of a panel that was created by a prior call to CWndResizer::CreatePanel.
panelID An ID of a child control (window) of the hooked-window.
Return values TRUE Succeeded. size contains valid value.
FALSE Failed. size value should be ignored.

CWndResizer::Hook method hasn't been called.

-or-

panelName or panelID is not valid.

Remarks If panelName or panelID refers to a panel that is part of horizontal split container, then CSize::cy member should be ignored. If panelName or panelID refers to a panel that is part of vertical split container, then CSize::cx member should be ignored.
Method
BOOL GetMinimumSize(LPCTSTR panelName, CSize & size) ;
BOOL GetMinimumSize(UINT panelID, CSize & size) ;
Description Gets the minimum size of the specified panel. (.NET analogy).
Parameters panelName Name of a panel that was created by a prior call to CWndResizer::CreatePanel.
panelID An ID of a child control (window) of the hooked-window.
Return values TRUE Succeeded. size contains valid value.
FALSE Failed. size value should be ignored.

CWndResizer::Hook method hasn't been called.

-or-

panelName or panelID is not valid.

Remarks If panelName or panelID refers to a panel that is part of horizontal split container, then CSize::cy member should be ignored. If panelName or panelID refers to a panel that is part of vertical split container, then CSize::cx member should be ignored.
Method
BOOL GetParent(LPCTSTR panelName, CString & parentName);
BOOL GetParent(UINT panelID, CString & parentName);
Description Gets the name of the parent of the specified panel.
Parameters panelName Name of a panel that was created by a prior call to CWndResizer::CreatePanel.
panelID An ID of a child control (window) of the hooked-window.
parentName Receives the name of the parent.
Return values TRUE Succeeded. parentName contains valid value.
FALSE Failed. size value should be ignored.

CWndResizer::Hook method hasn't been called.

-or-

panelName or panelID is not valid.

Remarks If parentName is "_root", it indicates that the panel is immediate child of the hooked-window.
Method
BOOL GetShowResizeGrip();
Description Gets a flag indication if a resize grip will be drawn on the lower-right corner of the hooked-window.
Parameters N/A No parameters.
Return values TRUE It will draw a resize grip at the bottom-right corner of the dialog/window.
FALSE It will NOT draw a resize grip at the bottom-right corner of the dialog/window.
Remarks This method never fails.
Method
BOOL Hook(CWnd * pWnd);
Description
Parameters pWnd A pointer to a CWnd that you want to resize and its child controls.
Return values TRUE Succeeded.
FALSE Failed.

pWnd is not valid.

-or-

This method has already been called once.

Remarks You must call this method before calling any other methods.

The window that the pWnd points to is called "hooked-window".

Method
BOOL InvokeOnResized();
Description Simulates resizing of the hooked-window.
Parameters N/A No parameters.
Return values TRUE Succeeded.
FALSE Failed.

CWndResizer::Hook method hasn't been called.

Remarks This method sends a WM_SIZE message to hooked-window.

After creating necessary panels and settings anchors, you would want to call this to apply settings initially.

Method
BOOL SetAnchor(LPCTSTR panelName, UINT & anchor);
BOOL SetAnchor(UINT panelID, UINT & anchor);
Description Sets the anchor of the specified panel. (.NET analogy).
Parameters panelName Name of a panel that was created by a prior call to CWndResizer::CreatePanel.
panelID An ID of a child control (window) of the hooked-window.
anchor Anchor value. See remarks.
Return values TRUE Succeeded. anchor contains valid value.
FALSE Failed.

CWndResizer::Hook method hasn't been called.

-or-

panelName, or panelID is not valid.

Remarks anchor could be one or more of the following:
Anchor values Description
ANCHOR_LEFT Anchors the left edge of the panel to its parent. This is the default for a panel.
ANCHOR_RIGHT Anchors the right edge of the panel to its parent.
ANCHOR_BOTTOM Anchors the bottom edge of the panel to its parent.
ANCHOR_CENTER_HORIZONTALLY The panel is horizontally centered within the parent rectangle. It has higher priority than ANCHOR_ALL. .NET does not have similar support.
ANCHOR_CENTER_VERTICALLY The panel is vertically centered within the parent rectangle. It has higher priority than ANCHOR_ALL. .NET does not have similar support.
ANCHOR_PRIORITY_RIGHT This is only useful if used with ANCHOR_HORIZONTALLY. If the width of the panel grows bigger than the maximum width allowed, by default, the panel's left edge will be anchored and right edge will be freed to adjust to the maximum size. If this is specified, then the right edge will be anchored and the left edge will be freed to adjust to the maximum size. .NET does not have similar support.
ANCHOR_PRIORITY_BOTTOM This is only useful if used with ANCHOR_VERTICALLY. If the height of the panel grows bigger than the maximum height allowed, by default, the panel's top edge will be anchored and bottom edge will be freed to adjust to the maximum size. If this is specified, then the bottom edge will be anchored and the top edge will be freed to adjust to the maximum size. .NET does not have similar support.
ANCHOR_VERTICALLY (ANCHOR_TOP | ANCHOR_BOTTOM)
ANCHOR_HORIZONTALLY (ANCHOR_LEFT | ANCHOR_RIGHT)
ANCHOR_ALL (ANCHOR_VERTICALLY | ANCHOR_HORIZONTALLY)

Note: A panel will never grow less than its allowed minimum size and more than its allowed maximum size.

Method
BOOL SetFixedPanel(LPCTSTR splitContainerName, short panel);
Description Sets one of the panel of a split container to be fixed. (.NET analogy).
Parameters splitContainerName Name of a panel that was created by a prior call to CWndResizer::CreateSplitContainer.
panel ID of the fixed panel. If this contains 1, then the left panel (or the top panel if splitContainer is a vertical split container) is fixed, or if it is 2 then the right panel (or the bottom panel if splitContainer is a vertical split container). Otherwise, splitContainer has no fixed panel.
Return values TRUE Succeeded.
FALSE Failed.

CWndResizer::Hook method hasn't been called.

Remarks As hooked-window is resizes, the fixed panel remain same and the other panel resizes.

Note: A panel will never grow less than its allowed minimum size and more than its allowed maximum size.

Method
BOOL SetIsSplitterFixed(LPCTSTR splitContainerName , BOOL fixed);
Description Sets whether or not splitter is set to fixed for a split container. (.NET analogy).
Parameters splitContainerName Name of split container that was created by a prior call to CWndResizer::CreateSplitContainer.
fixed A boolean value. If this is TRUE, then the splitter is fixed, otherwise the splitter is free to be moved by mouse.
Return values TRUE Succeeded.
FALSE Failed.

CWndResizer::Hook method hasn't been called.

Remarks If the splitter is set to fixed, user cannot move the splitter using mouse.
Method
BOOL SetMaximumSize(LPCTSTR panelName, CSize & size) ;
BOOL SetMaximumSize(UINT panelID, CSize & size) ;
Description Sets the maximum size of the specified panel. (.NET analogy).
Parameters panelName Name of a panel that was created by a prior call to CWndResizer::CreatePanel.
panelID An ID of a child control (window) of the hooked-window.
size Minimum size of the panel.
Return values TRUE Succeeded.
FALSE Failed.

CWndResizer::Hook method hasn't been called.

-or-

size is smaller than CWndResizer::GetMinimumSize.

Remarks If panelName or panelID refers to a panel that is part of horizontal split container, then cy member of size should be ignored. If panelName or panelID refers to a panel that is part of vertical split container, then cx member of size should be ignored.
Method
BOOL SetMinimumSize(LPCTSTR panelName, CSize & size) ;
BOOL SetMinimumSize(UINT panelID, CSize & size) ;
Description Sets the minimum size of the specified panel. (.NET analogy).
Parameters panelName Name of a panel that was created by a prior call to CWndResizer::CreatePanel.
panelID An ID of a child control (window) of the hooked-window.
Return values TRUE Succeeded.
FALSE Failed.

CWndResizer::Hook method hasn't been called.

-or-

panelName or panelID is not valid.

Remarks If panelName or panelID refers to a panel that is part of horizontal split container, then cy member of size should be ignored. If panelName or panelID refers to a panel that is part of vertical split container, then cx member of size should be ignored.
Method
BOOL SetParent(LPCTSTR panelName, LPCTSTR parentName);
BOOL SetParent(UINT panelID, LPCTSTR parentName);
BOOL SetParent(LPCTSTR panelName, UINT parentID);
BOOL SetParent(UINT panelID, UINT parentID);
Description Sets the name of the parent of the specified panel.
Parameters panelName Name of a panel that was created by a prior call to CWndResizer::CreatePanel.
panelID An ID of a child control (window) of the hooked-window.
parentID Name of the parent.
parentName Name of the parent.
Return values TRUE Succeeded. parentName contains valid value.
FALSE Failed. size value should be ignored.

CWndResizer::Hook method hasn't been called.

-or-

panelName, panelID , parentName or parentName is not valid.

Remarks This should not be thought of as Win32 SetParent API.

Set parentName to "_root", to indicates that the panel is immediate child of the hooked-window.

Method
void SetShowResizeGrip(BOOL show = TRUE);
Description Sets a flag indication if a resize grip will be drawn on the lower-right corner of the hooked-window.
Parameters show TRUE if the resize grip should be drawn, otherwise FALSE.
Return values N/A No return values.
Remarks This method never fails.
Method
BOOL Unhook();
Parameters N/A No parameters.
Return values TRUE Succeeded.
FALSE Failed.

CWndResizer::Hook method hasn't been called.

Remarks You do not have to call this method.

This method is automatically called when hooked-window receives WM_DESTROY message.

Points of Interest

This solution does not require you to calculate any numbers on your part. This solution encapsulates all functionalities in the CWndResizer.

One can re-design it such that it exposes other objects (CPanel, CSplitContainer, etc.). The reason I designed it this way is because I did not want the user to have to know too many objects, and to simplify memory allocation and de-allocation.

Another thing is that it depends on MFC classes (CWnd etc.); again, one can remove all references to MFC classes and use pure Win32 APIs.

History

  • 5th November, 2010: Initial post
  • 26th November, 2010: Article updated
  • 30th November, 2010: Source code and demo updated
推荐.NET配套的通用数据层ORM框架:CYQ.Data 通用数据层框架
新浪微博粉丝精灵,刷粉丝、刷评论、刷转发、企业商家微博营销必备工具"