Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Visual Component Framework

0.00/5 (No votes)
25 Oct 2000 1  
An Article describing working with the Visual Component Framework
  • Download source files - 1.8 Mb
  • Sample Image - vcf.jpg

    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:

    //
    
    //class Foo : public VCF::Object {  //this is OK
    
    //...
    
    //};
    
    //
    
    //class Foo {  //this is bad - there is no way to hook the RTTI up without at
    
    //...					//least deriving from VCF::Object
    
    //};
    
    // 

    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:

    //
    
    //#define FOO_CLASSID			"1E8CBE21-2915-11d4-8E88-00207811CFAB"
    
    //
    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:
    //
    
    //class Foo : public VCF::Object {  
    
    //public:
    
    //	BEGIN_CLASSINFO(Foo, "Foo", "VCF::Object", FOO_CLASSID)
    
    //	END_CLASSINFO(Foo)
    
    //...
    
    //};
    
    //
    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.
    //
    
    //class Foo : public VCF::Object {  
    
    //	class FooInfo : public ClassInfo<Foo> { 
    
    //	public: 
    
    //		FooInfo( Foo* source ): 
    
    //				ClassInfo<Foo>( source, 
    
    //						"Foo", 
    
    //						"VCF::Object", 
    
    //						"1E8CBE21-2915-11d4-8E88-00207811CFAB" ){ 
    
    //			if ( true == isClassRegistered()  ){ 
    
    //
    
    //			} 
    
    //		} 
    
    //
    
    //		virtual ~FooInfo(){}
    
    //
    
    //	};//end of FooInfo
    
    //
    
    //	...
    
    //};
    
    //

    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:

    //
    
    //class APPKIT_API Component : public Object, public Persistable{
    
    //public:
    
    //	BEGIN_ABSTRACT_CLASSINFO(Component, "VCF::Component", "VCF::Object", COMPONENT_CLASSID)
    
    //	PROPERTY( double, "left", Component::getLeft, Component::setLeft, PROP_DOUBLE );
    
    //	PROPERTY( double, "top", Component::getTop, Component::setTop, PROP_DOUBLE );
    
    //	PROPERTY( String, "name", Component::getName, Component::setName, PROP_STRING );
    
    //	EVENT("VCF::ComponentEvent", "onComponentCreated", "VCF::ComponentListener","VCF::ComponentHandler" );
    
    //	EVENT("VCF::ComponentEvent", "onComponentDeleted", "VCF::ComponentListener","VCF::ComponentHandler" );
    
    //	END_CLASSINFO(Component)
    
    //
    
    

    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.

    //
    
    //union{
    
    //	int IntVal;
    
    //	long LongVal;
    
    //	short ShortVal;
    
    //	unsigned long ULongVal;
    
    //	float FloatVal;
    
    //	char CharVal;
    
    //	double DblVal;
    
    //	bool BoolVal;
    
    //	Object* ObjVal;
    
    //	Enum* EnumVal;
    
    //};
    
    // 
    
    

    The rest of the class provides conversion and assignment operators allowing you to write code like this:

    //
    
    //	VariantData v;
    
    //	int i = 12;
    
    //	double d = 123.456;
    
    //	String s = "Foo";
    
    //	v = s;		//v now holds a reference to the String s, and it's data type is automatically set to PROP_STRING
    
    //	v = i;		//v now stores the int value 12, and data type is PROP_INT
    
    //	v = d;		//v now stores the double value 123.456, and the data type is PROP_DOUBLE
    
    // 

    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:

    //
    
    // 	operator float (){
    
    //		return FloatVal;
    
    //	}
    
    //
    
    //	operator=( const float& newValue ){
    
    //		FloatVal = newValue;
    
    //		type = PROP_FLOAT;
    
    //	}
    
    //
    
    

    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. 

    //
    
    //template <class PROPERTY> class TypedProperty : public Property {
    
    //public:
    
    //	typedef PROPERTY (Object::*GetFunction)(void);
    
    //	typedef void (Object::*SetFunction)(const PROPERTY& ); 
    
    //...
    
    //protected:	
    
    //	GetFunction m_getFunction;
    
    //	SetFunction m_setFunction;
    
    //};
    
    //
    
    

    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 !)

    License

    This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

    A list of licenses authors might use can be found here