C/C++ Users Journal May, 2005
There's no denying it. The so-called "standard controls" have let us down because they're simply not as customizable as we need them to be. Sure, they're nicebut they could be so much nicer. For instance, have you seen UIs like Figure 1 or Figure 2 in any application (except for your favorite Internet browser)? Most likely not, because they're difficult to implement and maintain.
In short, when it comes to UIs, controls are highly inflexible. Sure, the standard button [1] lets you set text, images, and background images. But what if you want two images, one on the right another one on the left? What if you want part of the text to be bold or of a different color? Or, a cell from a list control to be bigger than the rest? Can you create a list view and make the first column have 30 percent of the full width?
The reason why controls are so inflexible is because visual appearance is too tightly coupled with the control's behavior. While you can set the button's text, this is useless to the button's core behavior. All a button needs to know is to execute an action when clicked, and know if the left mouse button is pressed/depressed. All an edit box needs to know is its text and current selection (the edit box does not need to know about the text's font or color). And all a checkbox needs to know is whether it's selected or not. The rest is visual appearancewhich can, and should, be highly customizable. In a nutshell, the visual appearance should be separated from control behavior; thus, it should be implemented elsewhere.
Every control should expose a few surfaces that it can show visually. How each surface draws itself, where it draws itself, and what it contains is not the control's business. The only thing your control should focus on is its own behavior. Design your controls this way, and you'll have great flexibility and skinnable applications will greatly improve.
For example, assume a button has these surfaces:
The background can be transparent, the content can be one or more images with or without accompanying (perhaps HTML) text, is_pushed can be a different bitmap, is_hovering can be an overlay image or simply a border, and so on. The only logic the button needs is to inform its surfaces when they should be drawn. You'll be able to create and, especially, reuse lots of buttons.
Technically, a surface is something that knows how to draw itself on a device. Surfaces are hierarchical and can have children.
Having this, I built two loosely coupled modulesone that takes care of resizing surfaces, as the window (usually a dialog) they sit on is resized, and the other that takes care of drawing surfaces.
The need for reflection is similar to the need for a resource (.rc) file. You can create surfaces and set their properties at runtime. But more importantly, you can create relations between different surfaces. For example:
// 20 percent from parent pict.width = parent.width * 20/100;
When designing surfaces and their reflection, I've been influenced by discussions on the Boost-developers list (especially those started by Alan Gutierez in December 2004 and January 2005), HTML Cascading Style Sheets (CSS), and Avalon, Microsoft's next-generation GUI [2]. So, I start by defining this nifty reflection system [2], then show how you benefit from the surface/window decoupling. The system I present here is not your average reflection system. Because I've tweaked it for GUIs, you'll be able to write complex components with a minimum amount of scripting. Listing 1 gives you a taste of how you can script a splitter, where the left and right panes' width is a percentage of the parent's width.
Properties are at the core of the reflection mechanism. You can have get/set (default), get-only, or set-only properties. If you have a property of type A, unless this type is an enumeration, this property needs to have operators >> and/or << defined [3].
Your reflectable class will derive from reflectable_properties_object<your_class>. Registration of reflectable properties is as simple and nonintrusive as possibleyou register a certain property in a source file (usually your class's source file), using register_reflect, as in Listing 2. You can register the property, even though the getter and setter have the same name. A templated constructor takes care of overloading issues (Listing 3).
register_reflect is a smart class. It can register properties, no matter their kind: get-only, set-only, or get/set (Listing 4). Having said that, your properties do need a little discipline. A property can be set in two waysby scripting or programmatically. In both cases, I need to know about it to update any other objects/properties that depend on it:
left_pane.width = split.width * 20/100;
If split.width changes, the library needs to know about it in order to update the left pane's width. If a property gets modified by scripting, no problem, because I parse the script and update any other dependent properties. But, if the property gets modified programmatically, who's going to tell the library about it? This needs to happen automatically, otherwise it will be error prone.
This code is a no-no:
struct point {
long left, top;
};
To cope with this, whenever a property is set, it needs to notify the library. Listing 5 shows how. You do need to say notify(&me::left) instead of simply saying notify(), since the library needs to know which property has changed; otherwise, it would need to cache each property and comparean unneeded complication.
There are three types of properties:
Inheritance works. You can inherit from a reflectable object. Just remember to derive again from reflectable_properties_object<your_class>, and this derivation needs to come last, or reflection won't work properly; see Listing 7.
Aggregation works as well, too. When an aggregated object is of a type that is already reflectable, the library realizes this by default. Thus, it won't require the type to have any operators >> or << defined because the type has already published its properties. Accessing the aggregated properties in scripting is straightforward; see Listing 8.
Enumerations are a little more difficult to integrate in the reflection mechanism because you'll want each enumerated value to have a user-friendly string correspondence. Creating such a correspondence might not be such a big deal. The problem consists of maintaining it. Enumerations do changenew enumerated values are added, existing enumerated values are changed, and so on. Therefore, you'll want the enumeration and its string correspondence's array to sit next to each other. There are several mechanisms for providing this, and Paul Mensonides, the C-macro guru, has shown a few [4]. I chose a simpler approach; see Listing 9.
Notice that registering enumerations is easy and consistent with the rest of the library. Still, there are a few things you might have missed on the first look:
The registration mechanism lets you register global functions. When you register such a function, all of its arguments and its result type must have both operators >> and << overloaded. In fact, this is how any such function gets called: All its arguments are converted to string and passed to the function. Then, the nonconst arguments, if any, are updated, and the result is returned (as a string).
Note that the parameters must all be const or nonconst reference (not passed by value). The registering is exactly the same:
register_reflect r("max", &max_val);
I have not implemented script function overloading (that is, allow multiple scripting functions to have the same name). At this time, I do not think this is necessary.
Arrays can be passed to functionsbut const-only. The function is not allowed to modify the array, only to access it. You can also have arrays of reflectable_properties_objects. When passing an array to a function, you have two options:
// [1] pass the array itself
a.width { max(b.widths) }
// [2] in case it's a array of pointers,
// you can pass one of their properties
parent.width { max(children.width) }
parent.height { sum(children.height) }
Declaring arrays is simple, but for now you'll have to limit yourself to reflectable_array arrays, and for arrays of pointers, to reflectable_ptr_array; see Listing 10. Arrays, programmatically, do offer extra functions such as size, add, del, and at. You'll find them all documented when you download the latest win32gui [5].
As with any scripting language, there are pitfalls. Typos will cost you greatly. Referencing a property that does not exist, or an object that does not exist, will trigger an exception. There might be an extra validation system in the future, but at this time, there is not.
Manipulating arrays through scripting is often useful. However, I recommend that these arrays are small. Any function that is called upon an array will cause each element to be transformed into a string first, and for large arrays, might cause bottlenecks. Thus, use with care.
In the next installment, I will examine surfaces and what they can do for you. And in the long run, I'll delve into AvalonMicrosoft's next-generation GUI.