Revolutionize Your UI

C/C++ Users Journal July, 2005

Part II: Resizing

By John Torjo

John Torjo is a freelancer and consultant who specializes in C++, generic programming, and streams. He can be reached at john@torjo.com.

When it comes to resizing UIs, windows are way too inflexible. Thus, this month I introduce "surfaces," a concept that complements windows by providing highly customizable UIs that support resizing.

A window never draws itself—it just notifies its corresponding surfaces (every window has at least one surface). The surfaces take appropriate action, which might involve telling other surfaces to draw as well. The same goes for resizing. The window notifies its corresponding surfaces, which in turn notify the layout manager, and the proper windows and/or surfaces are resized.

Placing different controls on a dialog is similar to having different member data in a C++ class—they help you implement a concept. When you write business logic code, however, you don't really care about the dialog's appearance. You know that when an Add button is clicked, you need to add a new employee. Or when the Login button is clicked, you read the texts of the user_name and password edit boxes and perform the login. You do this regardless of how the controls are visually presented to users.

In short, you should let the windows implement their logic and the surfaces draw the UI. You'll find this very useful. Each window exposes what needs to be drawn, while surfaces take care of doing it. This gives you lots of flexibility, as checkboxes publish the:

For each of these, you can choose from existing surfaces (transparent, text, HTML text, bitmap, border, line, and the like). You can even combine them; for instance, to implement a bitmap+text button, just have its content be a transparent surface with two children: a bitmap and text surface. Just remember that surfaces are hierarchical—a window can, and usually does, contain multiple surfaces, which in turn can contain children surfaces; see Figure 1.

Layout Managers

When it comes to dialogs, once you have the child windows and surfaces created, you need to specify relationships between them (other than parent-child relationships). This is where reflection and scripting kick in. For instance, a relationship could look like:

u_name.bg = parent.bg + rgb(0,0,5);

Whenever the dialog background changes in this scenario, the background of the u_name surface is computed and automatically updated. In this article, I focus only on resizability, so forget about windows for now. Focus only on surfaces—that is, if a surface contains one window, it knows how to resize it [1].

From all existing surface relationships from a dialog, those that relate to sizing are automatically published to the layout manager (for that dialog). Whenever a surface's area gets changed, the layout manager resizes any dependent surfaces, and redraws what's needed (all behind the scenes, of course). Note that a window can have multiple layout managers, and can switch between them at runtime; see Figures 2(a) and 2(b) [2].

For instance, here's how to implement a dialog with two equal panes, a left pane (a) and right pane (b):

a.rect.width = parent.rect.width / 2;
a.rect.height = parent.rect.height;
b.rect.left = a.rect.width;
b.rect.width = a.rect.width;
b.rect.height = parent.rect.height;

You can also script a horizontal splitter, which splits two surfaces (Listing 1).

Because writing this by hand is error prone, I invented macros that work much like C++ macros. Assume the equal_panes macro is defined somewhere:

// equivalent of above
#equal_panes(a, b);

When scripting nontrivial dialogs, you'll usually have quite a few intermediary surfaces, which are meant only to hold other surfaces and dictate their layout. However, most of the time, it's best to go with the easiest solution. Check out Figure 3 and Listing 2: The only surfaces that need resizing are user_name, password, do_login, and cancel buttons.

Length Units

When designing and implementing this resizing technique, I've been partially influenced by Cascading Style Sheets (CSS). Thus, I allow for multiple units for measuring length:

Thus, when your controls need fixed coordinates, you can use any of the above, as in this example:

a.rect.left = 0.5in;
b.rect.left = a.rect.left.rect + 2mm;
b.rect.width = 30px;

Just make sure that there's no space between the number and its unit type:

// wrong!!!
a.rect.left = 0.5 in;

Grammar

I tweaked the script grammar to allow complex relations between surfaces. You'll use this mainly for resizing. For instance:

a.left = {
  if (parent.width > 1000) 
       then (parent.width - 1000) / 2;
  else 0;
}

When specifying a relation, use any of:

var.prop = statement

where "statement" is:

statement = expression; 
statement = { statement [statement] }
statement = if (expression) 
  then statement
 [else statement]

Each "statement" is executed. If it's an expression, it is computed and considered a return value. If it's an if statement and the tested expression is true, the expression after it is considered a return value. If the tested expression is false and there's an else, that expression is considered a return value. Every statement that yields a return value overrides the previous return value (the only statement that does not yield a return value is an if with no else, when its tested expression is false). The return value is assigned to var.prop.

Take a look at Listing 3. If parent.width is 1705, then the following things happen: parent.width, parent.height is printed, "between" is printed, and 1500 is assigned to a.left.

