Click here to Skip to main content
15,868,141 members
Articles / Desktop Programming / WTL
Article

Automatic Layout of Resizable Dialogs

Rate me:
Please Sign up or sign in to vote.
4.90/5 (36 votes)
19 Jan 20068 min read 100.7K   3K   79   16
A WTL extension which introduces layout maps to automatically update the layout in resizable dialogs.

Introduction

Recently I worked on a WTL project that involved a lot of dialogs, and most of them had more or less complicated layout schemes that could not be described with Visual Studio's Dialog Designer. In addition, they had to be resizable and even retain their layouts when being resized.

Think of a simple application where you want a control to always "fill" a dialog, whatever its size. Or you might wish to create a resizable dialog that always keeps its "OK" and "Cancel" buttons neatly in the corner. Usually, this requires you to write handlers for WM_SIZE, WM_WINDOWPOSCHANGED or similar and "hand-code" the layout in your dialog class.

For simple cases like the above, this can be accomplished with one or two lines of code. However, as the number of dialogs in your project - or the sophistication of their layout - grows, you will find yourself writing similar code again and again or polluting your code with layouting.

Layout maps

So I came up with a "semi-automatic" solution that pretty much meets the spirit of WTL. This solution is called "layout maps" and is, like all other ATL/WTL maps, based on macros. Though I don't particularly like hiding lots of code behind the innocent-seeming macros, I found it adequate in this case as it keeps things readable.

Note that WTL already contains a class for this purpose; it is called CDialogResize<T> and can be found - for whatever reason - in the header atlframe.h. Looking at its source code should reveal quickly how it can be used. It allows for each control to specify an action that is to be taken when the dialog is resized. This action can either be move or size - or none, which is given implicitly if a control is not listed. It is also possible to display a "gripper" in the lower right corner of the dialog, and to specify limits for its size. In fact, the solution presented here is quite similar to CDialogResize<T>, but takes it a step further.

