元对象(meta object)意思是描述另一个对象结构的对象,比如获得一个对象有多少成员函数,有哪些属性。在Qt中,我们将要用到的是QMetaObject这个类。
元对象系统基于以下3点:
- 以QObject作为基类;
- 类声明的私有区域中,Q_Object宏指令使我们能够使用元对象的特性,比如动态属性、信号、槽等;
- 元对象编译器(Meta-Object Compiler moc)为QObject子类生成具有元对象特性的代码
我们可以通过QObject类的一个成员函数获得该类的元对象:
QMetaObject *QObject::metaObject() const
通过这个元对象,进而可以获取一个QObject对象的更多信息:
返回运行时类的名称(不需要C++中的运行时类型识别机制RTTI)
QMetaObject::className()
返回类中方法的个数
QMetaObject::methodCount()
以上只是元对象的简单介绍,记住元对象系统的3点特性。之所以要介绍元对象,因为Qt中很多用法是基于元对象的,如果不支持元对象,比如没有继承自QObject,那么很多东西将无法使用
类型识别
众所周知,C++中使用dynamic_cast和typeid这两个运算符进行运行时类型识别(RTII),但是Qt提供另外两种运行时类型识别方法:
qobject_cast 和 QObject::inherits()
看名字就可以知道,这两个方法都是基于QObject的,也就是元对象系统。
cpp
if (QLabel *label = qobject_cast<QLabel *>(obj))
{
label->setText(tr("Ping"));
}
else if (QPushButton *button = qobject_cast<QPushButton *>(obj))
{
button->setText(tr("Pong!"));
}
qobject_cast
:
qobject_cast
是 Qt 提供的一个宏,专门用于在 QObject 层次结构中进行安全的向下转型。- 它仅适用于继承自
QObject
的类,主要用于在使用 Qt 的对象树时,通过指针进行类型转换。 - 如果对象类型不匹配,
qobject_cast
返回nullptr
。
dynamic_cast
:
dynamic_cast
是 C++ 的运行时类型识别(RTTI)操作符,用于在 C++ 的继承层次结构中执行类型安全的向下转型。- 它对于所有的 C++ 类型都有效,不仅仅是
QObject
的派生类。 - 如果类型不匹配,
dynamic_cast
返回nullptr
(对于指针)或抛出std::bad_cast
异常(对于引用)。
QObject::inherits(const char *className)的速度相对慢一些,所以尽可能使用qobject_cast。
Qt中的属性
1. 自定义属性
我们可能已经接触到很多Qt中的属性了,比如qreal类型的opacity属性表示"透明度",QRect类型的geometry表示"几何位置和大小",QPoint类型的pos属性代表"位置"。所谓属性,也就是类中的一个数据成员,我们可以获取(get)和设置(set)。除了Qt中一些类已经具备的属性,我们还可以自定义属性,也就是定义一种访问数据成员的方式。
在一个继承自QObject的类中使用 Q_PROPERTY 宏指令,比如:
cpp
Q_PROPERTY(bool focus READ hasFocus)
Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled)
Q_PROPERTY(QColor color MEMBER m_color NOTIFY colorChanged)
不要被这个用法搞晕了,其实很简单,刚开始指定属性类型和名称 ,然后READ 表示获取属性值的方法,一般这两点是必须的。其他都是可选的,比如WRITE 表示设置属性值得方法,MEMBER 表示这个属性在类中数据成员的名称 ,NOTIFY表示属性改变发出的信号。
2. 属性的类型
由上可知,属性的类型可以是bool、QString、QRect等等,我们可以通过 QVariant::Type 的枚举值获得所有可用于属性的类型。可以查到,它不支持枚举类型,但可以通过Q_ENUM来设置:
cpp
enum Priority { High, Low, VeryHigh, VeryLow };
Q_ENUM(Priority)
Q_PROPERTY(Priority priority READ priority WRITE setPriority NOTIFY priorityChanged)
当然,自定义的类型也是支持的,需要通过Q_DECLARE_METATYPE 注册元类型:
cpp
struct MyStruct
{
int i;
...
};
Q_DECLARE_METATYPE(MyStruct)
3. 属性的读与写
我们可以直接使用get和set方法来读写属性,也可以通过QObject与QMetaObject来间接地读写属性。
首先是设置属性值:
比如类QAbstractButton有一个"down"的属性,表示按钮是否被按下,它有一个成员函数 QAbstractButton::setDown() 来改变属性值,同时,我们也可以通过 QObject::setProperty() 对其进行设置:
cpp
QPushButton *button = new QPushButton;
QObject *object = button;
button->setDown(true);
object->setProperty("down", true);
值得注意的是,setProperty()这个函数不但可以改变属性值,也可以在运行时动态地为对象添加属性。
接下来是读取属性值:
如果有get函数,可以直接调用它,当然也可以通过 QObject::property() 来获取属性,它的返回值是 QVariant 类型的,通过 canConvert() 进行判断,然后将其转换为所需的类型。
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);
...
}
invokeMethod()
Qt中的信号槽机制是以元对象为基础的,通过名称以类型安全的方式来间接调用槽函数。
当调用槽函数时,实际是由invokeMethod()完成的。
比如显示一个窗口,一般是通过show()函数来完成,不过我们还能这样做:
cpp
MyWidget w;
QMetaObject::invokeMethod(&w, "show");
上面讲了如何将成员变量注册进元对象系统,那么对于成员函数,该怎么做呢?
在声明一个类的成员函数时,通过使用 Q_INVOKABLE 宏进行注册,可以使它们能够被元对象系统调用。
cpp
class Window : public QWidget
{
Q_OBJECT
public:
Window();
void normalMethod();
Q_INVOKABLE void invokableMethod();
};
反射机制
反射机制的特性
reflection 模式(反射模式或反射机制):是指在运行时,能获取任意一个类对象的所有类型信息、属性、成员函数等信息的一种机制。
元对象系统提供的功能之一是为QObject 派生类对象提供运行时的类型信息及数据成员的当前值等信息,也就是说,在运行阶段,程序可以获取 QObject 派生类对象所属类的名称、父类名称、该对象的成员函数、枚举类型、数据成员等信息,其实这就是反射机制。
因为 Qt 的元对象系统必须从 QObject 继承,又从反射机制的主要作用可看到,Qt 的元对象系统主要是为程序提供了 QObject 类对象及其派生类对象的信息,也就是说不是从 QObject 派生的类对象,则无法使用 Qt 的元对象系统来获取这些信息。
Qt 具体实现反射机制的方法
Qt 使用了一系列的类来实现反射机制,这些类对对象的各个方面进行了描述,其中QMetaObject 类描述了 QObject 及其派生类对象的所有元信息,该类是 Qt 元对象系统的核心类,通过该类的成员函数可以获取 QObject 及其派生类对象的所有元信息,因此可以说 QMetaObject 类的对象是 Qt 中的元对象。注意:要调用 QMetaObject 类中的成员函数需要使用 QMetaObject 类型的对象。
对对象的成员进行描述:一个对象包含数据成员、函数成员、构造函数、枚举成员等成员,在 Qt 中,这些成员分别使用了不同的类对其进行描述,比如函数成员使用类QMetaMethod 进行描述,属性使用 QMetaProperty 类进行描述等,然后使用QMetaObject 类对整个类对象进行描述,比如要获取成员函数的函数名,其代码如下:
cpp
QMetaMethod qm = metaObject->method(1); //获取索引为 1 的成员函数
qDebug()<<qm.name()<<"\n"; //输出该成员函数的名称。
信号和槽函数的实现原理
Qt中信号和槽的实现原理基于元对象系统(Meta-Object System,MOS)。以下是信号和槽的实现原理:
-
元对象系统: 在包含信号和槽的类声明中,使用
Q_OBJECT
宏。这个宏会告诉 Qt 的元对象编译器(MOC)处理这个类,生成额外的代码。这些额外的代码包括元对象的描述信息,以及支持信号和槽机制的相关数据。 -
元对象: Qt 的元对象是对类的额外描述,它包含了类的属性、方法、信号和槽的信息。这些信息被存储在元对象表中。
-
信号和槽的注册: 在包含信号和槽的类的构造函数中,MOC 自动生成的代码会注册这些信号和槽。这个注册过程将信号和槽的信息添加到元对象表中。
-
连接: 使用
connect
函数建立信号和槽之间的连接。在连接时,Qt 运行时系统会查找元对象表,找到信号和槽的信息。这时,建立了信号和槽之间的关联。 -
emit关键字: 当使用
emit
关键字发射一个信号时,实际上是调用了生成的槽函数。这个槽函数内部会遍历连接列表,调用所有连接到该信号的槽函数。 -
运行时调用: Qt 使用元对象系统在运行时实现信号和槽的调用。这样,它允许在不知道类的具体实现的情况下,进行动态的信号和槽的连接和调用。