Revolutionize Your UI

C/C++ Users Journal November, 2005

Part IV: Manipulating surfaces in code

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 UI, you'd like to manipulate a control just like you manipulate a dialog at design time—create and move the subwidgets any way you like, set some of their properties, specify how they should be resized, and let it run! Well, tough luck. They just don't work that way: Controls are very inflexible when it comes to drawing their UI. You can set a control's background—but what if you want a gradient background? You're on your own.

That is, unless you use surfaces. Windows just need to implement their logic, and let the surfaces draw the UI.

Who's Drawing The Windows?

Surfaces are hierarchical: each surface can have children, grandchildren, and so on. A surface can either belong to a window or to no window at all. When a window is to be drawn, it simply gets its corresponding surface (by calling wnd_surface(some_wnd);) and asks it to draw. The surface will draw itself and all its descendants.

Each type of window (button, edit box, combo box, and so on) defines a minimal surface interface. This interface specifies which surfaces are guaranteed to exist for this type of window. You can create and manipulate these surfaces in your script. If you don't create them, they are created by default.

For instance, for a normal (not push-like) checkbox, the minimal interface is:

- "content"
  - "mark"
  - "value"

In other words, the surface corresponding to the checkbox must have a descendant called content. The content surface must have two descendants, called mark and value; see Figure 1.

You can twist the checkbox appearance's to suit your needs. For instance, Listing 1 will generate Figure 2. Yes, it's gradient text over gradient fill!

Again, each type of window defines its minimal interface. Given this minimal interface, a sync_handler can update window's appearance when certain events happen—it updates the surface that corresponds to this window and/or its descendants. For a checkbox, the mark surface needs to be set to on/off each time the user clicks on the checkbox.

sync_handlers are the glue between windows and surfaces. Besides window-specific sync handlers, you can have general sync handlers. I've implemented a focus_sync_handler, which manages a surface called focus. When a window gets focus, this surface is made visible; when the window loses focus, it's hidden (see Listing 2).

So far, here are the surfaces I've implemented:

and the sync_handlers for these windows:

You can find out more about them after downloading the library from http://www.torjo .com/win32gui/.

Accessing Surfaces

First of all, the code dealing with surfaces is in the win32::gui::draw namespace. Don't forget to:

#include <win32gui/draw/surfaces/surface.hpp>

Each surface class that is provided by win32gui is found in the win32::gui::draw::surface namespace.

Usually, surfaces are created while the script is parsed (the surfaces {} / templates {} sections). Each surface class is identified by a unique name at runtime (more on that later).

Note that you never access a surface directly. You access it indirectly by using the win32::gui::draw::ptr<> class. The ptr<> class is for surfaces what wnd<> is for windows. It allows safe access to surfaces by implementing a smart pointer internally—you don't want any dangling surface pointers, and you're not going to get any.

Creating surfaces in code is easy. Use either:

ptr<> create_surface(const string &   	name);

or

template<class surf_type>
  ptr<surf_type> create_surface();

After you've created a surface, it has no parent. If you want to make it a child of an existing surface, just assign it a name, and then call add_if_not_already_in on its parent; see Listing 3. Note that for a surface, all its children are named (object_name() property), and you can't have two children with the same name.

Destroying a surface is easy, too:

surf->destroy();

This simply removes it from its parent (if any). If its reference count drops to zero, it is destroyed along with all its descendants. Otherwise, it becomes invisible until its destruction (when there are no more pointers to it).

To find the surface that corresponds to a window, use:

ptr<> surf = wnd_surface(some_wnd);

If there's no surface corresponding to this window, this will return null.

If you have a surface object, you'll notice that there's no child(name) function to get a reference to one of its children. This is intentional—surfaces are flexible. Imagine Figure 3(a) turns into 3(b), and you have this code:

wnd_surface(w)->child("txt")
  ->set("text", "oops!");

For 3(a), this code works. As you make your app more user friendly, 3(a) could turn into 3(b) and this code will fail; thus, no child() function. Instead, I've provided these powerful functions:

find_surface(name, [filter]);
try_find_surface(name, [filter]);

find_surface<surf_type>(name, 
  [filter]);
try_find_surface<surf_type>(name, 
  [filter]);

They all return the first surface that meets these criteria:

They search the children first, the grandchildren second, then grand-grandchildren, and so on. The first function throws in case the surface is not found, while the second returns null.

The third is the same as the first, only that the surface must be of surf_type type, or a type that derives from it. The fourth is the same as the second, only that the surface must be of surf_type type, or a type that derives from it (if nothing is found, it returns null).

You'll see quite a resemblance with find_wnd/try_find_wnd functions. Finally, casting works just like it does for windows—use the cast<> function; see Listing 4.

The First Surface Class

Let's create a simple surface class. Here are the steps:

  1. Derive from surface_<>.
  2. Implement surface_type(). You specify a unique name used to create an instance of this class at runtime. The easiest way is to name it the same as the class itself.
  3. (optional) Add some reflectable properties (that can be used in the script).
  4. (optional) Register those properties, using WIN32GUI_REGISTER_REFLECT. It is best if you have a separate file where you register all of your reflectable properties.
  5. Implement the draw() function.
  6. Your class should be default constructible. You should only provide a default constructor. Don't provide any other constructors because they won't be used anyway.

