5. Qt 核心

经过前面章节的介绍,我们应该对Qt有了一个初步的印象,本章将介绍下Qt Core机制。

Qt是基于C++的应用程序框架,拥有完备的C++图形库和集成了一系列代码模块,为了满足用户界面编程的运行时效率和高度灵活性, Qt Core在C++标准上进行了扩展,而这些扩展的特性是Qt核心机制的重要组成部分, 接下来我们将介绍下Qt Core的核心特点:

  • 元对象系统(The Meta-Object System)

  • 属性系统(The Property System)

  • 对象模型 (Object Model)

  • 对象树与所有权 (Object Trees & Ownership)

  • 信号与槽 (Signals & Slots)

提示

对于初学者,这些概念理解起来可能会比较吃力,建议先往后面学,边实践边学习,自然就会理解这些概念。

5.1. 对象模型

标准C++对象模型为面向对象编程提供了有效的实时支持,但是它的静态特性在一些领域中表现的不够灵活。为满足用户界面编程的实时性和灵活性, Qt在标准C++对象模型上扩展出Qt对象模型,这样Qt就将C++的速度和Qt对象模型的灵活性相结合起来。

在C++标准的基础上添加了以下特性:

  • 提供一种非常强大的无缝对象通信机制,称为信号和槽

  • 提供可查询和可设计的对象属性

  • 分层且可查询的对象树,以自然方式组织对象所有权

  • 受保护的指针(QPointer),在销毁引用的对象时将其自动设置为0,这与普通的C ++指针不同,在对象被销毁时,C ++指针变为悬挂的指针

  • 跨库工作的动态投射

  • 强大的事件和事件筛选器

  • 复杂的间隔驱动定时器,可以在事件驱动的GUI中优雅地集成许多任务

  • 面向国际化的上下文字符串翻译

  • 支持自定义类型创建

许多Qt特性的实现都是使用标准C++,继承自QObject,但是有些是例外,如对象通信机制(signals and slots)和动态属性系统(dynamic property system), 需要Qt自己的元对象编译器(moc)提供的元对象系统。

下面这些类是构成Qt对象模型的基础:

类名

作用

QMetaClassInfo

类的一些信息

QMetaEnum

有关枚举器的元数据

QMetaMethod

有关成员函数的元数据

QMetaObject

包含有关Qt对象的元信息

QMetaProperty

有关属性的元数据

QMetaType

管理元对象系统中的命名类型

QObject

所有Qt对象的基类

QObjectCleanupHandler

观察多个QObject的生命周期

QPointer

提供指向QObject的受保护指针的模板类

QSignalBlocker

QObject :: blockSignals() 周围的异常安全包装器

QSignalMapper

捆绑可识别发件人的信号

QVariant

充当最常见Qt数据类型的并集

我们下面简单介绍几个,关于这些基础类更多的信息参考下 Qt Object Model

5.1.1. QObject

QObject是Qt对象模型的核心,也是所有Qt对象的基类。

QObject是一个基础类,拥有一系列成员变量和操作接口,我们通过C++来继承和派生来使用这个类。 QObject配合Qt属性系统提供对象间通信,通过特定的函数接口来处理、过滤事件;QObject本身也提供基本的计时器支持。

在Qt中,QObjects将自己组织在 对象树 中,当使用另一个对象作为父对象创建QObject时, 该对象会自动将其自身添加到父对象的children() 列表中。父对象拥有该对象的所有权,也就是说,它将在父对象析构函数中自动删除其子对象。 在开发中可以使用findChild()或findChildren()根据名称和可选的类型查找子对象。

QObject的构建/销毁顺序

当在堆上创建QObject时(用new创建),可以以任何顺序构造,也可以从对象树中以任何顺序删除。

删除对象树中的任何QObject时,如果对象具有父对象,则析构函数会自动从其父对象中删除该对象。 如果对象有子对象,则析构函数会自动删除每个子对象。

