C++ Layout Manager, Part II

By: Daniel Horn

Abstract: Version 2 of the layout manager includes many enhancements, a bug fix, and interface changes that simplify use.

C++ Layout Manager, Part II

By Daniel Horn

Abstract: Version 2 of the layout manager includes many enhancements, a bug fix, and interface changes that simplify use.


An earlier paper (http://bdn.borland.com/article/0,1410,29572,00.html) introduced a simple layout manager for resizing and repositioning child windows in a parent window in a Windows program written in C++.  It was easy to use and handled many practical cases.  

This paper improves upon version 1 of the layout manager with many enhancements.  Many more different layouts are now possible.  At the same time, it's even easier to use than before in many cases.  We'll present a sample program demonstrating the new features.  We'll describe the interface changes and new methods in this new version of the layout manager.  And, oh yes, we fix a bug that users of version 1 may have encountered. 

Windows programmers who do not use the VCL do not have the same powerful visual tools available to them for designing their windows and dialog boxes.  At best they have a dialog resource editor (as in Resource Workshop or Visual Studio).  Note that the layout manager presented here is not integrated with a visual tool in the sense of, say, a GridBagLayout in Java.  It is a library of functionality that can be applied to any parent window (throughout, we use "parent" in the sense of a window with child or control windows); you will often use it with a resizable dialog box whose initial design was created with a resource editor.  The layout manager greatly simplifies the tasks of resizing and repositioning child windows in a parent window when the parent is resized.


The Sample Program:

The source (http://codecentral.borland.com/codecentral/ccWeb.exe/listing?id=19666) for the sample program demonstrates many of the features of the new version of the layout manager.  There's not much to do with the program except run it, open the various dialog boxes from the main menu, resize each dialog and see how the display changes.


Using the Layout Manager and Changes to the Interface

Using the layout manager is essentially the same as in version 1 except that the SimpleLayoutManager::AddChildConstraint() member function has slightly changed.

In the WM_CREATE message handler of a window procedure or the WM_INITDIALOG handler of a dialog box procedure, one adds lines like:

    layoutManager->AddChildConstraint(IDC_LIST2, CWA_HCENTER, CWA_BOTTOM);

This tells the layout manager that the child window with control id equal to IDC_LIST2 is to be added to its list of children to re-layout whenever the parent window is resized.  CWA_HCENTER and CWA_BOTTOM are sizing attributes (of type enum CWAttribute) that indicate that the child window is to be centered horizontally with respect to the parent window and that its location is to maintain a constant distance from the bottom of the parent.  

Users of version 1 will note that the signature of this method has changed.  We now pass the horizontal and vertical attributes as separate arguments; this has certain advantages, as you'll see, both in the interface and in the implementation code.

When two child windows overlap during a resize, a painting glitch will often occur.  As a result, Version 1 included an attribute called CWA_FORCEINVALIDATE which was used to ensure that a child window be repainted.  The CWA_FORCEINVALIDATE attribute is no longer available in version 2 (since, strictly speaking, it is neither a horizontal nor a vertical attribute); it is replaced by a new member function for setting a flag to indicate the action:

    void SimpleLayoutManager::SetForceInvalidate(int id, bool b);

"id" would indicate the child window, and, in practice, one would set "b" to "true" only.


Sizing and Positioning Attributes

The list of attributes used in SimpleLayoutManager::AddChildConstraint() is contained in the enum CWAttribute.

Here, and in all that follows, we will describe how the attributes and methods behave for the horizontal orientation only.  (The case for the vertical orientation is exactly analogous and symmetric to the horizontal case; just substitute "top" for "left", "bottom" for "right", CWA_TOPBOTTOM for CWA_LEFTRIGHT, and so on.)

When used as argument in AddChildConstraint():

CWA_LEFT means that the horizontal position of the child window does not change, i.e., the distance between the child's left side and its parent's left side remains the same when the parent is resized;

CWA_RIGHT: the distance between the child's right side and its parent's right side remains fixed;

CWA_LEFTRIGHT: the distance between the child's left and its parent's left side is fixed, and the distance between the child's right and the parent's right side is fixed;  the size of the child is stretched to accomplish this;

CWA_HCENTER: the horizontal center of the child is aligned with that of the parent window.

Except for the addition of CWA_HCENTER, this is basically the same as for version 1.  As you'll see in a little while, these attributes take on additional meanings  when one starts using the new methods in the SimpleLayoutManager class.


Enforcing a Minimum Window Size:

One useful new feature is the ability to ensure that the parent window always maintains a minimum width and height (or both).

To do this, you need to make two changes.

First, you need to specify the specify the minimum width and height values (in screen coordinates) using 

    void SimpleLayoutManager::SetMinSize(int minWidth, int minHeight);

Since the minimum size is often the initial size of the window or the design size of a dialog (in a resource editor), we provide a second version of this method that may be all you ever need:

    void SimpleLayoutManager::SetMinSize(void);

The second version just sets the minimum size to the current size of the window.

Now to ensure that this minimum size is actually enforced, all you have to do is add a message handler for the WM_SIZING message that looks like:

	case WM_SIZING:
		return layoutManager->DoSizing((RECT*)lParam);

Call "Test Dialog 1" from the sample program's "Window" menu to see this used in practice.

You will almost always want to use this feature in practice to avoid the unnecessary clutter, confusion, and paint problems that result from overlapping child windows.  (The other test dialogs in the sample deliberately do not use this feature, so you'll be able to judge this for yourself) .


New Features

Test Dialog 2 demonstrates the new CWA_HCENTER and CWA_VCENTER attributes for centering the "Move Down" and "Modify" buttons, respectively.

The best new feature of the layout manager -- the one that makes it even easier in many cases to use than before -- is the use of default arguments in 

    void SimpleLayoutManager::AddChildConstraint(int id, CWAttribute attrHorz = CWA_DEFAULT, CWAttribute attrVert = CWA_DEFAULT);

If one calls this member function with CWA_DEFAULT as either or both of the CWAttribute arguments (or, in fact, if one leaves them off altogether), then the function automatically makes an intelligent guess as to which attribute to use.  In the initial design of the window, if the child window is entirely to the left of the horizontal center of its parent, then it receives a CWA_LEFT attribute; if the child is to the right of its parent's center, then it gets CWA_RIGHT; and if the child straddles the parent's center, then it receives CWA_LEFTRIGHT.  

Most likely, you will always use this feature when you apply the layout manager to a window.  You should specify explicit attributes in the call to AddChildConstraint only when you discover that the implicit behavior is not exactly what you want (e.g., when a child should have a CWA_HCENTER attribute).  See Test Dialog 3 for an example of this feature in action; you will see that two of the buttons behave differently than desired because they end up with implicit CWA_LEFTRIGHT and CWA_TOPBOTTOM attributes (see the sample code to how to apply explicit CWA_HCENTER and CWA_VCENTER attributes in these cases).

Another example using implicit attributes, but with more controls, appears in Test Dialog 4.


More New Features: Relative Positions and Sizes/Weights

There are many other desired arrangements of child windows in practice than are supported by version 1 of the layout manager.

Here are some examples.  You may want a child window that::

  1. always is 1/2 the width (or height) of the parent window;
  2. always is the same size as another child;
  3. maintains a fixed horizontal (or vertical) distance from another child;
  4. is horizontally aligned with another child.

For example 1, you would add a call like:

    layoutManager->SetHorzSizeWeight(IDC_LIST, 50);

The first argument is the id of the child window.  The second argument tells how to size the child as a percentage of its parent's size; the "50" means that IDC_LIST should be 50% of the width of its parent.

If you wish to specify that the width should be a percentage of the width of another child window (example 2), then make a call like:

    layoutManager->SetHorzSizeWeight(IDC_COMBO1, 100, IDC_LIST);

This line of code says that the child window with id IDC_COMBO1 should have the same width as (i.e., 100% the width as) the child window specified by the id in the third argument (in this case, IDC_LIST).

SetHorzSizeWeight() affects the size of the child only.  The position of the child in its parent is determined as before by the argument passed into AddChildConstraint.  One thing to be aware of is the following:  if the attribute passed to AddChildConstraint is CWA_LEFTRIGHT, then we assume that that was actually what was intended and that, effectively, the change otherwise affected by SetHorzSizeWeight is ignored.  (There does not seem to be a meaningful and intuitive action for the combination of the resizing caused by the CWA_LEFTRIGHT attribute with the resizing caused by SetHorzSizeWeight.  Therefore, in this case, SetHorzSizeWeight has no actual effect).

If you want the child indicated by IDC_COMBO1 to always maintain a constant horizontal distance from IDC_LIST (example 3), you would call:

    layoutManager->SetRelativeHorzPos(IDC_COMBO1, IDC_LIST);

The "distance" is determined by the attribute passed into AddChildConstraint.  In other words, if IDC_COMBO1 has an attribute of CWA_LEFT, then the fixed distance is the distance between the left size of IDC_COMBO1 and the right side of IDC_LIST.  If the attribute is CWA_RIGHT, the distance is between the right size of IDC_COMBO1 and the left of IDC_LIST.  If the attribute is CWA_HCENTER, then the horizontal center of IDC_COMBO1 will match that of IDC_LIST.  If the attribute is CWA_LEFTRIGHT, then IDC_COMBO1 is aligned with both the left and right sides of IDC_LIST, and consequently, will have the same width as IDC_LIST; it is important to note that, in this last case, this sizing overrides the other possible sizing operations described earlier.

Finally, one may wish to indicate one side of IDC_COMBO1 is aligned (i.e., horizontal distance of 0) with IDC_LIST (example 4).  When the attribute is CWA_LEFT or CWA_RIGHT, one accomplishes this by:

    layoutManager->SetRelativeHorzPos(IDC_COMBO1, IDC_LIST, true);

(The added last argument of "true" indicating that alignment should be performed is ignored for other attributes).  CWA_LEFT indicates the left sides of the child windows should be aligned; CWA_RIGHT indicates that the right sides are aligned.

The details of this logic are contained in the function SetSizeAndPos() in SimpleLayoutManager.cpp.

Test Dialog 5 in the demo program illustrates the above and much more.


The Bug Fix

We finish off by describing the bug fix to version 1.

To see the bug, run the original sample program (http://codecentral.borland.com/codecentral/ccWeb.exe/listing?id=19502).  Resize the main window so that it's height is small enough so that the top of the listview control is no longer visible, then resize again so that the height is approximately the original height of the window.  If you look carefully, you'll notice that the bottom of the listview control is now below the status bar at the bottom of the window.

To see the bug fixed, run the new sample program.  Resize the main window as above, but notice that the listview control, whenever visible, maintains a constant distance from the bottom side of its parent.

The problem lies in the original version of SimpleLayoutManager::DoLayout in the lines that read:

    if ((constraint->attrib & CWA_TOPBOTTOM) == CWA_TOPBOTTOM)
        // yPos doesn't change
        yHeight = nHeight - (prevHeight - rect.bottom) - rect.top;

The fix is to change this to:

      if ((constraint->attrib & CWA_TOPBOTTOM) == CWA_TOPBOTTOM)
        // xPos doesn't change...
        if (rect.bottom > rect.top)
            yHeight = nHeight - (prevHeight - rect.bottom) - rect.top;
            yHeight = nHeight - constraint->prevYDist;

The source of the SimpleLayoutManager class has changed quite a bit in version 2, so you won't find exactly those lines in the source.  If you want to patch this fix onto version 1, you'll note that you also have to add an initial value for prevYDist in the ChildWindowConstraint constructor.  The code should look something like:

         RECT rect;
    GetWindowRect(GetDlgItem(layout->hParent, id), &rect);
    MapWindowPoints(NULL, layout->hParent, (LPPOINT)&rect, 2);

    prevYDist = (layout->prevHeight - rect.bottom) + rect.top;

A similar problem occurs for the handling of CWA_LEFTRIGHT; it needs to be fixed in a corresponding manner with a value "constraint->prevXDist".


Whats next?

One problem you may encounter when designing layouts is to refer to ID values that are no longer used (that is, you have a #define for an identifier for a child window that you have deleted).  You may want to modify the code in SimpleLayoutManager.cpp to throw an exception whenever an identifier used is invalid (i.e., there is no HWND associated with the id).  Since the purpose of this exception is to help you find design problems while testing your program, you don't want to catch this exception, or at least, catch it only to print a message to tell you the problem id before exiting the program.

Although this topic is presented in the context of a Windows SDK program, the ideas are general and one should consider how to apply them to other platforms/toolkits.  In particular, in .NET, the layout options in many cases seem to be limited to "Anchor" and "Dock".  One should think about how to apply some of the ideas in the SimpleLayoutManager class to a .NET form (for example, consider what happens in the form's Resize and Layout events).

I've exhausted everything I intended to achieve with this subject.  If you think an important feature is missing or unclear, please let me know.  In particular, I would like to hear ideas on how to simplify/clarify the interface and usability of the layout manager class.

Questions, comments, and suggestions may be sent to the author at dan@nerds.com ; use the title of the article or the words "BDN Article" in the email subject.

Server Response from: ETNASC03