【Qt开发流程】之对象模型2:属性系统

描述

Qt提供了一个复杂的属性系统,类似于一些编译器供应商提供的属性系统。然而,作为一个独立于编译器和平台的库,Qt不依赖于非标准的编译器特性,如__property[property]

Qt解决方案适用于Qt支持的所有平台上的任何标准c++编译器。它基于元对象系统,该系统还通过信号和槽提供对象间通信。

声明属性要求

要声明属性,请在继承QObject的类中使用Q_PROPERTY()宏。

这个宏用于在继承QObject的类中声明属性。属性的行为类似于类数据成员,但它们具有可通过元对象系统访问的附加特性。

cpp 复制代码
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])

声明需要属性名称和类型以及READ函数。该类型可以是QVariant支持的任何类型,也可以是用户定义的类型。其他项是可选的,但WRITE函数是常见的。除USER外,其他属性默认为false。

例如:

cpp 复制代码
Q_PROPERTY(QString title READ title WRITE setTitle USER true)

下面是取自类QWidget的属性声明的一些典型示例:

cpp 复制代码
Q_PROPERTY(bool focus READ hasFocus)
Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled)
Q_PROPERTY(QCursor cursor READ cursor WRITE setCursor RESET unsetCursor)

下面是一个示例,展示了如何使用member关键字将成员变量导出为Qt属性。注意,必须指定NOTIFY信号才能允许QML属性绑定:

cpp 复制代码
   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;

属性表现类似于类数据成员,但通过元对象系统可访问附加功能。

  • 如果未指定MEMBER变量,则需要一个READ访问器函数。它用于读取属性值。理论上,应该使用const函数来实现,并且必须返回属性的类型或该类型的const引用。例如,QWidget::focus是一个带有READ函数(QWidget::hasFocus())的只读属性。
  • WRITE访问器函数是可选的。它用于设置属性值。它必须返回void,并且必须接受一个参数,要么是属性的类型,要么是指向该类型的指针或引用。例如,QWidget::enabled具有WRITE函数QWidget::setEnabled()。只读属性不需要WRITE函数。例如,QWidget::focus没有WRITE函数。
  • 如果未指定READ访问器函数,则需要一个MEMBER变量关联。这使得给定的成员变量可读可写,而无需创建READ和WRITE访问器函数。如果需要控制变量访问,仍然可以使用READ或WRITE访问器函数(但不能同时使用)。
  • RESET函数是可选的。它用于将属性设置回上下文特定的默认值。例如,QWidget::cursor具有典型的READ和WRITE函数(QWidget::cursor()和QWidget::setCursor()),它还有一个RESET函数QWidget::unsetCursor(),因为对QWidget::setCursor()的调用没有意义。RESET函数必须返回void并且不接受参数。
  • NOTIFY信号是可选的。如果定义了,它应指定该类中的一个现有信号,该信号在属性值更改时发出。MEMBER变量的NOTIFY信号必须接受零个或一个参数,该参数的类型必须与属性的类型相同。参数将采用属性的新值。只有在属性实际上发生更改时,才应该发出NOTIFY信号,以避免在QML中不必要地重新评估绑定,例如。当需要为未具有显式设置器的MEMBER属性自动发出该信号时,Qt会自动发出该信号。
  • REVISION编号是可选的。如果包含,则定义了属性及其通知器信号在API的特定修订版中使用(通常用于暴露给QML)。如果不包含,则默认为0。
  • DESIGNABLE属性指示属性是否应在GUI设计工具(例如Qt Designer)的属性编辑器中可见。大多数属性都是DESIGNABLE的(默认为true)。可以指定一个布尔成员函数代替true或false。
  • SCRIPTABLE属性指示该属性是否应由脚本引擎访问(默认为true)。您可以指定一个布尔成员函数代替true或false。
  • STORED属性指示该属性是否应被视为存在于自己或依赖于其他值上。它还指示在存储对象状态时是否必须保存属性值。大多数属性都是STORED的(默认为true),但是例如,QWidget::minimumWidth()的STORED为false,因为它的值只是从属性QWidget::minimumSize()的宽度组件中获取的,后者是一个QSize。
  • USER属性指示该属性是否指定为类的用户界面或可编辑属性。通常,每个类只有一个USER属性(默认为false)。例如,QAbstractButton::checked是(可检查)按钮的可编辑属性。请注意,QItemDelegate会获取和设置部件的USER属性。
  • CONSTANT属性的存在表示属性值是常量的。对于给定的对象实例,常量属性的READ方法在每次调用时必须返回相同的值。该常量值可以在对象的不同实例间有所不同。常量属性不能具有WRITE方法或NOTIFY信号。
  • FINAL属性的存在表示该属性不会被派生类覆盖。在某些情况下,这可以用于性能优化,但是不受moc的强制执行。永远不要覆盖FINAL属性。

可以继承READ、WRITE和RESET功能。它们也可以是虚拟的。当它们在使用多重继承的类中被继承时,它们必须来自第一个继承的类。

属性类型可以是QVariant支持的任何类型,也可以是用户定义的类型。在本例中,类QDate被认为是用户定义的类型。

cpp 复制代码
Q_PROPERTY(QDate date READ getDate WRITE setDate)

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

对于QMap、QList和QValueList属性,属性值是一个QVariant,其值是整个列表或映射。注意,Q_PROPERTY字符串不能包含逗号,因为逗号分隔了宏参数。因此,必须使用QMap作为属性类型,而不是QMap<QString,QVariant>。

为了保持一致性,也使用QList和QValueList而不是 QList和QValueList。