QObject具有线程亲和性

当QObject接收到队列中的信号或发布的事件时,该对象的槽函数或事件处理程序将在该对象所在的线程中运行。 线程之外将无法接收排队的信号或已发布的事件。

默认情况下,QObject位于创建它的线程中。可以使用thread() 查询对象的线程亲和力,并使用moveToThread()来更改所在线程。 除此以外,所有QObject必须与其父代驻留在同一线程中。

QObject的成员变量不会自动成为其子对象,必须通过将指针传递给子对象的构造函数或通过调用 setParent() 来指定其父对象。

线程相关的可以参考下后面线程与进程章节,Q_OBject的所有成员函数参考下 这里

5.1.2. QMetaObject

QMetaObject 类包含QObject的一些描述信息(类型信息和signal&slot信息)。

如果一个类的声明中包含了Q_OBJECT宏,编译器会生成代码来实现这个类对应的QMetaObject类,并重载QObject::metaObject()方法来返回这个QMetaObject类的实例引用。 这样当通过QObject类型的引用调用metaObejct方法时,返回的是这个引用的所指的真实对象的metaobject。

5.2. 元对象系统简介

元对象系统(Meta-Object System)是对c++的扩展,让Qt更适合GUI编程,它提供了运行时类型信息、动态属性系统和信号与槽机制等。

元对象系统的功能建立在下面三个部分:

  • Q_OBject类作为提供元对象系统特性的基类;

  • 类的开头插入Q_OBJECT宏,这样才能启用元对象的特性,例如动态属性、信号和槽等等;

  • 元对象编译器 moc 是处理 Qt关于C++扩展的程序,为每个QObject子类提供实现元对象特性所需的代码。

在构建项目的时候,qmake创建makefile会自动调用生成moc的构建规则。 在预处理阶段 moc 工具会读取 C++ 头文件,如果找到一个或多个包含 Q_OBJECT宏 的类声明,moc将会生成一个包含元对象支持的C++源文件, 这个生成的源文件要么被#include到类的源文件中,再由GNU编译套件中的C++编译器进行编译和链接。

除了信号与槽机制,元对象系统还提供如下功能(通过直接或者间接继承 QObjectQMetaObject 而来):

  • QObject :: metaObject() 返回该类的关联元对象。

  • QMetaObject :: className() 在运行时以字符串形式返回类名称,而无需通过C ++编译器提供本机运行时类型信息(RTTI)支持。

  • QObject :: inherits() 函数返回对象是否是继承QObject继承树中指定类的类的实例。

  • QObject :: tr() 和QObject :: trUtf8() 转换字符串以进行国际化。

  • QObject :: setProperty() 和QObject :: property() 通过名称动态设置和获取属性。

  • QMetaObject :: newInstance() 构造该类的新实例。

QObject类可以使用qobject_cast() 执行动态强制转换。qobject_cast() 函数类似于标准C++中的动态dynamic_cast(),其优点是不需要RTTI支持,并且它可以跨动态库运行。 它尝试将其参数强制转换为尖括号中指定的指针类型,如果对象的类型正确(在运行时确定),则返回非零指针;如果对象的类型不兼容,则返回nullptr。

典型的使用如下面的代码:

lubancat_qt_tutorial_code/QtCore/widget.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//省略....
ui->spinStu1->setProperty("isStu1",true);        //isStu1动态属性
ui->spinStu2->setProperty("isStu2",true);        //isStu2动态属性
//省略....
connect(ui->spinStu1,SIGNAL(valueChanged(int)), this,SLOT(do_spinChanged(int)));
connect(ui->spinStu2,SIGNAL(valueChanged(int)), this,SLOT(do_spinChanged(int)));

