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

Automatic Layout of Resizable Dialogs

0.00/5 (No votes)
19 Jan 2006 1  
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

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

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

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:

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