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