Revolutionize your UI

C/C++ Users Journal September, 2005

Part III: Drawing

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.

Surfaces are a new concept that complement windows. Windows should only implement their logic, while surfaces implement the drawing of the UI. This separation-of-concern increases reusability—once you implement a text_surface class, you can use it anytime your window needs to show text. Once you implement a bitmap_surface class, you can use it anytime your window needs to show one or more bitmaps, and so on. Besides that, you can combine surfaces any way you like—have a button with a gradient background (surface::gradient_fill), a bitmap on the left (surface::bitmap), and some text on the right (surface::text). Or when hovering, the text becomes bold. In the previous installment, I showed you that surfaces know how to resize themselves. Now let me show you how you can use them for drawing.

As Figure 1(a) illustrates, monthly calendars are one of the best examples to illustrate how inflexible controls are. Everything surrounded in red could be a surface. Being a surface, you could choose its size, placement, and contents. Each surface can be manipulated alone. Now Figure 1(b) (and much more) is possible!

Drawing Basics

Each window has a corresponding surface. This surface can have child surfaces, which in turn can have children of their own, and so

on. Clearly surfaces are hierarchical. When a window needs to be drawn, it simply tells its corresponding surface to draw. The surface draws itself, then draws all its children according to their index. Each surface has an index_ within_parent; children surfaces are drawn in ascending order (first surface with index 0, then surface with index 1, and so on).

Each surface has a name. This makes it easy to find surfaces and aids scripting (just try to imagine how you could do scripting if surfaces weren't addressed by name!). Names are unique within siblings.

Scripting Away

The true power of surfaces comes to light when combined with reflection. I've demonstrated in previous installments that resizing can happen for any surface property that has been registered, using reflection::register_ reflect.

This is not your average reflection. You can set relations between surface properties, and they are updated automatically!

For starters, Listing 1 is what the usual script for a dialog looks like. First, you can define some macros, which you can use in the later sections. Then you define some templates, and next you define the surfaces, somewhat similar to C++ declarations:

surface_type surface_name [ { child1,      child2,... } ];

Here, you define a hierarchy of surfaces that is guaranteed to exist. In my example, the ok surface has the content, bg, is_focused children. Any surface can choose to create other children surfaces, but you're guaranteed to have at least those surfaces that were scripted.

Then comes the properties section, where you set surface properties' relations. A relation can be:

The latter is a powerful tool. In Listing 1, whenever the background of the dialog changes, the bg.from_col and bg.to_col properties automatically update—no coding required.

When creating relations, you can use all the powerful tools reflection provides—calling functions and/or using complex expressions (Listing 2). Last comes the layout section, which I explained in the previous column.

Listing 1 yields something similar to Figure 2.

In addition to the surfaces you manually script in the surfaces section, some surfaces are created by default—those that correspond to each control from the dialog. They are assigned straightforward names—the name you would use in code, without the m_ prefix. Say you create an IDC_user _name edit box using the Resource Editor. The Resource Splitter [1] creates the m_ user_name variable. Then, the win32gui library creates the user_name surface. By default, this surface simply does the default drawing of the control. You can override this in two ways:

Script Templates

The templates section is the most powerful of all, giving you the same feeling that C++ templates give you over plain-old classes. Use it to give a consistent look-and-feel of your application. In short, templates let you specify defaults for windows and surfaces.

Say, for example, you want to give all buttons a bluish background. You can, of course, manually set each button's background to blue (a tedious and error-prone process), or you can say something like this:

window button { 
  surfaces { fill bg }
  bg.col = rgb(0,0,150);
}

The best thing is that if you change your mind and want something else, you only have to change one or two lines of script.

Here are the things you can fit inside the templates section (in any order):

window type_of_window {
  [surfaces { 
    surface_type1 name1; 
    [ surface_type2 name2 [;...]]}]
  [property1 [ = value1]]
  [property2 [ = value2]]
  [...]
  ]
}
surface type_of_surface {
  [surfaces { 
    surface_type1 name1; 
    [ surface_type2 name2 [;...]]}]
  [property1 [ = value1]]
  [property2 [ = value2]]
  [...]

 
 ]
}
alias surface original_surface_type      alias_type;

For a type of window (a button, for example), you can enforce what child (and grandchild, and so on) surfaces it contains, and set some of their properties.

In the same manner, for a type of surface, you can enforce what child (or grandchild, and so on) surfaces it contains, and set some of their properties.

Sometimes you might find this a little bit inflexible. What if you need to have some text surfaces shown in bold blue, while others in italic black? Aliases come to save the day. Simply create an alias of the surface type, and then set each type's properties alone—Listings 3(a) and 3(b). When you need aliases, I recommend you go with the 3(b) way—create two or more aliases and modify them, instead of the direct type. This clearly states why you would need each alias. Once you've created an alias, you can use it as a surface type.

Listing 4 shows templates in action. If you were to create a login dialog based on this template, it could look like Figure 3.

