可视化组件框架





5.00/5 (14投票s)
一篇描述使用可视化组件框架的文章。
引言
Visual Component Framework 的灵感来源于 NeXTStep 的 Interface Builder、Java IDE(如 JBuilder、Visual J++)以及 Borland 的 Delphi 和 C++ Builder 等环境的易用性。我想要一个通用的 C++ 类框架,可以用于快速、直观地(在设计 UI 时)构建应用程序,同时使框架的核心尽可能跨平台。本文将讨论我遇到的一些问题以及我尝试解决它们的方法。Visual Component Framework 是一个开源项目,如果您认为它可能有用,请随时获取并使用。如果您真的喜欢冒险,可以志愿帮助开发它,使其变得更好,特别是将其作为插件集成到 VC++ 环境中。有关该项目、提供帮助或仅仅是浏览 Doc++ 生成的文档的更多信息,请前往 Source Forge 上的 VCF 项目 此处,或项目网站 此处。代码可从 CVS 获取(请遵循此处有关在 Windows 上设置 CVS 的说明),或从 此处的 zip 文件获取(大约 1.8 MB - 其中包含 Xerces 的 XML 库 - 一个 XML 解析器。请查看网站获取最新版本)。
Visual Component Framework (VCF) 分为三个 DLL:FoundationKit、GraphicsKit 和 ApplicationKit。本文讨论 VCF 的核心——FoundationKit。FoundationKit 提供了框架的基本核心类和高级 RTTI 功能。早期我就知道我需要某种 RTTI 或反射,类似于 Java 或 ObjectPascal 提供的功能。这是必需的,因为可视化设计环境需要一种方式来暴露组件的属性和事件,并提供一种一致、可定制且可扩展的方式来编辑它们。考虑到这一点,该框架允许开发人员查询对象的 Class
,进而可以访问类的名称、超类、属性、事件和方法。为了实现这一点,该框架大量使用了模板和 STL。Class
是一个抽象基类,模板类从其派生。类提供以下信息:
Class
的名称——存储在成员变量中,而不是依赖typeid(...).name()
来检索类名。并非所有编译器都支持typeid(...).name()
函数。- Class ID——它代表类的 UUID(通用唯一标识符)。当引入分布式功能时,这将非常有用。
- 在运行时发现
Class
所有属性的能力。属性被定义为通过 getter 和 setter 方法访问的类属性。属性可以是原始类型(int、long double 等)、Object 派生类型或枚举。属性也可以是其他对象的集合。 - 检索类的超类。
- 创建
Class
对象所代表的类的实例的能力。当然,这假设有一个默认构造函数可用。
第一步(显然)是确保您的类派生自 Framework 对象。例如:
// //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 //}; //
接下来,您应该为您的类定义一个类 ID(字符串形式)。在 Windows 上,我使用 guidgen.exe 创建 UUID。定义应如下所示:
// //#define FOO_CLASSID "1E8CBE21-2915-11d4-8E88-00207811CFAB" //下一步是将宏添加到您的类定义(.h/.hpp 文件)中。这些对于基本的 RTTI 信息(类-超类关系)是必需的,并确保您将继承超类的任何属性。例如:
// //class Foo : public VCF::Object { //public: // BEGIN_CLASSINFO(Foo, "Foo", "VCF::Object", FOO_CLASSID) // END_CLASSINFO(Foo) //... //}; //该宏接受类类型 ID、用作类名称的字符串、表示类超类的字符串以及表示类 ID 的字符串(其中类 ID 是 UUID 的字符串表示)。宏最终会创建一个公共嵌套类,用于注册您正在编写的类。上面的宏为
Foo
类的开发者生成以下内联代码:// //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 // // ... //}; //
isClassRegistered()
方法会检查 ClassRegistry
以查看类是否已注册,如果没有,则会在 ClassRegistry
中创建一个新条目。类存储在单例 ClassRegistry
对象中,该对象包含每个已注册类类型的单个 Class
实例,因此多个对象实例共享一个 Class
实例。为了做到这一点,框架有一个定义的抽象类(Class
)和两个模板类 TypedClass
和 TypedAbstractClass
。TypedClass
和 TypedAbstractClass
中指定的模板参数用于安全地允许类比较,并允许在运行时创建对象(此功能仅由 TypedClass
支持)。这两个模板类是必需的,因为框架中可能存在抽象类,这些类根据定义无法实例化,但需要在类层次结构中,因为所有 Class
实例都有一个 getSuperClass()
方法,允许程序员在运行时遍历类层次结构。ClassRegistry
在一个映射中保存所有类,并且每次注册一个新的 Class
实例时,都会找到超类并将其所有属性复制到新注册的实例中,从而确保派生类“继承”其超类的属性和事件。
要添加更详细的 RTTI,您可以添加属性、事件和方法。Component 类声明中可以找到一个示例:
// //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) //
这演示了公开三个属性和两个事件。属性允许您在运行时动态发现对象的属性。Property
包含一个指向访问器方法(或“get”方法)的方法指针,以及可选地一个指向变异器或“set”方法的指针。此外,Property
具有显示名称和显示描述,可以读取和修改。与 Class
类一样,Property
是抽象的,大部分是纯虚方法,以允许框架拥有任意属性集合而无需了解确切类型。实际工作由派生自 Property
的模板类完成,并正确实现方法。为了允许通用地获取和设置各种类型,另一个类 VariantData
与 Property
一起使用。该类包装了大多数 C++ 标准原始类型、String
、Enum
(稍后会详细介绍)和 Object
派生类。该类的核心是类型的联合,唯一例外是对 String 的引用(它只是 std::basic_string<char>
的 typedef)。它还有一个描述实例所持有数据类型的成员变量。
// //union{ // int IntVal; // long LongVal; // short ShortVal; // unsigned long ULongVal; // float FloatVal; // char CharVal; // double DblVal; // bool BoolVal; // Object* ObjVal; // Enum* EnumVal; //}; //
类的其余部分提供了转换和赋值运算符,允许您编写如下代码:
// // 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 //
赋值函数允许我们看到上述魔法。它还确保数据类型设置正确。其中一个转换/赋值函数如下所示:
// // operator float (){ // return FloatVal; // } // // operator=( const float& newValue ){ // FloatVal = newValue; // type = PROP_FLOAT; // } //
这有什么用?因为我们可以在运行时拥有一个对象的属性集合,我们不一定会知道它们的类型。VariantData
类允许我们忽略这一点,让编译器为我们处理需要发生的事情。换句话说,我可能有一个属性集合,其中一个属性是 int,另一个是 Object*,第三个是 bool。VariantData
允许我对所有类型编写相同的代码样式,编译器会为我解析类型。这是因为我们的“get”和“set”方法带有类型签名,并且当使用 VariantData
实例时,编译器会根据方法签名调用正确的转换运算符。
啊,但是我们如何定义这些方法签名呢?记住 Property
类是抽象的。实际工作是通过其他几个派生自 Property
的模板类完成的,但这些类实际实现了 Property
的方法。所以让我们来看看 TypedProperty
类。该类使用其模板类型来指定它要表示的数据类型。然后它声明 typedef 来获取和设置类的成员函数。
// //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; //}; //
现在我们的属性类中有了类型安全的成员函数指针,换句话说,如果我们为一个 int 类型(TypedProperty<int>
)指定了一个新属性,那么 get 和 set 函数将如下所示:
- Get:
typedef int (Object::*GetFunction)(void);
- Set:
typedef void (Object::*SetFunction( const int& );
所以当我们的 Property::set(Object* source, VariantData* value )
方法被调用时,上面提到的 VariantData
中的魔法就会发生,换句话说,TypedProperty<int>
类中的 set()
方法会将 source
绑定到 set 方法指针(m_setFunction
),并将解引用的 value
指针传递进去,这反过来又会导致编译器调用 VariantData
的 int()
转换运算符,现在我们已经安全地将一个 int 值传递给了函数,而无需担心它。这个过程对上面提到的任何类型都有效,尽管有特定的模板类派生自 Property 以支持 Object
和 Enum
指针。
我几次提到了 Enum
类,我想你们都迫不及待地想知道这些小家伙是谁以及它们是什么。Enum
类顾名思义,用于包装 C++ 枚举类型,并提供一种类型安全的方式来使用它们,而无需在运行时担心具体类型。这允许迭代枚举值(并循环回到开头),以及检索第一个和最后一个枚举值。它还允许拥有各种枚举值的字符串表示形式,用于显示目的。Enum
类可以通过实际的枚举类型、整数或字符串来设置。
好了,今天就到这里。我会尽快写出接下来的两部分(假设有人真的对这种编码疯狂感兴趣!)。