【Qt元对象系统解析】

发布于:2024-05-07 ⋅ 阅读:(29) ⋅ 点赞:(0)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


概述

Qt实质上是一个跨平台的C++开发类库,是用标准 C++编写的类库,它为开发GUI应用程序和非 GUI应用程序提供了各种类。Ot对标准 C++进行了扩展,引入了一些新的概念和功能,例如信号与槽、对象属性等。Qt的元对象编译器(Meta-ObjectCompiler,MOC)是一个预处理器,在源程序被编译前先将这些Qt 特性的程序转换为标准 C++兼容的形式,然后再由标准C++编译器进行编译。这就是为什么在使用信号与槽机制的类里,必须添加一个Q_OBJECT宏的原因,只有添加了这个宏,moc才能对类里的信号与槽的代码进行预处理。Qt Core 模块是 Qt 类库的核心,所有其他模块都依赖于此模块,如果使用 qmake 来构建项目,QtCore 模块则是被自动加入的。Qt为 C++语言增加的特性就是在 QtCore 模块里实现的,这些扩展特性由 Qt的元对象系统实现包括信号与槽机制、属性系统、动态类型转换等。


一、元对象系统

Qt的元对象系统(Meta-Object System)提供了对象之间通信的信号与槽机制、运行时类型信息和动态属性系统。
元对象系统由以下三个基础组成:
1、QObject 类是所有使用元对象系统的类的基类。
2、类如果要使用元对象的特性,比如信号与槽、动态属性,必须在类的private部分声明Q_OBJECT宏。
3、MOC(元对象编译器)为每个QObiect的子类提供必要的代码来实现元对象系统的特性。构建项目时,MOC 工具读取C++源文件,当它发现类的定义里有QOBJECT宏时,它就会为这个类生成另外一个包含有元对象支持代码的C++源文件,这个生成的源文件连同类的实现文件一起被编译和连接。

除了信号与槽机制,元对象还提供其他功能:
QObject::metaObject()函数返回类关联的元对象,元对象类 QMetaObject 包含了访问元对象的一些接口函数,例如 QMetaObject::className()函数可在运行时返回类的名称字符串。

QObject *obj = new QPushButton;
obj->meta0bject()->className();
//返回"QPushButton"

QMetaObject::newInstance()函数创建类的一个新的实例。
QObject::inherits(const char *className)函数判断一个对象实例是否是名称为 className 的类或 QObject的子类的实例。例如:

// QTimer 是Q0bject 的子类
QTimer *timer = new imer;
//返回 true
timer->inherits("QTimer");
//返回 true
timer->inherits("QObject");
//返回 false,不是QAbstractButton 的子类
timer->inherits("QAbstractButton");

QObject::tr()和 QObject::trUtf8()函数可翻译字符串,用于多语言界面设计。
QObject::setProperty()和 QObject::property()函数用于通过属性名称动态设置和获取属性值。
对于 QObject及其子类,还可以使用 qobject_cast()函数进行动态投射(dynamic cast)。例如,假设 QMyWidget 是 QWidget 的子类并且在类定义中声明了Q_OBJECT宏。创建实例使用下面的语句:

QObject *obj = new QMywidget;

变量 obj 定义为 QObject 指针,但它实际指向 QMyWidget 类,所以可以正确投射为QWidget,即:

QWidget *widget = qobject_cast<Qwidget*>(obj);

从 QObiect 到 QWidget 的投射是成功的,因为obj实际是 QMyWidget 类,是 QWidget 的子类。也可以将其成功投射为 QMyWidget,即:

QMyWidget *mywidget = qobject_cast<QMywidget *>(obj);

投射为 QMyWidget是成功的,因为qobject_cast()并不区分 Qt 内建的类型和用户自定义类型。但是,若要将 obj 投射为 QLabel 则是失败的,即:

QLabel *label = qobject_cast<QLabel *>(obj);

这样投射是失败的,返回指针 label为NULL,因为QMyWidget 不是 QLabel 的子类。
使用动态投射,使得程序可以在运行时对不同的对象做不同的处理。

二、信号与槽

1.connect()函数的不同参数形式

QObject::connect()函数有多重参数形式,一种参数形式的函数原型是:

QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal
const QObject *receiver, const char *method,Qt: :ConnectionType type = Qt: :AutoConnection)