void Widget::do_spinChanged(int value)
{
    QSpinBox *spinBox = qobject_cast<QSpinBox *>(sender());   //sender()获取信号发射者
    if (spinBox->property("isStu1").toBool())                 //根据动态属性判断是哪个spinBox,这里只有两个,此处就简单判断
    {
        stu1->setScore(value);
        ui->textEdit->append("spinStu1 改变");
    }
    else
        stu2->setScore(value);
        ui->textEdit->append("spinStu2 改变");
}

我们使用两个QSpinBox,通常来说一个QSpinBox的valueChanged()信号对应着一个do_spinChanged(int)槽函数。 而上面的写法呢,就是多个QSpinBox对应这一个槽函数,在这个槽函数中,我们通过qobject_cast()直接获得发信号的QSpinBox, 再通过判断按钮的某些属性(比如objectName()或者设置的动态属性)来确定具体哪个QSpinBox值改变了。

QSpinBox只能通过qobject_cast()获取QSpinBox的子类, qobject_cast()执行成功会得到对象的指针,获取失败则返回NULL。

5.3. 属性系统

Qt提供了一个复杂的属性系统,类似于一些编译器供应商提供的属性系统。但是,作为一个独立于编译器和平台的库, Qt不依赖于非标准的编译器特性。Qt的属性系统在任何标准的C++编译器中都能起作用。

在ui设计界面中,我们在Qt Designer的属性框中可以看到组件的各种属性,也可以修改这些属性的值:

core01.png

属性基于元对象系统实现,使用宏Q_PROPERTY定义属性,下面是QWidget类中的部分属性:

qwidget.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
...
Q_PROPERTY(bool modal READ isModal)
Q_PROPERTY(Qt::WindowModality windowModality READ windowModality WRITE setWindowModality)
Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled)
Q_PROPERTY(QRect geometry READ geometry WRITE setGeometry)
Q_PROPERTY(QRect frameGeometry READ frameGeometry)
Q_PROPERTY(QRect normalGeometry READ normalGeometry)
Q_PROPERTY(int x READ x)
Q_PROPERTY(int y READ y)
Q_PROPERTY(QPoint pos READ pos WRITE move DESIGNABLE false STORED false)
Q_PROPERTY(QSize frameSize READ frameSize)
Q_PROPERTY(QSize size READ size WRITE resize DESIGNABLE false STORED false)
Q_PROPERTY(int width READ width)
Q_PROPERTY(int height READ height)
Q_PROPERTY(QRect rect READ rect)
...

属性类似于类数据成员,但是它具有可通过元对象系统访问的功能。 宏Q_PROPERTY定义属性的语法如下:

属性语法
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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]
        [REQUIRED])

其关键词含义如下:

  • type 表示属性类型,属性类型可以是QVariant支持的任何类型,也可以是用户定义的类型。

  • READ 表示该属性为只读属性,且指定getFunction()来获取该属性的值,

  • WRITE 表示该属性可写,指定setFunction()来设置该属性的值,只读属性不需要WRITE功能;

  • MEMBER 表示指定成员变量与属性关联,这个属性可读可写,指定getFunction来读属性值,setFunction来设置属性值;

  • RESET 可选项,指定resetFunction()来设置默认值;

  • NOTIFY 可选项,指定该类中的一个现有信号notifySignal(),只要该属性的值发生更改,该信号就会发出;

  • REVISION 可选项,它定义将在API的特定版本中使用的属性及其通知程序信号(通常用于QML)

  • DESIGNABLE 可选项,指示该属性在GUI设计工具(例如Qt Designer)的属性编辑器中是否可见;

  • SCRIPTABLE 可选项,指示该属性是单独存在还是依赖于其他值。

  • USER 可选项,指示该属性是被指定为该类的面向用户还是可编辑的属性

  • CONSTANT 可选项,指示该属性值是恒定的

  • FINAL 可选项,指示该属性不会被派生类覆盖

  • REQUIRED 可选项,指示该属性应由该类的用户设置

lubancat_qt_tutorial_code/QtCore/student.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#ifndef STUDENT_H
#define STUDENT_H

