Download source files - 1.8 Mb
Introduction
The Visual Component Framework was inspired by the ease of use of
environments like NeXTStep's Interface Builder, Java IDE's like JBuilder, Visual
J++, and Borland's Delphi and C++ Builder. I wanted a generic C++ class
framework I could use to build app's quickly and visually (when designing UIs),
as well as having the core of the framework be as cross platform as possible.
This article will discuss some of the issues I ran into, and how I attempted to
solve them. The Visual Component Framework is an Open Source project, so feel
free to grab and use it if you think it might be useful. If you're really adventuresome
you can volunteer to help develop it, making it even better, especially in tying
it into the VC++ environment as an add-in. For more information on either
the project, helping out, or just browsing the Doc++ generated documentation
please go to the VCF project on Source Forge here,
or the project website here. The code
is available from CVS (follow the how-to here for setting up CVS on windows), or
the a zip file here (it's around 1.8 MB -
this includes the XML libs for Xerces - an XML parser. Check the site for the latest
version).
The Visual Component Framework (VCF) is divided into three DLL's, the
FoundationKit, the GraphicsKit, and the ApplicationKit. This article discusses
the FoundationKit, the heart of the VCF. The FoundationKit provides the basic
core classes and the advanced RTTI capabilities of the framework. Early on I
knew I needed some sort of RTTI, or reflection, similar to what is provided in
Java or ObjectPascal. This was a requirement because visual design environments
need to a way to expose a component's properties and events, and to provide a
way to edit them in a consistent, customizable and extendable manner. With this
in mind the framework allows the developer to query an object for its Class
,
which in turn provides access to the class's name, superclass, Properties,
Events and Methods. To achieve this the framework makes heavy use of templates
and STL. Class
is an abstract base class that template classes
device from. Classes provide the following information:
- the name of the
Class
- this is stored in a member variable rather than
relying on typeid(...).name()
to retrieve the class name. Not all compiler's
support the typeid(...).name()
function.
- the Class ID - this represents a UUID (Universally Unique IDentifier) for the class. This will prove
useful when distributed features creep in.
- the ability to discover all the properties of a
Class
at runtime. A
property is defined as some class attribute that is provided access to via
getter and setter methods. Properties can be primitive types (int, long
double, etc), Object derived types, or enums. Properties can also be
collections of other objects.
- retrieving the super class of the class.
- the ability to create a new instance of the class the
Class
object
represents. This of course assumes a default constructor is available.
In order for the RTTI to work in the Framework developers of derived classes
must do three things for their classes to participate in the Framework. Failure
to implement these steps will mean their classes will not have correct RTTI. A
series of macros (defined in ClassInfo.h) have been written to make this easier.
The first step is (obviously) making sure that your class is derived from a
Framework object. For example:
Next you should define a class id (as a string) for your class. On Windows I use guidgen.exe to create
UUIDs. The define should look
something like this:
The next step is to add to macros to your class definition (.h/.hpp file). These
are required for the basic RTTI info (class-super class relation ships) and to
make sure you'll inherit any properties of your super class. For example:
The macro takes the class type-id, the string to use as the class name, the
string that represents the classes supper class, and a string that represents
the class id (where the class id is a string representation of a UUID). What the macros end up creating is a public nested class used to register your class that you're
writing. The above macros generate the following inline code for the developer
of the
Foo
class.
The isClassRegistered()
method checks the ClassRegistry
to see if the class is already registered, if it is not then a new entry in the ClassRegistry
is made. Classes are stored in a singleton ClassRegistry
object,
which contains a single Class
instance for each registered class
type, thus multiple object instance's share a Class
instance. To do
this the framework has an abstract class defined (Class
), and two template
classes TypedClass
and TypedAbstractClass.
The
template parameter specified in the TypedClass
and TypedAbstractClass
are used to safely allow class comparisons and to allow the creation of an
object at run time (this feature is only supported by TypedClass
).
The two template classes are necessary because it is possible to have abstract
classes in the framework that by definition cannot be instantiated, but need to
in the class hierarchy, since all Class
instances have a getSuperClass()
method, allowing the programmer to walk the class hierarchy at runtime.
The ClassRegistry
keeps all the classes in a map, and each time a new Class
instance is registered, the super class is found and all of it's properties are
copied over to the newly registered instance, making sure that derived classes
"inherit" the properties and events of their super class.
To
add more detailed RTTI you can add properties, events, and methods. An example
of this can be found in the Component class declaration:
This demonstrates exposing three properties and two events. Properties allow
you to dynamically discover the attributes of an object at runtime. A Property
holds a method pointer to an accessor method (or "get" method), and
optionally a method pointer to a mutator or "set" method. In addition
a Property
has display name, and a display description that can be
read and modified. Like the Class
class, the Property
is abstract, with mostly virtual pure methods, to allow the framework to have an
arbitrary collection of properties without knowing the exact type . The real
work is done by template classes that derive from Property
and
properly implement the methods. To allow the generic getting and
setting of a wide variety of types, another class is used in conjunction with Property
called VariantData
. This class wraps most of the C++ standard primitive types,
String
's, Enum
's (more on those later), and Object
derived classes. The core of
the class is a union of types, the one exception being a reference to a String
(which is just a typedef around std::basic_string<char>
). It
also has a member variable that describes the type of data the instance holds.
The rest of the class provides conversion and assignment operators allowing you to write code like this:
The assignment functions allow for the sort of magic we see above. It also
makes sure the data type is set correctly. One of the conversion/assignment
functions looks like this:
Why is this useful ? Because we can have a collection of properties of a
given Object at runtime, we will not necessarily know what the types are. The VariantData
class allows us to ignore this, allowing the compiler to sort out what needs to
happen for us. In other words I might have a collection of properties, one of
which is an int, another some Object*, and a third a bool. The VariantData
lets me write the same style of code for any of the types, and the compiler will
resolve the type for me. This happens because our "get" and
"set" methods have type signatures with them and when the VariantData
instance is used, the compiler invokes the correct conversion operator based on
the method signature.
Ah, but how do we get these method signatures defined ? Remember that the Property
class is an abstract one. The real work is done through several other template
classes that derive from Property
, but actually implement the
methods of Property
. So lets take a look at the TypedProperty
class. This class uses it's template type to specify the type of data that it is
to represent. It then declares typedefs to get and set member functions for and
Object.
Now we have member function pointers in our property class that are type
safe, in other words, if we specified a new Property for an int type (TypedProperty<int>
),
then the get and set functions would read as follows:
- Get:
typedef int (Object::*GetFunction)(void);
- Set:
typedef void (Object::*SetFunction( const int& );
So when our Property::set(Object* source, VariantData* value )
method is called, the magic in the VariantData
mentioned
above occurs, in other words, the set()
method in TypedProperty<int>
class binds the source
to the set method method pointer (m_setFunction
),
and passes in the de-referenced value
pointer, which in turn causes
the VariantData
's int()
conversion operator to be
called by the compiler, and now we have safely passed in an int value to the
function, without ever needing to worry about it. This process works the same
for any of the types mentioned above, though there are specific template classes
that derive from Property to support Object
and Enum
pointers.
I've mentioned a the Enum
class a couple of times now, and I
imagine you're all just dying with anticipation to discover just who and what
these little fella's are. The Enum
class is used, not surprisingly,
to wrap C++ enum types and provide a type safe way of using them without
worrying about the specific type at runtime. This allows for iterating the enum
values (and wrapping back to the beginning), and retrieving the first and last
enum values. It also allows for having string representations of the various
enum values for display purposes. Enum
classes can be set either by
the actual enum type, as an int, or from a string.
Well that about wraps it up for now. I will try and write the next two
installments as soon as I can (assuming anyone is actually interested in this
kind of coding insanity !)