QT核心机制2:属性系统

写在前面

这篇文章基本是对Qt官方文档某些章节的翻译理解了,翻译这些章节的原因是我认为这些是Qt中最核心的东西,翻译的过程也就是强迫自己认真去读它们的过程,我不会完全一字一句的照搬原文,而是按我自己的理解去翻译其中的重点,毕竟我的目的是理解它们,将它们按自己可以灵活使用的方式组织,而不是机械的把它们从一种语言转变成另一种语言。涉及的官方文档原文内容主要包括以下章节:

  1. The Meta-Object System 元对象系统
  2. The Property System 属性系统
  3. Signals & Slots 信号与槽

在这里约定,对原文的翻译用正常字体,个人的理解使用斜体字体。

总共分为三篇文章,本文章为对The Property System 属性系统的翻译。

所有三篇翻译的链接:

QT核心机制1:元对象系统 – CodingLover

QT核心机制2:属性系统 – CodingLover

QT核心机制3:信号与槽 – CodingLover

属性系统

如同很多编译器厂商提供的编译器一样,Qt也提供了一个精妙的属性系统。然而,作为一个独立于编译器和架构的库,Qt不依赖于诸如__property或[property]这样的非标准的编译器特性。Qt的这套属性系统特性可以用于任何Qt支持的编译器与架构。它基于元对象系统(Meta-Object System),这套系统同时也提供信号与槽机制用于对象间通讯。

以下摘录简要说明了属性与成员变量的区别:

  • 成员变量是一个“内”概念,反映的是类的结构构成。属性是一个“外”概念,反映的是类的逻辑意义。
  • 成员变量没有读写权限控制,而属性可以指定为只读或只写,或可读可写。
  • 成员变量不对读出作任何后处理,不对写入作任何预处理,而属性则可以。
  • public成员变量可以视为一个可读可写、没有任何预处理或后处理的属性。 而private成员变量由于外部不可见,与属性“外”的特性不相符,所以不能视为属性。
  • 虽然大多数情况下,属性会由某个或某些成员变量来表示,但属性与成员变量没有必然的对应关系, 比如与非门的 output 属性,就没有一个所谓的 $output 成员变量与之对应。

属性 与成员变量的 区别与联系 及属性修饰词理解

属性的声明要求

在继承了QObject类的类中使用Q_PROPERTY宏定义即可声明属性。下面说明了此宏定义的使用格式:

Q_PROPERTY(type name
            (READ getFunction [WRITE setFunction] |
             MEMBER memberName [(READ getFunction | WRITE setFunction)])
            [RESET resetFunction]
            [NOTIFY notifySignal]
            [REVISION int]
            [DESIGNABLE bool]
            [SCRIPTABLE bool]
            [STORED bool]
            [USER bool]
            [CONSTANT]
            [FINAL])

type,name,以及位于"()"括号中的内容为必须存在的,括号中使用“|”隔开的部分为可选择项,至少应该使用其中的一项,使用”[]“包裹的选项为可选项目。

以下为一些QWidget类中的属性声明例子,这里需要注意,Q_PROPERTY宏后面可以不用写”;“,因为宏结尾已经写了,当然写上也没什么

// 声明一个布尔类型的,名为focus的属性,可以通过hasFocus方法作为访问器进行读取
Q_PROPERTY(bool focus READ hasFocus)

// 声明一个布尔类型的,名为enabled的属性,可以通过isEnabled方法读取,通过setEnabled方法写入
Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled)

// 声明一个QCursor类型的,名为cursor的属性,可以通过setCursor方法读取,通过unsetCursor方法写入
Q_PROPERTY(QCursor cursor READ cursor WRITE setCursor RESET unsetCursor)

以下是一个如何使用MEMBER关键字导出成员变量为属性的例子,当使用MEMBER关键字导出属性,我们需要牢记必须使用NOTIFY关键字来指定一个信号,这样QML才能进行属性绑定。

Q_PROPERTY(QColor color MEMBER m_color NOTIFY colorChanged)
     Q_PROPERTY(qreal spacing MEMBER m_spacing NOTIFY spacingChanged)
     Q_PROPERTY(QString text MEMBER m_text NOTIFY textChanged)
     ...
 signals:
     void colorChanged();
     void spacingChanged();
     void textChanged(const QString &newText);

 private:
     QColor  m_color;
     qreal   m_spacing;
     QString m_text;

这两种创建属性的方法,功能之一应该是为Qt的QML语言服务的,通过配合属性绑定,QML可以在绑定的控件对象属性发生变化时,通过WRITE关键字指定的访问器自动更新绑定的属性的值,或者在属性发生改变从而发出SIGNAL指定的信号时,通过READ访问器自动将属性的值更新到绑定的控件对象。建议参考以下文章:https://zhuanlan.zhihu.com/p/152477333。QML目前我只知道概念,没有用过。