#include <QObject>

class Student : public QObject
{
    Q_OBJECT

//...省略....

    //定义属性
    Q_PROPERTY(int score READ getScore WRITE setScore NOTIFY scoreChanged)     //定义属性score
    Q_PROPERTY(QString name MEMBER m_name)      //定义属性name
    Q_PROPERTY(int age MEMBER m_age)            //定义属性age

//...省略....
  • 第13行 定义一个类型为int,名称为score的属性,可以使用getScore()来读取deta的值,可以使用setScore()来设置score的值,当score的值发生改变的时候,会产生scoreChanged()信号

  • 第14行 定义了一个QString类型,名称是name的属性,并且指定成员变量m_name与关联

  • 第15行 定义了一个int类型,名称是age的属性

在Qobject提供了两个函数直接通过属性名来访问和设置属性,通过QObject::setProperty()函数设置属性,通过QObject::Property()函数读取属性值。 很多时候,我们并不知道我们使用的类有那些属性,以及如何去操作这些属性,我们可以使用QMetaObject的函数获取属性的数量,然后可以遍历属性。

lubancat_qt_tutorial_code/QtCore/widget.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include "widget.h"
#include "ui_widget.h"
#include <QMetaProperty>

Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    stu1 = new Student("xiaoming", this);
    //设置属性值
    stu1->setProperty("score",95);
    stu1->setProperty("age",20);
    stu1->setProperty("sex","Boy");  //sex是动态属性

    //关联
    connect(stu1,SIGNAL(scoreChanged(int)),this,SLOT(do_scoreChange(int)));

    stu2 = new Student("xiaomei", this);
    //设置属性值
    stu2->setProperty("score",85);
    stu2->setProperty("age",21);
    stu2->setProperty("sex","Girl");  //sex是动态属性
    connect(stu2,SIGNAL(scoreChanged(int)),this,SLOT(do_scoreChange(int)));

    ui->spinStu1->setValue(stu1->property("score").toInt());  //通过property获取属性值
    ui->spinStu2->setValue(stu2->property("score").toInt());  //通过property获取属性值
//.....省略

动态属性

使用QObject :: setProperty()可以在运行时用于向类的实例添加新属性。

如果我们设置的属性已经存在且类型相同,则设置的值将会赋予给存在的属性,并返回true。 如果这时的属性存在但是属性的类型不兼容,则不更改属性,并返回false。

但是,如果QObject中不存在具有给定名称的属性(即,该类中未使用Q_PROPERTY()声明该属性), 则会将具有给定名称和值的新属性自动添加到QObject中,但仍返回false。 这意味着不能使用返回false来确定是否实际设置了特定的属性,除非事先知道该属性已经存在于QObject中。

需要注意的是,动态属性是按实例添加的,它们是添加到QObject而不是QMetaObject的。 通过将属性名称和无效的QVariant值传递给QObject::setProperty()。

使用QObject::property()查询动态属性,就像在编译时使用Q_PROPERTY()声明的属性一样。

lubancat_qt_tutorial_code/QtCore/widget.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
//....省略.....
ui->spinStu1->setProperty("isStu1",true);        //isStu1动态属性
ui->spinStu2->setProperty("isStu2",true);        //isStu2动态属性
//....省略.....

void Widget::do_spinChanged(int value)
{
    QSpinBox *spinBox = qobject_cast<QSpinBox *>(sender());   //获取信号发射者
    if (spinBox->property("isStu1").toBool())                 //根据动态属性判断是哪个spinBox
    {
        stu1->setScore(value);
        ui->textEdit->append("spinStu1 改变");
    }
    else
    {
        stu2->setScore(value);
        ui->textEdit->append("spinStu2 改变");
    }
}
  • 第2~3行,使用setProperty()设置属性,如果属性名称是不存在的,就会重新定义一个属性,这时的属性就是动态属性

