Qt 中的属性系统(Property System)

本节内容主要讲解我对 Qt 属性系统的理解。官方文档参考 The Property System。

如何理解“属性系统”这个概念?

一般我们说一个类有什么属性,指的就是这个类有啥成员变量。比如 People 类中有个 int age 的私有成员变量,我们就可以说这个 People 类有个“年龄”属性可以更改读取。

Qt 提供的这个属性系统,作用就是把类的信息暴露出来成为通用的大家都认识的信息。比如用 C++ 语言写的People 类中有个 int age 变量,但是如果用 QML 语言去读取就会出问题,因为 QML 有自己的规则,它不认识 C++ 啊。怎么办呢?用 Qt 的属性系统就可以解决这个问题。属性系统可以这样理解:当一个类的成员变量或者成员函数用属性系统处理一下,它们就从 C++ 内部中暴露出来,而且大家都认得。

属性系统是专门为元对象系统服务的。

如何声明一个属性并赋予读/写操作?

Qt 有自己的语法,只需要在 QObject 及其子类中用 Q_PROPERTY 宏写就可以了。比如 QWidget 这个类,其中使用属性系统的代码如下所示:

Q_PROPERTY(QCursor cursor  READ cursor  WRITE setCursor  RESET unsetCursor)

上面这一行代码就声明了一个 cursor 属性,指明了它是 QCursor 类型的,而且指明了需要用自己的 cursor() 函数来读取这个属性值,指明了用自己的 setCursor() 函数来修改属性值,还指明了用自己的 unsetCursor() 函数进行默认值设置。一行语句就把一个属性声明好了,代码还算是很简洁的。

如何将类的变量导出为一个属性值?

注意上述的属性值 cursor 可不是 QWidget 的一个成员变量,要想将 QWidget 类中的某个变量导出成为一个属性值,应该用 MEMBER 关键字将变量注册到 Qt 的属性系统。如下:

   Q_PROPERTY(QColor  color  MEMBER m_color  NOTIFY colorChanged)
   ...
signals:
   void colorChanged();
private:
   QColor m_color;

上面的代码把 QWidget 类中的 m_color 成员变量导出为一个属性值,并且起了个新名字“color”。那么外界所能看到的或者说只认可的属性值只有 color 而不是 m_color。

我们要时刻记住,一个属性在行为上是类似于类的成员变量的。

Q_PROPERTY 中的常用格式

我们声明了一个属性值,常用的操作无非就是读、写、将成员变量导出为属性值、关联信号等。下文就从语法的角度来说说怎么做。

  • 指定读取属性值的函数

假设有个布尔类型的属性值 focus,用 READ 来指定读取的函数为 hasFocus()。因为是读操作,所以养成好的习惯,hasFocus() 函数最好也是 const 类型的。代码如下:

Class Widget : public QObject
{
    Q_PROPERTY(bool focus READ hasFocus)
    Q_OBJECT
public:
    bool hasFocus() const;
}
  • 指定修改属性值的函数

还是 focus 这个属性值,要对它进行操作,用 WRITE 关键字来指定修改属性值的函数为 setFocus()。设置函数有个限制,就是函数返回值必须是 void。这也好理解,我就是修改数值而已,不需要返回什么。第二个限制是函数参数只能有一个,把目标值作为参数即可。代码如下:

Class Widget : public QObject
{
    Q_PROPERTY(bool focus WRITE setFocus)
    Q_OBJECT
public:
    bool hasFocus() const;
    void setFocus(bool on);
}
  • 导出成员变量为一个属性值

上面两个读/写的属性值都不是类中的成员变量,是凭空声明出来的一个属性值。要想将类中已有的成员变量设置为属性值,需要用 MEMBER 关键字。这样的话 focus 这个属性值就变的可读可写了。要读的话用类自己的 hasFocus() 函数,要写的话用自带的 setFocus() 修改 m_focus 变量就可以了,属性值会自动跟着变的。

虽然 READ、WRITE、MEMBER 这三个关键字都可以赋予属性值可读可写的特性,但是 READ、WRITE 和 MEMBER 不能同时使用,赋予可读可写特性一次就够了,不能赋予两次。就好像一个对象不能被析构两次一样。

代码如下:

Class Widget : public QObject
{
    Q_PROPERTY(bool focus MEMBER m_focus)
    Q_OBJECT
public:
    bool hasFocus() const;
    void setFocus(bool on);
private:
    bool m_focus;
}
  • 给属性值设置关联的信号

如果我们希望某个属性值变化时能发射出信号,Qt 的属性系统是用 NOTIFY 关键字来指定信号。代码如下:

Class Widget : public QObject
{
    Q_PROPERTY(bool focus MEMBER m_focus NOTIFY focusChanged)
    Q_OBJECT
public:
    bool hasFocus() const;
    void setFocus(bool on);
signals:
    void focusChanged();
private:
    bool m_focus;
} 