As an additional benefit, templates and macros are inherited. There's a root where you can set the global templates and macros (inherited everywhere). Each dialog inherits the parent dialog's templates and macros. Just think of the consistency and reusability this all brings.

You edit the roots' script and each dialog's script using the Resource Splitter (Figure 4).

Extra Logic

The window does its logic, and surfaces draw the UI. This works, but most of the time you need to keep the behavior (windows) and look-and-feel (surfaces) in sync: Consider a button that has a surface called is_focused, which is shown only when the button has focus. Once the button gains focus, it must show the is_focused surface; once it loses focus, it must hide it.

Usually each window needs a bit of such extra logic (more specifically, an event handler). Fortunately, you have advanced subclassing to help you. Thus, the window can implement its own behavior, and you can implement the extra logic in parallel—completely transparent to the window itself. I call these event handlers surface::sync_handlers [2].

Much like advanced subclassing, a sync_handler can be:

template<
  class self,
  class impl, 
  s_type = sync_automatic
  h_type = events_before,
  int idx = 0,
struct sync_handler;

The parameters are:

Listing 5 shows you how simple it is to implement an automatic sync_handler to update the is_focused surface. If you want to make it a manual sync_handler, try Listing 6.

Do note that you can have multiple sync_handlers for a given window. For example, you could have a sync handler that updates the is_focused surface, and one that updates the background, when the mouse is hovering.

To address sync_handlers in script, in the surfaces section, do this:

//    for a surface
// actually applies to the window
// this surface belongs to
sync(surf_name,  hdler_name);

//    for a window
sync (hdler_name);

For instance, to update the background of u_name when the mouse is hovering, you could say: sync(u_name,hover_cng_bg); (assuming there's a hover_change_bg manual sync handler). You can use them in the templates section, in the window or surface constructs, after surfaces have been defined, and before defining the properties; see Listing 7.

Anchors

I've implemented surface::html_text, which is a wrapper over the old lite_html control. Nice, but you've asked for bitmaps inside the text as well—a reasonable request. But why should I reinvent the wheel when there's already a surface::bitmap? So whenever a bitmap or something else complex should be drawn, I simply leave an anchor—an empty space—and go on. Later, another surface draws in the space provided by the anchor. Thus, you can freely mix HTML text with whatever other complex things you might want to draw (gradient text, overlaid bitmaps, and so on) [3].

Anchors are best not only when mixing complex surfaces (HTML text with bitmaps, and so on), but when you need to print intermixed data, some of it being variable; see Figure 5.

The one that creates the anchor, has two choices. It creates:

Creating an anchor in code is easy; see Listing 8. Linking to an anchor is easy as well:

// in script
price_usd.anchor = "price";
// in code 
// (price_use is a surface)
price_usd.anchor("price");

Using Surfaces In Code

So far, I've shown you mainly how to use surfaces in script. That was no coincidence—you should use surfaces mainly in scripting. When used in code, there are a few things you should pay attention to. I'll describe them in detail in the next installment. For now, I present a crash course.

You always deal with surfaces indirectly via smart pointers, specifically, the ptr<> class. ptr<> is to surfaces, what wnd<> is for windows. In the same way that there's a window_base for windows, there's a surface_base for surfaces. Casting works in the same way as for windows. Listing 9 presents a few examples.

Again, each window has a corresponding surface. To find it, simply use the wnd_surface(window) function:

wnd<> w = ...;
ptr<> surf = wnd_surface(w);

Once you've found the surface corresponding to the window, you'll usually want to find one of its descendants. It's not a very good idea to try to address it directly because surfaces could be moved around as your program matures. Just think of having a button containing only text, which in time could get more complex (Listing 10). If you were to say:

wnd<> w = ...;
ptr<> surf = wnd_surface(w);
ptr<text> t = surf->child("txt");

it would simply cause an exception because txt is no longer a direct child of surf (it's in fact a child of content, which is a child of surf).

Thus, I've provided no child method for surfaces. The only method of finding a descendant is by using the find_surface and try_find_surface functions. They work much like the find_wnd/try_find_wnd functions. Take a look at them in action (Listing 11).

Coming Up

In the next installment, I'll go deeper into surfaces in code. I'll also deal with the creation of surfaces in code and how to create your own surface class.

I've implemented a few sync handlers, but there's more to do. So, if you want to do some hard-core programming, just send me some e-mail.

Notes

  1. [1] http://www.torjo.com/win32gui/doc/resource_splitter.html.
  2. [2] Under the hood, they're event handlers used in advanced subclassing.
  3. [3] You could argue that you can use the existing property's relations instead of anchors. For certain types of surfaces, you know where an anchor is, only when you do the actual drawing. You could, when encountering an anchor, do the calculations and update the property's relations accordingly. But this gets very tedious and error prone.
  4. [4] This rectangle's starting point might be slightly different than the original starting point. In case the width and/or height exceed the original surface, the drawing surface has the right to change the starting point.