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:

Add a layout map to your class, using the macros BEGIN_LAYOUT_MAP(), END_LAYOUT_MAP() and several others, as described below.

CDialogLayout<T> handles the WM_SIZE and WM_INITDIALOG messages, so make sure the message handlers get properly called:

Call CHAIN_MSG_MAP(CDialogLayout<T>) at the end of your message map.

If you handle those messages yourself, call SetMsgHandled(FALSE) (or bHandled=FALSE, if you're still using the old-style maps) from your handlers.

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:

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:

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:

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:

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

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.

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 for your interest in the article! But you should know that I haven't been using this, or even Win32/WTL, in a long time. So it's quite likely that you are more of an expert about it than I...

I think the problem is that DeferWindowPos() expects coordinates relative to the parent window, which would be different for the two controls.

Are your child windows (your "wizards", if I get it right) implemented as WTL classes as well? In that case, maybe you should make the wizards classes themselves derive from CDialogLayout, layout them internally, and treat them as single "layout units" in your dialog class.

Hello. Great article. I'm already running it in a WTL8 app with no problemos, and I even find it suitable for CFormView windows. The only gotcha I had is that I was using WTL's types CRect, CSize et al, so I had to remove the ATL ones.

Now I'm wondering. Is there any way to set a minimal size of the dialog?

- remove typedef _DialogSplitHelper::DLGTEMPLATEEX DLGTEMPLATEEX; in a few places
- #include <atldlgs.h> and prerequisites to get the DLGTEMPLATEEX declaration
- don't derive CDialogLayout from CMessageMap

Other than that, it works great (I have yet to check for the memory leaks mentioned below).

When I run the sample application, I found the Button "OK" and "Cancel" may partly out of the dialog's region, while the control above, such as button "Add..." and "Remove", worked well. Seems that OK and Cancel is not in the layer-out.

My machine is Windows XP, SP2. I dont build the program, just run the .exe in the download package.

It is cool and works as expected, but leaking memory. After some tracking, found that memory allocated by following line( of class CTestDialog3 ),
// ...
BEGIN_LAYOUT_CONTAINER( ABS(7), ABS(7), ABS(-7), ABOVE_PAD(IDOK, 7) )
// ...
start leaks.

WTL already includes resizable dialogs (CDialogResize), and they seem quite similar to yours -- including macros which form a resizing map. Michael Dunn wrote a great CP article about it, a couple of years ago.

I think you it would be better to turn your code into an enhancement to CDialogResize, instead of starting from scratch! You would generate much more interest that way. Especially since WTL is now on SourceForge, you might be able to contribute to a future WTL release.

I found CDialogResize not flexible enough for some tasks, for example the case where you need two controls occupying half of the dialog's size. And I didn't see how I could expand CDialogResize's resize maps to accomplish this without getting clumsy code constructs.
But perhaps I should include a few words about CDialogResize in the article.

There are two classes operating in the background: the DialogTemplate and DialogItemTemplate classes read the dialog from a resource and operate on the related structures. The main reason for this is that I couldn't use the Win32 MapDialogRect() function since it needs a valid window handle, but I wanted to operate on the dialog resources directly.

Since this is quite a bit of code, and the functionality isn't really dependent on any template parameters, I decided to put it into .cpp files rather than headers. But of course, you could just inline all the methods and do away with the cpp files.