Listing 5 shows how to implement a border surface. Notice that the usage of WIN32GUI_REGISTER_REFLECT is within the anonymous namespace—it expands to register_reflect r##__LINE__, creating a variable whose construction does the registering. You want this to happen in the anonymous namespace, to avoid defining the same variable twice (two WIN32GUI_REGISTER_REFLECTs in two separate files on the same line). The draw function has three arguments:

Here's what you need to remember when implementing draw():

Surface Gotchas

Surfaces can be intimidating at first. So here are a few things to watch out for:

A surface can never draw outside the parent's rectangle. When drawing, besides the rect property, each surface has a constrained_rect property. This computes the rectangle that the surface is allowed to draw on based on its ascendants. To put it simply:

Surfaces are minimal: Don't draw more than you need. For instance, you should never draw a surface's background. If you want a background, just add (in the script) one background surface, such as fill or gradient_fill. By default, a surface is transparent. Only what you draw on it is shown on screen.

Whenever a surface's (reflectable) property is changed, the surface is invalidated and will be redrawn right away. This happens automatically: You don't need to write any code. For example, if you change the border's color, the border will automatically be redrawn. This is pretty neat—you never have to worry about marking a surface as "Hey, I need to be painted."

Once a surface is hidden, every descendant is hidden as well.

When you draw a surface on screen:

Windows can be enabled/disabled, and some—but not all—show this visually. Same goes for surfaces. For some, it does not make sense to be drawn disabled. The easiest example is the gradient_fill surface. What would disabled mean for it? Thus, if a surface can be enabled/disabled, it exposes the Boolean property enabled. Set this to False, and you've disabled it. text, html_text, gradient_text, bitmap—they all expose it.

Setting Surface Properties

A surface class is a reflectable class; thus, the simplest way to set one of its properties is by calling set on it:

ptr<> surf = ...;
surf->set("text", "Cool!");

This works like a charm, but you can do much better. The simplest example that comes to mind is disabling a window. If so, the surface corresponding to this window and all its descendants must be disabled. You could walk through all its descendants and disable those that contain an enabled property, but this would be tedious and error prone.

Or, you can use the propagate_set function:

surf->propagate_set(
  name, value, [[propagate_type, [filter]);
surf->propagate_set<surf_type>(
  name, value, [[propagate_type, [filter]);

This will propagate the property, depending on propagate_type:

As an additional filter, you can specify surf_type. set is called only on surface classes that are of type surf_type or derive from it.

It's this simple:

// set text to s & its descendants
s->propagate_set("text", "Cool!");
// disable s & its descendants
s->propagate_set("enabled", "0");

If you have a window, you can call propagate_set on it as well:

propagate_set( w,
  name, value, [[propagate_type, [filter]);
propagate_set<surf_type>( w,
  name, value, [[propagate_type, [filter]);

For instance:

wnd<> w = ...;
propagate_set(w, "text", "Cool!");

which is equivalent to:

wnd<> w = ...;
if ( ptr<> s = wnd_surface(w))
  s->propagate_set("text", "Cool!");

Scrolling

Simply put, scrolling isn't easy. First of all, you expect scrolling to occur automatically when needed. For this to happen, I had to twist the library a little bit.

Each surface exposes whether it's scrollable or not. It does this by specifying a virtual space [1]:

// horiz scroll
surf-> scroll_horiz(len);
// vert scroll
surf-> scroll_vert(len);

By default, the aforementioned are both set to 0cm. Set them to a positive value to specify scrolling. Set them to 0cm to specify there's no scrolling in a given direction:

// no horiz scrolling
surf->set("scroll_horiz","0cm");
// vert scroll up to 15cm
surf->set("scroll_vert","15cm");

If there's scrolling, you can manipulate the current scroll position:

// set current horiz pos
surf->horiz_pos(len);
// set current vert pos
surf->vert_pos(len);

When the surface is drawn, if it's scrollable, it will take into account horiz_pos() and/or vert_pos() [2].

When doing the drawing, each surface that has one child called horiz_scroll, one called vert_scroll, and one called content holds the scrollbars (see Figure 4). I call this the scroll holder (SH). The SH looks at all content's descendants. Based on their virtual space [1], it will compute its own virtual space. Then, based on its horiz_pos() and vert_pos() of content, it will compute each descendant's horiz_pos() and vert_pos(). Finally, it will do the drawing.

To make things easy, a window is considered scrollable if its corresponding surface has a child called content and is considered scrollable. If you don't want this, set (in the script) its corresponding surface's scrollable property to 0. When a window is scrollable, the horiz_scroll and vert_scroll surfaces are created by default, as children of the window's corresponding surface.

In case you implement a sync_handler for a window and want this window to be scrollable, make sure that this window's corresponding surface contains one child surface called content. The other surfaces that make up the minimal surface interface should descend from this. If you take a look at my code, you'll see I've followed this thoroughly.

Next time you look at an application's UI, just think of how easily most of its controls could be implemented using surfaces; the same goes for third-party controls.

Are surfaces cool? Have trouble using them? Want to do hard-core programming? Just drop me an e-mail. Volunteers and feedback are most welcome.

Notes

  1. Virtual space—the space the surface thinks it would take for it to be shown in full.
  2. Therefore, if you're developing a surface class and want it to be scrollable, you'll have to implement it yourself!
CUJ