  • 第9行,根据动态属性判断根据动态属性判断是哪个spinBox,然后调用对应student的setScore函数

自定义属性

属性使用的自定义类型需要使用Q_DECLARE_METATYPE()宏进行注册,以便可以将其值存储在QVariant对象中。 这使得它们既适用于在类定义中使用Q_PROPERTY()宏声明的静态属性,又适用于在运行时创建的动态属性。

附加属性

QMetaClassInfo 提供了有关类的附加信息, 类信息项是简单的一个键值对,在源代码中使用宏Q_CLASSINFO()设置,查询检索类信息使用name()和value()。示例:

lubancat_qt_tutorial_code/QtCore/widget.cpp
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MyClass
{
    Q_OBJECT
    //定义附加的类信息
    Q_CLASSINFO("author", "Embedfire")
    Q_CLASSINFO("url", "https://embedfire.com/")

public:

};

void Widget::on_btnObjectInfo_clicked()
{
    //...省略
    //获取元对象
    const QMetaObject *meta=obj->metaObject();
    //遍历附加的类信息
    for (int i=meta->classInfoOffset();i<meta->classInfoCount();++i)
    {
        QMetaClassInfo classInfo=meta->classInfo(i);
        ui->textEdit->appendPlainText(
            QString("Name=%1; Value=%2").arg(classInfo.name()).arg(classInfo.value()));
    }
}

5.4. 对象树

Qt中对使用QObject及其子类创建的对象,用 对象树(object tree) 的形式来组织和管理。 当你创建一个QObject时设置一个父对象,它会被添加到父对象的children()列表中,当父对象被删除时,其子对象就会全部自动删除。

这种组织方法非常适合用户GUI,例如:QShortcut(键盘快捷键)是相关窗口的子窗口,因此当用户关闭该窗口时,快捷键也会被删除。

使用Qt Creator创建一个工程(QWidget),完成后向项目中添加新文件,模板选择C+ +类,类名为Student,基类为QObject。 示例程序(部分):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget w;
    w.show();
    return a.exec();
}

////////////

class Student : public QObject
{
    Q_OBJECT

public:
    explicit Student(QString name, QObject *parent);
    ~Student();                                //析构函数

private:
QString m_name;
};
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
Student::Student(QString name , QObject *parent) : QObject{parent}
{
    m_name = name;
}

Student::~Student()
{
    qDebug("Student对象被删除");
}

Widget::~Widget()
{
    delete ui;
    qDebug() << "删除 widget";
}

上面看出,其中Widget对象w是建立在栈上的,创建了一个Student类,并且析构函数中,Student的对象被销毁时,就会输出相应的信息。

在主窗口中,堆上创建(使用new操作符)一个Student类的对象,并指定Widget为其父窗口:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
Widget::Widget(QWidget *parent)
    : QWidget(parent)
    , ui(new Ui::Widget)
{
    ui->setupUi(this);

    // 创建Student对象,指定Widget为父部件
    stu1 = new Student("xiaoming", this);

}

运行程序,当Widget窗口被销毁时,将输出信息输出信息为:

删除 widget
Student对象被删除

在Qt中经常只看到new操作而看不到delete操作,因为而当窗口部件销毁时会自动销毁其子部件,这些都是Qt的对象树所完成的。

5.5. 信号与槽

在GUI编程中,我们经常需要在改变一个组件的同时,通知另一个组件做出响应,这种对象间的通信,我们可以使用回调来实现(利用函数指针),这样处理有缺点:类型不安全和紧耦合。 在Qt中,我们可以选择 信号和槽 实现对象之间的通信,当发生特定事件时会发出信号,而槽就是响应特定信号而调用的函数。