All you need to do to add dialog-layouting capabilities to your WTL dialog is to follow these simple steps:

  1. Derive your dialog class from CDialogLayout<T> (in addition to CDialogImpl<T>).
  2. Add a layout map to your class, using the macros BEGIN_LAYOUT_MAP(), END_LAYOUT_MAP() and several others, as described below.
  3. CDialogLayout<T> handles the WM_SIZE and WM_INITDIALOG messages, so make sure the message handlers get properly called:
    1. Call CHAIN_MSG_MAP(CDialogLayout<T>) at the end of your message map.
    2. If you handle those messages yourself, call SetMsgHandled(FALSE) (or bHandled=FALSE, if you're still using the old-style maps) from your handlers.
  4. Set m_bGripper to TRUE or FALSE according to whether or not you want a "size gripper" in the lower right corner. The default is TRUE.

Anchors

The key concept used for layouting the controls are "anchors", which are also used by .NET Windows Forms. Any control can anchor to any combination of the four edges (left, top, right and bottom) of the main dialog. If a control anchors to an edge, the distance between the control and that edge is always kept constant.

So, the usual behavior of controls can be seen as anchoring "top-left" (they move when you drag the upper left corner of the dialog, but not when you drag the lower right corner), which is also the default behavior with CDialogLayout.

Example 1: Simple dialog box

Image 1

In this example, I would like to automatically layout a simple dialog box when the user resizes it. It only contains two buttons, OK and Cancel, in the lower right corner. They should stick to the lower right corner even if the dialog box is resized. To accomplish this, one would use the following code:

#include <DialogLayout.h>
class CTestDialog :
    public CDialogImpl<CTestDialog>,
    public CDialogLayout<CTestDialog>
{
public:
    enum { IDD = IDD_TESTDIALOG };

    BEGIN_MSG_MAP(CTestDialog)
        MSG_WM_INITDIALOG(OnInitDialog)
        COMMAND_ID_HANDLER_EX(IDOK, OnOK)
        COMMAND_ID_HANDLER_EX(IDCANCEL, OnCancel)
        CHAIN_MSG_MAP(CDialogLayout<CTestDialog>)
    END_MSG_MAP()


    BEGIN_LAYOUT_MAP()
        LAYOUT_CONTROL(IDOK, 
               LAYOUT_ANCHOR_RIGHT | LAYOUT_ANCHOR_BOTTOM)
        LAYOUT_CONTROL(IDCANCEL, 
               LAYOUT_ANCHOR_RIGHT | LAYOUT_ANCHOR_BOTTOM)
    END_LAYOUT_MAP()


    BOOL OnInitDialog(HWND, LPARAM)
    {
        // ...
        
        m_Gripper = FALSE;

        SetMsgHandled(FALSE);
        return TRUE;
    }

    
    void OnOK(UINT, int, HWND)
    {
        EndDialog(IDOK);
    }


    void OnCancel(UINT, int, HWND)
    {
        EndDialog(IDCANCEL);
    }
};

The default anchor is LAYOUT_ANCHOR_LEFT | LAYOUT_ANCHOR_TOP (which means stick to the upper left corner), so you don't need to list controls with this behavior explicitly.

Example 2: Dialog box with ListBox

Image 2

The next example features a dialog with OK/Cancel buttons like the above, but it also has a list box and Add/Remove buttons. The list box should always "fill" the window of the dialog box, so that it grows or shrinks with the dialog box. This is accomplished by the LAYOUT_ANCHOR_ALL, which is in fact shorthand for ORing all the four flags together. Similarly, you can use LAYOUT_ANCHOR_HORIZONTAL or LAYOUT_ANCHOR_VERTICAL for combining only left and right or top and bottom, respectively.

Of course, we focus on the layout here, so we don't care about what the buttons actually do. We simply need to add some entries to the layout map:

BEGIN_LAYOUT_MAP()
    LAYOUT_CONTROL(IDC_LIST, LAYOUT_ANCHOR_ALL)
    LAYOUT_CONTROL(IDC_BUTTON_ADD, LAYOUT_ANCHOR_RIGHT | LAYOUT_ANCHOR_TOP)
    LAYOUT_CONTROL(IDC_BUTTON_REMOVE, LAYOUT_ANCHOR_RIGHT | LAYOUT_ANCHOR_TOP)
    LAYOUT_CONTROL(IDOK, LAYOUT_ANCHOR_RIGHT | LAYOUT_ANCHOR_BOTTOM)
    LAYOUT_CONTROL(IDCANCEL, LAYOUT_ANCHOR_RIGHT | LAYOUT_ANCHOR_BOTTOM)
END_LAYOUT_MAP()

Layout containers

So far, the anchors of the controls always referred to the edges of the main dialog window. However, there are cases where this solution is not flexible enough. This is where the layout containers come into play. In fact, there is always one layout container surrounding the entire dialog box, which is implicitly created by the BEGIN_LAYOUT_MAP() macro.

If you wish, you can define additional layout containers and even nest them. For this purpose, use the macros BEGIN_LAYOUT_CONTAINER() and END_LAYOUT_CONTAINER(). All LAYOUT_CONTROL entries inside a layout container use the edges of the container rather than the main dialog for anchorage.

BEGIN_LAYOUT_CONTAINER() takes four parameters, one layout rule for all the four edges of the container. There are different types of layout rules, and you can use them in any combination:

  • The ABS() rule places an edge at an absolute position inside the parent container, given in DLUs (dialog units). You can also use negative numbers to start counting from the right or bottom edge of the parent.
  • The RATIO() rule takes a floating-point number between 0.0 and 1.0 and places the edge so that it always divides the parent container in that ratio.
  • The LEFT_OF(), ABOVE(), RIGHT_OF() and BELOW() rules take the ID of a dialog control and align the container's edge with the respective edge of the control. Note that if there is also a LAYOUT_CONTROL() entry for the control, it should occur before the layout container in the layout map.
  • The LEFT_OF_PAD(), ABOVE_PAD(), RIGHT_OF_PAD() and BELOW_PAD() rules work just like the rules above, but an additional padding can be specified (in DLUs) that is added between the container's edge and the control.

Some examples:

  • A container which always maintains a space of 7 DLUs to all edges of its parent container:
    BEGIN_LAYOUT_CONTAINER( ABS(7), ABS(7), ABS(-7), ABS(-7) )
    // ...
    END_LAYOUT_CONTAINER()
  • A container which always occupies the upper-left quarter of its parent:
    BEGIN_LAYOUT_CONTAINER( ABS(0), ABS(0), RATIO(0.5), RATIO(0.5) )
    // ...
    END_LAYOUT_CONTAINER()

    Note that ABS(0) and RATIO(0) have the same effect.

Layout containers need not be attached to any control in your dialog. However, this is a frequent application for them (e.g. with group boxes), so I have added the convenient BEGIN_LAYOUT_CONTAINER_AROUND_CONTROL() macro which takes just the ID of the container control as a parameter. If you look at its definition, it is just a shorthand:

#define BEGIN_LAYOUT_CONTAINER_AROUND_CONTROL(ctrlID) \
    BEGIN_LAYOUT_CONTAINER( LEFT_OF(ctrlID), ABOVE(ctrlID), \
        RIGHT_OF(ctrlID), BELOW(ctrlID) )

Example 3: Two ListBoxes with "Move" buttons

Image 3

The next example is a bit more complicated than the preceding ones, and (not surprisingly) needs additional layout containers. There are two list boxes and some buttons to move items between the list boxes. Again, we only care about the layout:

  • As before, the "OK" and "Cancel" buttons should stay in the lower right corner of the dialog.
  • The list boxes should always have the same size, and each take about half the width of the dialog's area.
  • The move buttons should be both horizontally and vertically centered between the list boxes.

A layout map that would meet these conditions would be (note that there may be several equivalent layout maps):

BEGIN_LAYOUT_MAP()
    LAYOUT_CONTROL(IDOK, LAYOUT_ANCHOR_RIGHT | LAYOUT_ANCHOR_BOTTOM)
    LAYOUT_CONTROL(IDCANCEL, LAYOUT_ANCHOR_RIGHT | LAYOUT_ANCHOR_BOTTOM)
    BEGIN_LAYOUT_CONTAINER( ABS(7), ABS(7), ABS(-7), ABOVE_PAD(IDOK, 7) )

        BEGIN_LAYOUT_CONTAINER( ABS(0), ABS(0), RATIO(0.5),
                RATIO(1.0) )    
            LAYOUT_CONTROL(IDC_LIST1, LAYOUT_ANCHOR_ALL)
        END_LAYOUT_CONTAINER()

        BEGIN_LAYOUT_CONTAINER( RATIO(0.5), ABS(0), RATIO(1.0),
                RATIO(1.0) )
            LAYOUT_CONTROL(IDC_LIST2, LAYOUT_ANCHOR_ALL)
        END_LAYOUT_CONTAINER()

        BEGIN_LAYOUT_CONTAINER( RIGHT_OF_PAD(IDC_LIST1, 7), 
                RATIO(0.0), LEFT_OF_PAD(IDC_LIST2, 7), RATIO(0.5) )
            LAYOUT_CONTROL(IDC_BUTTON_MOVELEFT,
                    LAYOUT_ANCHOR_LEFT | LAYOUT_ANCHOR_BOTTOM)
        END_LAYOUT_CONTAINER()

        BEGIN_LAYOUT_CONTAINER( RIGHT_OF_PAD(IDC_LIST1, 7),
                RATIO(0.5), LEFT_OF_PAD(IDC_LIST2, 7), RATIO(1.0) )
            LAYOUT_CONTROL(IDC_BUTTON_MOVERIGHT,
                    LAYOUT_ANCHOR_LEFT | LAYOUT_ANCHOR_TOP)
        END_LAYOUT_CONTAINER()

    END_LAYOUT_CONTAINER()
END_LAYOUT_MAP()

The following figure shows the arrangement of the four inner containers:

Image 4

How it works

Though there is a lot of code behind those macros, it is really straightforward. All of the macros map to one of these classes: CLayoutControl, CLayoutContainer, or CLayoutRule.

With the BEGIN_LAYOUT_MAP() macro, you actually define a method named SetupLayout(), called once from the WM_INITDIALOG handler. This method creates the tree-like structure of layout containers and layout rules, which is then kept in memory until the dialog is destroyed.

The WM_SIZE handler then calls the method DoLayout() which is propagated through the tree. Essentially, this is where all the rules are actually applied. The controls are then repositioned all at once using DeferWindowPos() calls.

Known issues

Windows XP sometimes shows strange behavior when using the new Common Controls manifest. This applies especially to group boxes, which tend to disappear when the dialog is resized. I believe that this is a Windows bug, and have employed a simple workaround which just redraws the group boxes every time they are repositioned. This may introduce some flickering, though.

A drawback of the layout maps is that you need a control ID for every item of your layout map - even static controls which do not normally have their own ID. However, you would need these if you had coded your layout yourself.

Conclusion

Using the layout maps described above, it is possible to achieve sophisticated layout schemes with resizable dialogs. The presented solution is both simple and flexible, and integrates well into WTL.

Revision history

  • 07-16-2005
    • Original article.
  • 01-19-2006
    • Added capability to display a "gripper" in the corner.
    • Added a paragraph about WTL's CDialogResize.
    • Corrected some minor errors in the article text and source code.

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


Written By
Software Developer (Senior) Accenture
Germany Germany
Till is living in Munich, Germany, and works as an IT consultant. His current focus is mainly on Java Enterprise projects, but tries to stay up to date with the latest .NET developments.

Comments and Discussions

 
GeneralResizing for contained windows [modified] Pin
filippov.anton10-Nov-10 21:57
filippov.anton10-Nov-10 21:57 
Hello.

My dialog has some contained windows on it (childs, something like wizard), and controls in this windows is not reachable by CLayoutControl::FindControl(nID). How to add it into resize procedures?

Add new m_pRootContainer for each of contained windows?

I added new class for controls in Dlg Sheet:
template<class TWizard>
class CLayoutControlT : public CLayoutNode
{
private:
	UINT m_nID;
	UINT m_nAnchor;

	TWizard* m_pWizard; // it needed to get wndCtrl in CalcRect

public:
	CLayoutControlT(TWizard* pWizard, UINT nID, UINT nAnchor = LAYOUT_ANCHOR_NONE)
		: m_pWizard(pWizard)
		, m_nID(nID), m_nAnchor(nAnchor)
	{}

	UINT GetID() const { return m_nID; }
	UINT GetAnchor() const { return m_nAnchor; }
	CRect GetMargins(CWindow wndLayout, CWindow wndCtrl) const
	{ // because we don't know about size and position ctrl before size starting
		CRect rcMargins;
		CRect rcLayout, rcCtrl;
		wndLayout.GetWindowRect(rcLayout);
		wndCtrl.GetWindowRect(rcCtrl);

		rcMargins.SetRect(	rcCtrl.left - rcLayout.left,
			rcCtrl.top - rcLayout.top,
			rcLayout.right - rcCtrl.left - rcCtrl.Width(),
			rcLayout.bottom - rcCtrl.top - rcCtrl.Height()	);

		return rcMargins;
	}

private:
	HWND CalcRect(CWindow wndLayout, LPCRECT prcLayout, LPRECT prc)
	{
		ATLASSERT( wndLayout.IsWindow() );
		ATLASSERT( prcLayout != NULL );
		ATLASSERT( prc != NULL );

		// Retrieve the control's window handle
		CWindow wndControl = m_pWizard->GetDlgItem( m_nID ); // get right wndCtrl (Wizard has overridden function for ctrl search)
		ATLASSERT( wndControl.IsWindow() );

		// Get the control's current bounds
		CRect rcOld;
		wndControl.GetWindowRect(rcOld);
		wndLayout.ScreenToClient(rcOld);

		CopyRect( prc, rcOld );

		CRect rcMargins(GetMargins(wndLayout, wndControl)); // get margins through layout window

		if ( m_nAnchor & LAYOUT_ANCHOR_LEFT )
		{
			prc->left = prcLayout->left + rcMargins.left;
			if ( !(m_nAnchor & LAYOUT_ANCHOR_RIGHT) )
				prc->right = prc->left + rcOld.Width();
		}
		if ( m_nAnchor & LAYOUT_ANCHOR_TOP )
		{
			prc->top = prcLayout->top + rcMargins.top;
			if ( !(m_nAnchor & LAYOUT_ANCHOR_BOTTOM) )
				prc->bottom = prc->top + rcOld.Height();
		}
		if ( m_nAnchor & LAYOUT_ANCHOR_RIGHT )
		{
			prc->right = prcLayout->right - rcMargins.right;
			if ( !(m_nAnchor & LAYOUT_ANCHOR_LEFT) )
				prc->left = prc->right - rcOld.Width();
		}
		if ( m_nAnchor & LAYOUT_ANCHOR_BOTTOM )
		{
			prc->bottom = prcLayout->bottom - rcMargins.bottom;
			if ( !(m_nAnchor & LAYOUT_ANCHOR_TOP) )
				prc->top = prc->bottom - rcOld.Height();
		}

// here is some right points, I think

		return (HWND) wndControl;
	}

public:
	void DoLayout(CWindow wndLayout, HDWP hDwp, const CRect& rcLayout)
	{
		CRect rcControl;
		CWindow wndControl = CalcRect( wndLayout, rcLayout, rcControl );

		StoreNewWindowRect( wndControl, hDwp, rcControl );

		wndControl.DeferWindowPos( hDwp, NULL, RECT_BREAK(rcControl),
			SWP_NOACTIVATE | SWP_NOZORDER );
	}
};


in LayouMap:
class CNewWizard : public Wizard
{
// ...
public:
// ...
	BEGIN_LAYOUT_MAP()
		pCurrentContainer->AddLayoutNode( new CLayoutControl( m_Tmpl, *pCurrentContainer, (IDOK), (LAYOUT_ANCHOR_RIGHT | LAYOUT_ANCHOR_TOP) ) );

		pCurrentContainer->AddLayoutNode( new CLayoutControlT<CNewWizard>( this, (IDC_BUTTON1), (LAYOUT_ANCHOR_RIGHT | LAYOUT_ANCHOR_BOTTOM) ) );
	END_LAYOUT_MAP()
}

IDOK - it a button on main dialg
IDC_BUTTON1 - it a button on sheet

After that - only border of dialog have resizing ability (OK button not repositioning), but trace window (if print control rect after positioning) shows, that coordinates of rect is changed, but window don't.
If comment line with adding IDC_BUTTON1 - all works right. What wrong?

Problem in wndControl.DeferWindowPos( hDwp, NULL, RECT_BREAK(rcControl), SWP_NOACTIVATE | SWP_NOZORDER ) ( CLayoutControlT::DoLayout() )
After it no reposition to all controls in collection.


Thanks.
modified on Friday, November 12, 2010 7:05 AM

GeneralRe: Resizing for contained windows Pin
Till Krullmann28-Nov-10 0:52
Till Krullmann28-Nov-10 0:52 
GeneralGreat article Pin
Fernando A. Gomez F.21-Apr-08 6:10
Fernando A. Gomez F.21-Apr-08 6:10 
GeneralSome fixes I had to do with WTL 8 Pin
roel_25-Mar-08 13:41
roel_25-Mar-08 13:41 
QuestionDialog Gripper bar Pin
gjr7-Aug-07 20:28
gjr7-Aug-07 20:28 
GeneralFix for memory leaks Pin
Cristi13-Jun-06 11:26
Cristi13-Jun-06 11:26 
GeneralGood Work! But Button &quot;OK&quot; and &quot;Cancel&quot; dispear Pin
Phil. Invoker31-Oct-05 15:07
Phil. Invoker31-Oct-05 15:07 
Generalit is very nice, but memory leaks detected Pin
Lin Lin18-Aug-05 21:05
Lin Lin18-Aug-05 21:05 
QuestionHow does this relate to CDialogResize? Pin
Don Clugston17-Jul-05 14:31
Don Clugston17-Jul-05 14:31 
AnswerRe: How does this relate to CDialogResize? Pin
Till Krullmann17-Jul-05 22:17
Till Krullmann17-Jul-05 22:17 
GeneralRe: How does this relate to CDialogResize? Pin
A.Domo31-Jul-05 2:14
A.Domo31-Jul-05 2:14 
GeneralRe: How does this relate to CDialogResize? Pin
A.Domo31-Jul-05 2:19
A.Domo31-Jul-05 2:19 
GeneralRe: How does this relate to CDialogResize? Pin
Till Krullmann31-Jul-05 4:59
Till Krullmann31-Jul-05 4:59 
GeneralFlick when change width of dialog Pin
Sergey Solozhentsev15-Jul-05 18:50
Sergey Solozhentsev15-Jul-05 18:50 
GeneralRe: Flick when change width of dialog Pin
gri15-Jul-05 22:53
gri15-Jul-05 22:53 
GeneralRe: Flick when change width of dialog Pin
Sergey Solozhentsev17-Jul-05 19:23
Sergey Solozhentsev17-Jul-05 19:23 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.