属性的行为类似于类的数据成员,但是当通过元对象系统访问时,其具有一些附加的特性。以下是一些声明属性时的要求以及它们的特性(重点已加粗,只列出了目前需要了解的关键字)

  • 如果没有指定MEMBER变量,则需要READ访问器方法。它用于读取属性值。理想情况下,const函数(const函数保证不会修改类中的数据成员)可用于此目的,并且它必须返回属性的类型或对该类型的const引用。例如,QWidget::focus是一个带有READ方法QWidget::hasFocus()的只读属性。
  • WRITE访问器函数是可选的。用于设置属性值。它必须返回void,并且必须接受一个实参,要么是属性的类型,要么是指向该类型的指针或引用。例如,QWidget::enabled具有WRITE函数QWidget::setEnabled()。只读属性不需要WRITE函数。例如,QWidget::focus没有WRITE功能。
  • 如果没有指定READ访问器函数,则需要关联MEMBER变量。这使得给定的成员变量可读可写,而不需要创建READ和WRITE访问器函数。如果需要控制变量访问,除了MEMBER变量关联之外,还可以使用READ或WRITE访问器函数(但不能同时使用两者)。这里可以理解为使用MEMBER关键字关联一个变量为属性时,Qt会自动生成默认的READ和WRITE方法。
  • NOTIFY功能是可选的。如果定义了它,应该在类中指定一个现有信号,每当属性的值发生变化时(一般为调用WRITE访问器更新属性时),该信号需要被发出。MEMBER变量的NOTIFY信号必须接受0或1个参数,这些参数存储了变化后的数值并与MEMBER变量类型相同。NOTIFY信号只应该在属性真正更改时发出,以避免在QML中不必要地重新计算绑定。当MEMBER属性没有显式的WRITE访问器时,Qt会自动发出这个信号。

READ、WRITE、RESET方法可以被继承。它们也可以是虚拟的。当它们被使用多个继承的类继承时,它们必须来自第一个继承的类。

属性类型可以是QVariant(QVariant类可以看成最常见Qt数据类型的union)支持的任何类型,也可以是用户定义的类型。在这个例子中,类QDate被认为是一个用户定义的类型。

Q_PROPERTY(QDate date READ getDate WRITE setDate)

因为QDate是用户定义的,所以必须在属性声明中包含"QDate"头文件。

使用元对象系统读写属性

使用通用的QObject::property()和QObject::setProperty()方法可以在只知道属性名字的情况下读写属性。在下面的代码片段中,对QAbstractButton::setDown()的调用和对QObject::setProperty()的调用都设置了属性“down”。

 //以下例子中,当我们使用object对象指针访问对象时,我们只知道这个对象继承了QObject,但是只要知道属性名字,依然可以正常访问属性。
 QPushButton *button = new QPushButton;
 QObject *object = button;

 button->setDown(true);
 object->setProperty("down", true);

更推荐通过属性的WRITE访问器访问属性,因为它更快,并且支持在编译阶段进行诊断,但是通过这种方式设置属性需要您在编译时了解类。通过名称访问属性使您可以访问在编译时不知道的类。您可以在运行时通过查询类的QObject、QMetaObject和QMetaProperties来查询类的属性。

//以下例子中object指针指向了某个继承自QObject类的对象,之后通过访问元对象系统的相关对象查询属性的总数,并遍历这些属性获取属性名称与属性值。 
QObject *object = ...
 const QMetaObject *metaobject = object->metaObject();
 int count = metaobject->propertyCount();
 for (int i=0; i<count; ++i) {
     QMetaProperty metaproperty = metaobject->property(i);
     const char *name = metaproperty.name();
     QVariant value = object->property(name);
     ...
 }

在上面的代码片段中,QMetaObject::property()用于获取关于某个未知类中定义的每个属性的元数据。属性名从元数据中获取,并传递给QObject::property()以获得当前对象中属性的值。

一个声明属性并读写属性的综合例子

假设我们有一个类MyClass,它派生自QObject,并在其私有部分中使用Q_OBJECT宏。我们希望在MyClass中声明一个属性来跟踪优先级值。该属性的名称将是priority,其类型将是一个名为priority的枚举类型,它在MyClass中定义。