信号和插槽用于对象之间的通信。信号与槽机制是Qt的主要功能也是与其他框架提供的功能最大不同的部分。 部分框架使用回调函数来实现对象间的通信,与回调相比,信号和插槽由于提供了更大的灵活性而稍慢一些,尽管实际应用中的差异并不明显。 通常,发出连接到某些插槽的信号的速度比使用非虚拟函数调用直接调用接收器的速度大约慢十倍。 这是定位连接对象,安全地迭代所有连接(也就是检查后续接收方在发射期间是否未被销毁)以及以通用方式编组任何参数所需的开销。

信号就是一个公共访问函数,当对象的状态发生改变时,信号被某一个对象发射(emit),与其相关联的槽将被执行, 槽函数的执行就像一个正常的函数调用一样。

槽是普通的C++成员函数,可以被正常调用,只不过它可以与信号关联,当与其关联的信号被发射时,这个槽函数就会被调用。

信号与槽机制独立于任何GUI事件循环,只有当所有的槽正确返回以后,发射函数(emit)才返回。

信号与槽的关联

一个信号可以连接多个槽,多个信号也可以连接一个槽,一个信号可以和另外一个信号相连接(第一个信号发射,也会把第二个信号发射),可以简单看下:

signal001

绑定信号的槽的connect函数原型如下:

qobject.h
1
2
3
4
5
static QMetaObject::Connection connect(const QObject *sender, const char *signal,const QObject *receiver, const char *member, Qt::ConnectionType = Qt::AutoConnection);

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

inline QMetaObject::Connection connect(const QObject *sender, const char *signal,const char *member, Qt::ConnectionType type = Qt::AutoConnection) const;

函数共有五个参数,第一个是信号的发送者,第二个是信号,第三个是信号的接收者,第四个是响应的槽函数,第五个为信号与槽连接的方式, 信号与槽常用的写法:

//方式1
connect(m_serial, &QSerialPort::readyRead, this, &MainWindow::readData);
//方式2
connect(m_serial, SIGNAL(readyRead()), this, SLOT(readData()));

信号与槽的连接后,可以使用disconnect()函数可以取消信号与槽的连接,具体的函数查看下Qt帮助文档。

自定义信号

在例程中,定义了一个Student类:

lubancat_qt_tutorial_code/QtCore/student.h
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#ifndef STUDENT_H
#define STUDENT_H

#include <QObject>

class Student : public QObject
{
    Q_OBJECT

    //定义附加的类信息
    Q_CLASSINFO("author", "Embedfire")
    Q_CLASSINFO("url", "https://embedfire.com/")

    //定义属性
    Q_PROPERTY(int score READ getScore WRITE setScore NOTIFY scoreChanged)     //定义属性score
    Q_PROPERTY(QString name MEMBER m_name)      //定义属性name
    Q_PROPERTY(int age MEMBER m_age)        //定义属性age

public:
    explicit Student(QString name, QObject *parent);
    ~Student();                                //析构函数
    int     getScore();
    void    setScore(int value);
    void    subScore();
    void    addScore();

private:
    int  m_age=18;
    int  m_score=60;
    QString m_name;

signals:
    void    scoreChanged(int  value);        //自定义信号

};

#endif // STUDENT_H

添加自定义信号后,在setScore函数中设置发射信号:

lubancat_qt_tutorial_code/QtCore/student.cpp
1
2
3
4
5
6
7
8
void Student::setScore(int value)
{
    if (m_score != value)
    {
        m_score= value;
        emit scoreChanged(m_score);  //发射信号
    }
}

信号与槽的关联之后,发射信号的对象并不需要知道Qt是如何找到槽函数,这些复杂的底层操作都被Qt隐藏。

5.6. 测试例程

在PC ubuntu20.04上,打开Qt Creator点击编译运行例程,使用 Desktop Qt 5.15.2 GCC 64bit 编译套件,进行简单测试:

core01.png

运行后,然后测试:

core01.png

关闭窗口会输出:

Student对象被删除
Student对象被删除

在lubancat_rk板卡上运行,选择 LubanCat_RK 交叉编译套件。