以上就是最常用的几个操作。除此之外还有 RESET、REVISION、DESIGNABLE、SCRIPTABLE、STORED、USER、CONSTANT、FINAL。这些关键字的含义及用法参考官方文档 The Property System。

实际操作一下读/写属性值?

上文我们创建了 QObject 的子类 Widget,并且指定了修改 focus 属性值的函数,现在我们创建 Widget 类的一个对象 w 来看看实际代码中如何写。

由于赋予属性值读/写有两种办法(方法一是用 READ、WRITE;方法二是 MEMBER ),那么实际使用中针对这两种方式使用略有不同。

如果是用 READ、WRITE,那么直接调用指定的函数即可,如:

Widget *w = new Widget;
w->setFocus(true);

如果是用 MEMBER,那么用 QObject 的 property() 和 setProperty() 两个函数,如:

Widget *w = new Widget;
w->property("focus");
w->setProperty("focus", true);

两种方法哪个好?

当然是 WRITE。它的效率跟高、速度更快,而且在编译阶段就可以进行类型检查。缺点就是还没运行前你就得了解这个类是有 setFocus() 这个函数。而采用 MEMBER 方式时,我们不需要知道这个类有啥函数、有啥变量,只需要知道这个类有一个叫“focus”的属性值就可以了。

我怎么知道一个类中有啥属性?

那既然 MEMBER 的好处是只需要知道这个类有一个叫 focus 的属性值,再极端点,我连这个类有啥属性值都不知道时怎么办?Qt 的元对象系统是如此的强大,已经为你准备好了相关的函数了。那就是 QMetaObject、QMetaProperties。下列代码输出了 Widget 类中所有的属性名称和属性值:

Widget *w = new Widget;

const QMetaObject *metaobject = w->metaObject();
int count = metaobject->propertyCount();

for (int i = 0; i < count; ++i) {
    QMetaProperty metaproperty = metaobject->property(i);
    const char *name = metaproperty.name();
    QVariant value = w->property(name);
    ...
} 

完整的示例

上文讲解的 Widget 类由于代码分散在各处,可能对一个类如何操作属性值没有直观的感受,下面用完整的代码来演示属性的一系列操作。

声明代码

class Widget : public QObject
{
    Q_OBJECT
    Q_PROPERTY(bool focus READ hasFocus WRITE setFocus NOTIFY focusChanged)
public:
    Widget(QObject *parent = 0);
    ~Widget();

    bool hasFocus() const
    {
        return m_focus;
    }

    void setFocus(bool on)
    {
        m_focus = on;
    }
signals:
    void focusChanged();

private:
    bool m_focus;
}

解读

我们有一个继承于 QObject 的 Widget 类。我们用 Q_PROPERTY 宏声明了一个属性来跟踪私有变量 m_focus 值,属性名使用 focus,属性类型是个布尔类型。用 READ 指定了读取函数 hasFocus(),用 WRITE 指定了修改函数 setFocus,用 NOTIFY 指定了发射信号 focusChanged()。

使用代码

现在我们有个 Widget 指针和一个 QObject 指针,设置属性值的方法是:

Widget *w = new Widget;
w->setFocus(true);

QObject *o = w;
o->setProperty("focus", true);

在程序运行过程中添加属性

QObject::setProperty() 函数也可以用于在运行期添加新的属性。如果对象中已经存在该属性,则新添加的属性值会更改原有值并返回true;如果对象中不存在该属性,那么会自动添加到QObject中,注意了,此时返回值仍有可能是 false。所以根据返回值是不能确定属性值是否被设置成功。

说的有点拗口,这样说吧,在已知类中存在某属性的情况下,可以根据返回值判断是否设置成功。如果添加新的属性值,就不能用返回值判断是否设置成功。

注意事项

因为是在程序运行过程中新增的属性,所以这个属性可以理解为是“临时的”。它们只会加到 QObject 实例中,不会加到最为核心的 QMetaObject 实例中。就好比一个公司已经发展起来了,后来新入职的员工就不是核心人员。

那么要想删除这个“临时的”属性,只需要掉用 QObject::setProperty() 函数将空的 QVariant 值传进去即可。

如何自定义属性类型?

自定义的属性类型,需要用 Q_DECLARE_METATYPE 宏进行注册,这样就可以存储在QVariant中了。

如何给类添加额外的属性信息?

除了正规常用的属性外,我们还可以用 Q_CLASSINFO 宏给类添加额外的属性信息,语法就是“键值-对”形式。例如:

Q_CLASSINFO("Version", "3.0.0")

那么在程序运行的过程中,随时都可以调用 QMetaObject::classInfo() 函数来获取这些额外属性信息。

以上就是我对 Qt 属性系统的理解。

原文地址:https://www.cnblogs.com/bruce1992/p/14382906.html