使用这种参数形式的connectO)进行信号与槽函数的连接时,一般句法如下:

connect(sender, SIGNAL(signal()), receiver, SLOT(slot()));

这里使用了宏 SIGNAL()和 SLOT()指定信号和槽函数,而且如果信号和槽函数带有参数,还需注明参数类型,如:

connect(spinNum, SIGNAL(valueChanged (int)), this, SLOT(updateStatus(int));

另外一种参数形式的connect()函数的原型是:

QMetaObject::Connection Q0bject::connect(const Q0bject* sender, const 0MetaMethod& signal, const QObject* receiver, const QMetaMethod& method, Qt::ConnectionType type = Qt::AutoConnection)

对于具有默认参数的信号与槽(即信号名称是唯一的,没有参数不同而同名的两个信号),可以使用这种函数指针形式进行关联,如:

connect(lineEdit, &QLineEdit::textChanged, this, &widget::on_textChanged) ;

QLineEdit 只有一个信号 textChanged(QString),在自定义窗体类 widget 里定义一个槽函数on_textChanged(QString),就可以用上面的语句将此信号与槽关联起来,无需出现函数参数。这在信号的参数比较多时更简便一些。
而对于具有不同参数的同名信号就不能采用函数指针的方式进行信号与槽的关联,例如QSpinBox有两个valueChanged()信号,分别是:

void QSpinBox::valueChanged(int i)
void QSpinBox::valueChanged(const QString &text)

即使在自定义窗体 widget 里定义了一个槽函数,如:

void onValueChanged(int i);

在使用下面的语句进行关联时,编译会出错。

connect(spinNum, &QSpinBox::valueChanged, this, &widget::onValuechanged);

不管是哪种参数形式的 connect()函数,最后都有一个参数 Qt::ConnectionType type,缺省值为Qt::AutoConnection。枚举类型 Qt::ConnectionType 表示了信号与槽之间的关联方式,有以下几种取值。
1、Qt::AutoConnection(缺省值):如果信号的接收者与发射者在同一个线程,就使用 Qt::DirectConnection方式;否则使用 Qt:QueuedConnection方式,在信号发射时自动确定关联方式。
2、Qt:DirectConnection:信号被发射时槽函数立即执行,槽函数与信号在同一个线程。
3、Qt::QueuedConnection:在事件循环回到接收者线程后执行槽函数,槽函数与信号在不同的线程。
4、Qt:BlockingQueuedConnection:与Qt::QueuedConnection 相似,只是信号线程会阻塞直到槽函数执行完毕。当信号与槽函数在同一个线程时绝对不能使用这种方式,否则会造成死锁。

2. 使用 sender()获得信号发射者

在槽函数里,使用 QObject:sender()可以获取信号发射者的指针。如果知道信号发射者的类型,可以将指针投射为确定的类型,然后使用这个确定类的接口函数。
例如,在 QSpinBox 的 valueChanged(int )信号的槽函数里,可以通过 sender()和 qobject_cast获得信号发射者的指针,从而对信号发射者进行操作。

SpinBox* spinBox = qobject_cast<QSpinBox*>(sender());

3. 自定义信号及其使用

在自己设计的类里也可以自定义信号,信号就是在类定义里声明的一个函数,但是这个函数无需实现,只需发射(emit)。
例如,在下面的自定义类 QPerson 的 signals 部分定义一个信号 ageChanged(int )。

class QPerson : public QObject 
{
	Q_OBJECT
private:
	int m_age=10;
public :
	void incAge();
signals :
	void ageChanged( int value);
}

信号函数必须是无返回值的函数,但是可以有输入参数。信号函数无需实现,只需在某些条件下发射信号。例如,在incAge()函数中发射信号,其代码如下。

void QPerson::incAge()
{
	m age++;
	emit ageChanged(m age);//发射信号
}

在 incAgeO)函数里,当私有变量 m age 变化后,发射信号 ageChanged(int),表示年龄发生了变化。至于是否有与此信号相关联的槽函数,信号发射者并不管。如果在使用QPerson 类对象的程序中为此信号关联了槽函数,在incAge0)函数里发射此信号时,就会执行相关联的槽函数。至于是否立即执行槽函数,发射信号的线程是否等待槽函数执行完之后再执行后面的代码,与connect()函数设置信号与槽关联时设置的连接类型以及信号与槽是否在同一个线程有关。