使用元对象系统对属性进行读写操作

可以使用泛型函数QObject::property()和QObject::setProperty()来读写属性,而不需要知道除了属性名称之外的任何关于所属类的信息。在下面的代码片段中,对QAbstractButton::setDown()的调用和对QObject::setProperty()的调用都设置属性"down"。

cpp 复制代码
QPushButton *button = new QPushButton;
QObject *object = button;

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

通过WRITE访问属性是两种方法中更好的一种,因为它更快,并且在编译时提供更好的诊断,但是以这种方式设置属性要求您在编译时了解该类。按名称访问属性使您可以访问在编译时不知道的类。您可以在运行时通过查询类的QObject、QMetaObject和QMetaProperties来发现类的属性。

cpp 复制代码
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中定义。

我们在类的私有部分使用Q_PROPERTY()宏声明属性。所需的READ函数名为priority,我们还包含了一个名为setPriority的WRITE函数。枚举类型必须使用Q_ENUM()宏在元对象系统中注册。注册枚举类型使枚举数名称可用于调用QObject::setProperty()。我们还必须为READ和WRITE函数提供自己的声明。MyClass的声明可能看起来像这样:

cpp 复制代码
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 };
    Q_ENUM(Priority)

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

signals:
    void priorityChanged(Priority);

private:
    Priority m_priority;
};

READ函数是const,并返回属性类型。WRITE函数返回void,并且只有一个属性类型的参数。元对象编译器执行这些要求。

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

cpp 复制代码
MyClass *myinstance = new MyClass;
QObject *object = myinstance;

myinstance->setPriority(MyClass::VeryHigh);
object->setProperty("priority", "VeryHigh");
qDebug().noquote() << "[" << __FILE__ << __LINE__ << "]" << myinstance->property("priority");

当把

cpp 复制代码
object->setProperty("priority", "VeryHigh");

改为

cpp 复制代码
object->setProperty("priority", "VeryHi");

再次输出,会输出为空,会自行检查,是安全的。

在这个例子中,作为属性类型的枚举类型在MyClass中声明,并使用Q_ENUM()宏在元对象系统中注册。这使得枚举值可以作为字符串使用,就像在调用setProperty()时一样。如果枚举类型在另一个类中声明,则需要它的完全限定名(即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()声明的属性一样。

属性和自定义类型

属性使用的自定义类型需要使用Q_DECLARE_METATYPE()宏注册,以便它们的值可以存储在QVariant对象中。这使得它们既适合与类定义中使用Q_PROPERTY()宏声明的静态属性一起使用,也适合与运行时创建的动态属性一起使用。

向类添加附加信息

连接到属性系统的是一个额外的宏Q_CLASSINFO(),它可以用来将额外的名值对附加到类的元对象上,例如:

cpp 复制代码
Q_CLASSINFO("Version", "3.0.0")

与其他元数据一样,类信息可以在运行时通过元对象访问;详情可以参考QMetaObject::classInfo()

使用场景

  1. 自定义控件:通过属性系统,可以方便地添加自定义控件所需要的属性,并在运行时动态修改其属性值,以实现控件的个性化定制。

  2. 翻译:在多语言支持的应用程序中,可以将所有需要翻译的字符串作为属性添加到组件中,这样可以方便地将这些字符串全部翻译成不同的语言。

  3. 皮肤定制:通过属性系统,可以在运行时动态地修改控件的样式属性,以实现界面的不同皮肤定制。

    比如改变样式:

cpp 复制代码
{
    // 创建QPushButton控件
    QPushButton *button = new QPushButton("Click me", this);

    // 添加样式属性
    button->setProperty("buttonColor", QColor(220, 220, 220));
    button->setProperty("buttonTextColor", QColor(50, 50, 50));

    // 设置初始样式
    setButtonStyle(button);

    // 在运行时修改样式属性
    button->setProperty("buttonColor", QColor(0, 255, 0));
    button->setProperty("buttonTextColor", QColor(255, 0, 0));
    setButtonStyle(button); // 重新设置样式
}

// 定义函数来根据样式属性设置样式表
void setButtonStyle(QPushButton* button) {
    QColor buttonColor = button->property("buttonColor").value<QColor>();
    QColor buttonTextColor = button->property("buttonTextColor").value<QColor>();
    button->setStyleSheet(QString("QPushButton { background-color: rgb(%1, %2, %3); color: rgb(%4, %5, %6); }")
                          .arg(buttonColor.red())
                          .arg(buttonColor.green())
                          .arg(buttonColor.blue())
                          .arg(buttonTextColor.red())
                          .arg(buttonTextColor.green())
                          .arg(buttonTextColor.blue()));
}

属性系统的优势

  1. 灵活性:通过属性系统,可以轻松地在运行时动态修改对象的属性值,从而实现动态控制组件的行为。

  2. 扩展性:开发人员可以轻松地为自定义的控件添加属性,并通过属性系统来进行控制,从而扩展控件的属性和功能。

  3. 易用性:Qt属性系统提供了简单易用的API,开发人员可以方便地使用属性系统来管理和操作对象的属性。

结论

年轻人嘛,现在没钱算什么,以后没钱的日子还多着呢

相关推荐
用户805533698034 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner4 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz9 天前
QML Hello World 入门示例
qt
xcyxiner12 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner13 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner13 天前
DicomViewer (添加模型类)3
qt
xcyxiner14 天前
DicomViewer (目录调整) 2
qt
xcyxiner14 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
LDR00616 天前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言
雪碧聊技术16 天前
Tree.js是什么?一文讲透
开发语言·javascript·ecmascript