65.9K
CodeProject 正在变化。 阅读更多。
Home

可视化组件框架

starIconstarIconstarIconstarIconstarIcon

5.00/5 (14投票s)

2000 年 10 月 26 日

BSD
viewsIcon

155039

downloadIcon

1462

一篇描述使用可视化组件框架的文章。

  • 下载源文件 - 1.8 MB
  • Sample Image - vcf.jpg

    引言

    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 对象所代表的类的实例的能力。当然,这假设有一个默认构造函数可用。
    为了让 RTTI 在框架中工作,派生类的开发者必须为他们的类做三件事才能参与框架。未能实施这些步骤将意味着他们的类将没有正确的 RTTI。已经编写了一系列宏(定义在 ClassInfo.h 中)来简化此操作。

    第一步(显然)是确保您的类派生自 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)和两个模板类 TypedClassTypedAbstractClassTypedClassTypedAbstractClass 中指定的模板参数用于安全地允许类比较,并允许在运行时创建对象(此功能仅由 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 的模板类完成,并正确实现方法。为了允许通用地获取和设置各种类型,另一个类 VariantDataProperty 一起使用。该类包装了大多数 C++ 标准原始类型、StringEnum(稍后会详细介绍)和 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 指针传递进去,这反过来又会导致编译器调用 VariantDataint() 转换运算符,现在我们已经安全地将一个 int 值传递给了函数,而无需担心它。这个过程对上面提到的任何类型都有效,尽管有特定的模板类派生自 Property 以支持 ObjectEnum 指针。

    我几次提到了 Enum 类,我想你们都迫不及待地想知道这些小家伙是谁以及它们是什么。Enum 类顾名思义,用于包装 C++ 枚举类型,并提供一种类型安全的方式来使用它们,而无需在运行时担心具体类型。这允许迭代枚举值(并循环回到开头),以及检索第一个和最后一个枚举值。它还允许拥有各种枚举值的字符串表示形式,用于显示目的。Enum 类可以通过实际的枚举类型、整数或字符串来设置。

    好了,今天就到这里。我会尽快写出接下来的两部分(假设有人真的对这种编码疯狂感兴趣!)。

    © . All rights reserved.