我们在类的private部分使用Q_PROPERTY()宏声明该属性。所需的READ函数名为priority,我们还包括一个名为setPriority的WRITE函数。枚举类型必须使用Q_ENUM()宏向元对象系统注册。注册枚举类型使枚举器的名称可在调用QObject::setProperty()时使用(设置属性类型时,直接输入字符串格式的枚举名称即可将对应的枚举值赋值给属性,后面调用演示的代码段中会看到使用示例)。我们还必须为READ和WRITE函数提供自己的声明。然后MyClass的声明可能像这样:

 class MyClass : public QObject
 {
     Q_OBJECT
     Q_PROPERTY(Priority priority READ priority WRITE setPriority NOTIFY priorityChanged)

 public:
     MyClass(QObject *parent = 0);
     ~MyClass();

     enum Priority { High, Low, VeryHigh, VeryLow };

     //向元对象系统注册枚举,后面使用setPriority时就可以直接使用枚举名称来映射枚举值
     Q_ENUM(Priority) 

     void setPriority(Priority priority)
     {
         m_priority = priority;
         emit priorityChanged(priority);
     }

     //const函数,声明方法为const方法表明此方法不会改动类中的数据成员变量
     Priority priority() const
     { return m_priority; }

 signals:
     void priorityChanged(Priority);

 private:
     Priority m_priority;
 };

READ访问器方法是静态方法,其用于返回当前的优先级。WRITE访问器方法返回void并且只有一个参数用于传入新的优先级。这样是为了满足元对象系统的要求。

给定一个指向MyClass实例的指针或指向MyClass实例的QObject的指针,我们有两种方法来设置它的priority属性:

 MyClass *myinstance = new MyClass;
 QObject *object = myinstance;

 myinstance->setPriority(MyClass::VeryHigh);

//前面使用了Q_ENUM宏定义注册了枚举名字,这样我们就可以直接传入字符串格式的枚举名字,元对象系统会自动给属性赋值名字对应的枚举值
 object->setProperty("priority", "VeryHigh");

在这个例子中,枚举类型即属性类型在MyClass中声明,并使用Q_ENUM()宏在元对象系统中注册。这使得枚举值在调用setProperty()时可以以字符串的形式使用。如果在另一个类中声明了枚举类型,则需要它的完全限定名称(即OtherClass::Priority),如果同一个枚举名称在多个类中使用,那么我们需要在使用时指定它的全名(比如:OtherClass::Priority),而且其他类也需要继承QObject并且使用Q_ENUM()宏定义注册其中的枚举类型。

也可以使用类似的宏Q_FLAG()。与Q_ENUM()类似,它注册了一个枚举类型,但它将该类型标记为一组标志,即可以通过按位或的方式(OR)组合在一起的值。一个I/O类可能有Read和Write的枚举值,如果希望QObject::setProperty()可以接受Read | Write的用法,则应该使用Q_FLAG()来注册这种枚举类型。

动态属性

QObject::setProperty()还可以用于在运行时向类的实例添加新属性。当使用名称和值调用它时,如果QObject中存在同名称的属性,并且给定的值与属性的类型兼容,则值存储在已有属性中,并返回true。如果值与属性的类型不兼容,则不更改属性,并返回false。但是,如果指定名称的属性在QObject中不存在(比如它没有使用Q_PROPERTY()声明),一个具有给定名称和值的新属性将自动添加到QObject,但仍然返回false。这意味着不能使用返回false来确定是否实际设置了某个特定属性,除非您事先知道该属性已经存在于QObject中。

注意,动态属性是在每个实例基础上添加的,也就是说,它们被添加到QObject,而不是QMetaObject。可以通过将属性名称和无效的QVariant值传递给QObject::setProperty()来从实例中删除属性。QVariant的默认构造函数会构造一个无效的QVariant。

可以使用QObject::property()查询动态属性,就如同使用Q_PROPERTY()在编译时声明的属性一样。

使用自定义类型创建属性

C++中我们可以通过typedef,union,enum,struct等方式创建自定义类型。

属性使用的自定义类型需要使用Q_DECLARE_METATYPE()宏注册,以便它们的值可以存储在QVariant对象中。这样一来,它们就可以用于使用Q_PROPERTY创建的静态属性,或者运行时创建的动态属性。

以下实例演示了如何注册自定义类型:

 // 注册一个结构体类型
 struct MyStruct
 {
     int i;
     ...
 };

 Q_DECLARE_METATYPE(MyStruct)
 // 如果存在命名空间,Q_DECLARE_METATYPE宏必须位于命名空间外,并且注册时指明类型所处的命名空间 
 namespace MyNamespace
 {
     ...
 }

 Q_DECLARE_METATYPE(MyNamespace::MyStruct)

向类添加额外的信息

属性系统附带一个名为Q_CLASSINFO()的宏,它可用于将附加的名值对附加到类的元对象上:

Q_CLASSINFO("Version", "3.0.0")

类信息可以如同其他元数据(meta-data)一样在运行时使用QMetaObject::classInfo()访问。

发表评论

您的电子邮箱地址不会被公开。