Macros

Again, writing sizing relationships by hand can be complex and error-prone. However, there are templates that usually tend to repeat themselves, such as: dock control a on the left of control b, dock control c on the bottom of control d, and so on. That's why I turned to macros—to automate this process. In script, before relationships are defined, there is a section called macros:

macros {
  ...
}

Each macro is similar to its C++ macro counterpart. Every macro starts with a pound sign (#). Here's how a macro is defined (in the macros section):

#dock_left(dest,src,extra_dist) {
  dest.rect.left = src.rect.left + src.rect.width + extra_dist;
}

Remember that when you use it in scripting, you do need to start the macro with pound:

#dock_left(a2, a1,10px)

Listing 4 is the equivalent of Listing 2 written using macros. Similar to C++, there's also a preprocessing phase that translates all macros into scripting code.

Hide and Seek

Surfaces can be hidden. When a surface is hidden, its width and height become zero (the old width and height are saved, to be used when the surface is shown again). Make sure to treat the x-axis and y-axis differently, and don't assume too much. Take a look at Figure 4 and script it so that even when A is hidden, B moves to the right:

// good
b.rect.left = a.rect.left + a.rect.width;
b.rect.height = parent.rect.height;
// bad
b.rect.left = a.rect.left + a.rect.width;
b.rect.height = a.rect.height;

In the latter case, when a is hidden, b's height becomes zero, which hides b as well.

To make things easier, I added two rules:

Thus, to accomplish Figure 4, you can only say:

#dock_left(b,a,0)

If so, when a is hidden, b takes up all parent's space. If you don't wish that, simply assign a fixed width:

b.rect.width = 2in;

Adding/Deleting Windows

The above works just fine for dialogs. They usually have a fixed set of controls, and you seldom add/delete controls to/from a dialog (eventually, sometimes you hide a few).

However, in addition to those, there are container windows—that is, containers for other windows. Their job is to host other windows and to manage how they are sized. Imagine a window that hosts all its children in a table. As more children are added and/or deleted, the table is automatically resized (Figure 5). Behind the scenes, a shape is automatically assigned to hold each newly added window. However, some shape relations might change, due to the addition or deletion of a window.

To accommodate this, there's an adddel manager in addition to the layout manager. Whenever a window is created or destroyed, its parent is notified. The parent, in turn, notifies the adddel manager, which updates the shape relations and notifies the layout manager of the changes (in turn, the layout manager redraws the affected surfaces).

Here's what to remember when dealing with container windows:

Why Surfaces?

I could have made a system that would take care of resizing windows only—not surfaces—but I chose not to. There were several reasons for doing so:

Therefore, I chose to simply implement resizing directly on surfaces.

Implementation Details

For a dialog, where are all the relationships kept? The answer is simple—in your application's resource file (win32gui.rc2). For each dialog, there is a string (similar to Listing 5) that holds all these relationships. At this time, you'll use the Resource Splitter to edit this string, much like you edit a .css file manually. (In the future, I'll create a WYSIWYG tool that lets you edit surface properties.)

Error Handling

Every now and then, you can insert a bug in scripting. The problem is that these bugs are much harder to trace than with, say, C++. While in the future there might be better checking (while parsing the scripting code), at this time, there isn't.

Possible errors include:

In case an error occurs, you can catch it in debug mode (your code breaks at the offending line) or in release mode (an exception is thrown).

In release mode, the exception is caught by win32gui, and you'll get a chance to handle it yourself. By default, nothing happens. There might be a visual cue, such as one or more windows out of place or not redrawn. What you can reasonably do now is to simply log the error message. To handle such an exception, you should register an exception function:

register_reflection_error_handler(func); 

while func has this prototype:

void func(const std::string& err);

Just Testing

Surfaces are not fully implemented yet. To give you a feel for how surface resizing works, check out the samples/surfaces/resizing example, which lets you script a few surfaces. Then you can resize the main window and see how those surfaces behave. In Figure 6, each color represents the shape's level (how many parents it has). Also, the program visually shows the name of each shape. In the next installment, I show you how windows expose surfaces, how surfaces draw themselves, surface anchors, and how you can enhance your user's visual experience.

Notes

  1. [1] A surface can contain children surfaces, and it can contain at most one window. If a surface contains a window, as the surface gets resized, it automatically resizes the corresponding window.
  2. [2] Another reason for having layout managers is the complexities of resize relationships. Usually, the parent dictates how children get resized (each child rectangle relates to its parent and/or siblings). But sometimes the children can dictate the parent's rectangle. Imagine a button that contains a bitmap and text: Based on the height of the text (the font and/or number of text lines) and the height of the bitmap, you compute the height of the button.