Qt元对象和属性机制
Qt元对象和属性机制
Qt是很早期的库,当时c++连标准库都不完善,如果Qt在c++14以后编写,绝对不会搞这么复杂
概述
Qt的元对象系统提供了3个重要的特性:
- 信号和槽机制:实现各
QObject
组件之间的通信 - 实时类型信息:通过运行时使用的类型,执行不同的函数,复用接口
- 动态属性系统:存储类的相关信息,用于在运行时识别类对象的信息
其中后两者可以认为是一种反射的实现。
使用元对象系统需要满足三个条件:
- 只有
QObject
派生类才可以使用元对象系统特性。 - 在类声明前使用
Q_OBJECT
宏来开启元对象功能。 - 使用MOC工具为每个
QObject
派生类提供实现代码。
QObject
QObject
类是所有需要利用元对象系统的对象的基类。而 Q_OBJECT
宏则是一个加在继承自 QObject
的类的定义部分的宏,用于启用元对象系统,会被MOC处理。
尽管
Q_OBJECT
宏提供了许多便利,但要发挥其全部功能,类必须继承自QObject
。这是因为许多由Q_OBJECT
宏启用的特性都依赖于QObject
类的基础设施。如果一个类包含了Q_OBJECT
宏却不继承自QObject
,这将导致编译错误。反之则如果一个类继承了QObject
而不启用Q_OBJECT
,则获取其元对象信息会得到其最近的启用了元对象机制的对象树父对象的信息,显然这不是我们的本意。
因此强烈建议所有 QObject
的子类都启用 Q_OBJECT
宏。启用元对象特性的类将会获得以下能力:
-
能使用信号与槽
-
启用事件机制
-
能挂载在对象树上
QObject::setParent() QObject::findChild() QObject::findChildren()
-
提供多语言支持
QObject::tr() QObject::trUtf8()
-
提供反射机制
QObject::inherits(); // 返回继承信息,对象是否是 QObject 继承树中一个类的实例 QObject::objectName(); // 唯一的对象名 QOject::setProperty() 和 QObject::property(); // 通过名字动态设置和动态获取对象属性 QObject::metaObject(); // 返回与该类绑定的 meta-object 对象 QMetaObject::className(); // 在运行时以字符串的形式返回类的名字(无需RTTI) QMetaObject::newInstance(); // 构造该类的一个新实例 qobject_cast<类A>(类B); // 基于元对象系统实现的dynamic_cast,要求A和B都是QObject的子类,效率比dynamic_cast高
MOC
元对象编译器(Meta-Object Compiler, MOC)为每个 QObject
子对象自动生成必要的代码来实现元对象系统的特性,工作流程可以分为以下几个步骤:
- 扫描:MOC 扫描源代码,寻找
Q_OBJECT
宏和其他 Qt 特有的宏Q_XXX
。 - 生成:对于每一个包含
Q_OBJECT
宏的类,MOC 会为其生成一个额外的 C++ 源文件moc_xxxx.cpp
。这个文件包含了每个类实现 Qt 元对象特性所需的元数据和辅助代码。 - 编译:生成的源文件随后被编译器编译,与应用程序的其他部分一起链接。
MOC文件分析
详见《统信UOS应用开发实战教程》P32开始
MOC存在的坑点
声明和实现在同一文件中时
当类的声明和实现都在同一个源文件(.cpp
文件)中时,在这种结构中使用 Q_OBJECT
宏会带来一个问题:由于 MOC 默认只处理头文件,结果是MOC 不会为这些类生成相应的 .moc
文件,从而导致编译错误,因为 Qt 的信号和槽机制依赖于这些 MOC 生成的代码。
为了解决这个问题,开发者需要在源文件的末尾手动包含由 MOC 生成的 moc
文件。这一操作确保了 MOC 生成的代码被正确编译和链接。例如:
// MyClass.cpp
#include <QObject>
class MyClass : public QObject {
Q_OBJECT
public:
MyClass() { /* ... */ }
// ...
};
// MyClass 实现的其他部分
#include "moc_myclass.cpp" // 需要在源文件的末尾包含,以确保在类的定义之后包含 MOC 生成的代码
其他限制
这里的限制主要是指 moc 工具的限制,主要表现在以下几方面:
-
模板类中不能使用 Q_OBJECT。
class SomeTemplate<int> : public QFrame { Q_OBJECT // WRONG ... signals: void mySignal(int); };
-
多重继承时 QObject 必须是第一个,且不支持 QObject 的虚继承。
// CORRECT class SomeClass : public QObject, public OtherClass { ... };
-
函数指针不能作为信号或槽的参数,可以用 typedef 简化函数指针,建议用继承或虚函数代替函数指针。
// WRONG class SomeClass : public QObject { Q_OBJECT public slots: void apply(void (*apply)(List *, void *), char *); };
// CORRECT typedef void (*ApplyFunction)(List *, void *); class SomeClass : public QObject { Q_OBJECT public slots: void apply(ApplyFunction, char *); };
-
枚举类型或 Typedefs 作为信号和槽的参数时必须是
完全限定名称
。原因是 QObject::connect() 检测参数签名时,是比较的数据类型的字面意思。例如,Alignment 和 Qt::Alignment 会当作两种不同的参数类型。class MyClass : public QObject { Q_OBJECT enum Error { ConnectionRefused, RemoteHostClosed, UnknownError }; signals: void stateChanged(MyClass::Error error); };
-
内嵌的类不能有信号与槽。
class A { public: class B { Q_OBJECT public slots: // WRONG void b(); }; };
-
信号槽的返回类型不能是引用。
-
在类的 signals 和 slots 代码块中,只能是信号和槽函数的声明,而不能是其他的声明。
Property
利用元对象系统所提供的动态类型信息,Qt还构建了一套强大的属性系统。
属性系统是基于元对象系统的,因此必须在 QObject
和启用了 Q_OBJECT
宏的类中才能用。
属性是做什么的
在C++中是没有属性概念的,只有成员变量。因为面向对象的思想是抽象封装,属性是类给外部展示的特性。而成员变量属于类的内部信息,直接暴漏出去就破坏了封装性,因为使用者可以对类特性进行直接修改。而属性将取值、赋值的细节进行了封装,外部只能使用它而不能控制它。
而 Qt 提供的这个属性系统,作用就是把类的信息暴露出来成为通用的大家都认识的信息,从而能暴露给 QML 之类的其他语言处理。
通常在界面插件开发、QML中使用属性系统,例如在Qt Creator的设计界面中,控件都是以属性的方式暴露给外部使用者。
如何使用属性
对于属性,常用的操作无非就是读、写、将成员变量导出为属性值、关联信号等。
声明一个属性并赋予读写操作
在 QObject 及其子类中用 Q_PROPERTY
宏声明。它是定义在 QObject 类中的宏,所以只有 QObject 类或者继承自它的类才能使用。
// Q_PPROPERTY 宏
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 | 指定读取属性值的函数,最好是一个 const 函数,函数返回值是该属性的类型或者该属性类型的引用。在没有指定 MEMBER 时,READ 必须指定! |
WRITE | 指定修改属性值的函数,函数参数有且仅有一个,参数类型必须是属性类型或者该属性类型的指针或引用,无返回值(或者说返回 void 值)。可选参数! |
MEMBER | 导出成员变量为属性值,使得成员变量不需要读函数和写函数的支持也能读写。读写用的是QObject 的函数:property() 和 setProperty()。不能与 READ/WRITE 同时使用! |
RESET | 重置属性值为某个默认值,该函数是一个没有参数且返回值是 void 的函数。可选参数! |
NOTIFY | 定义一个信号,当属性值已经变化后,Qt 会发出这个信号。当存在参数为 MEMBER 时,这个信号应该不带参数或者带一个参数,带一个参数时参数为属性的新值。可选参数! |
REVISION | 定义 API 的版本号,使得读/写接口和信号只能在特定的版本中使用。默认值是 0,可选参数! |
DESIGNABLE | 指定 Qt Designer 中是否能编辑这个属性。默认值是 true,可选参数! |
SCRIPTABLE | 指定 QtScript 是否可以访问这个属性。默认值是 true,可选参数! |
STORED | 指定属性是独立属性还是依赖于值其他属性,比如 QWidget::minimumWidth() 依赖于 QWidget::minimumSize()。默认值是 true,可选参数! |
USER | 指定类的这个属性为用户只读或者可编辑,一个类中只能定义一个 USER 属性。默认值是 false,可选参数! |
CONSTANT | 属性指定了 CONSTANT 即成为了一个常量属性。类的不同对象这个属性值必须有不同的值。常量属性没有 write 方法和信号。 |
FINAL | 属性指定了 FIANL 将不能被派生类重写。 |
READ、WRITE、ERSET 指定的函数可以被继承,也能被定义为虚函数。当被多重继承时,取第一个父类的值。
比如 QWidget 这个类,其中使用属性系统的代码如下所示:
Q_PROPERTY(QCursor cursor READ cursor WRITE setCursor RESET unsetCursor)
上面这一行代码就声明了一个 cursor 属性,指明了它是 QCursor 类型的,而且指明了需要用自己的 cursor() 函数来读取这个属性值,指明了用自己的 setCursor() 函数来修改属性值,还指明了用自己的 unsetCursor() 函数进行默认值设置。
指定读取属性值的getter
使用 READ
关键字:
class Widget : public QObject
{
Q_PROPERTY(bool focus READ hasFocus)
Q_OBJECT
public:
bool hasFocus() const;
}
上述代码指定getter为 hasFocus() 函数
指定修改属性值的setter
使用 WRITE
关键字:
class Widget : public QObject
{
Q_PROPERTY(bool focus WRITE setFocus)
Q_OBJECT
public:
bool hasFocus() const;
void setFocus(bool on);
}
上述代码指定setter为 setFocus() 函数。注意 setter 函数返回值必须为 void
将类的变量导出为一个属性
注意上述的属性值 cursor 可不是 QWidget 的一个成员变量,要想将 QWidget 类中的某个变量导出成为一个属性值,应该用 MEMBER
关键字将变量注册到 Qt 的属性系统。如下:
Q_PROPERTY(bool focus MEMBER m_focus NOTIFY focusChanged)
...
bool hasFocus() const;
void setFocus(bool on);
signals:
void focusChanged();
private:
bool m_focus;
上面的代码把 QWidget 类中的 m_focus 成员变量导出为一个属性值,并且起了个新名字“focus”。那么外界所能看到的或者说只认可的属性值只有 focus 而不是 m_focus。
同时,MEMBER
关键字会自动绑定代码中读写 m_focus 的代码,同时使得 focus 是一个可读可写的属性。注意此时不能再设置 READ/WRITE
,因为可读可写特性只能赋予一次。
给属性值设置关联的信号
如果我们希望某个属性值变化时能发射出信号,要使用 NOTIFY
关键字:
实际操作一下读/写属性值?
上文我们创建了 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);
...
}
在程序运行过程中动态添加属性
QObject::setProperty()
可以在运行时给实例添加属性值。
-
属性已经存在于对象中
给的属性值类型正确,这个值就被添加到对象中了,函数返回 true。否则属性值不改变,函数返回 false。
-
类中没有这个属性
自动在对象中添加新属性,并赋值,但是函数返回 false。
动态属性只是添加到 QObject 中,而不是 QMateObject 中,故无法通过 QMetaProperties 方法获取到。
移除动态属性可以通过给 setProperty 传入一个空的 QVariant 参数
如何自定义属性类型
自定义的属性类型,需要用 Q_DECLARE_METATYPE
宏进行注册,这样就可以存储在QVariant中了。
如何给类添加额外的属性信息
除了正规常用的属性外,我们还可以用 Q_CLASSINFO
宏给类添加额外的属性信息,语法就是“键值-对”形式。例如:
Q_CLASSINFO("Version", "3.0.0")
那么在程序运行的过程中,随时都可以调用 QMetaObject::classInfo()
函数来获取这些额外属性信息。
完整的示例
上文讲解的 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);