Qt 机制
元对象系统(Meta-Object System)
元对象系统 = 描述一个 QObject 对象自身信息,并提供信号槽、属性、运行时类型信息、动态调用等能力的机制。
1 | class Person : public QObject |
必须继承自 QObject,并声明 Q_OBJECT 宏,QT 会给 Person 生成额外的元信息。
比如:
1 | Person |
于是 Qt 可以实现很多普通 C++ 不直接提供的功能。
比如可以在运行时得到类名:
1 | p->metaObject()->className(); |
还有信号槽机制,Signal / Slot:
1 | connect(sender, &Sender::dataReady, |
信号/槽
定义信号和槽
- 信号(Signal):在类中使用
signals: 标签声明信号函数。信号函数不需要实现,只需声明即可。1
2signals:
void buttonClicked(); - 槽(Slot):使用
slots: 标签声明普通成员函数作为槽函数,也可以是静态函数、Lambda 表达式等。1
2public slots:
void handleButtonClick();
连接信号和槽
通过 QObject::connect() 函数绑定信号与槽。当信号被触发时,对应的槽函数就会自动执行。
信号和槽是多对多的关系。
1 | QMetaObject::Connection connect( |
发出信号
当某个事件发生时(如用户点击按钮、数据更新完成等),通过调用 emit 关键字来发射信号。
1 | emit buttonClicked(); |
一旦信号被发出,所有与其连接的槽函数都会按照连接顺序依次执行。
事件队列
.exec() 启动了 QT 主线程的事件循环,事件加入 Event Queue(事件队列)逐个处理。
跨线程的 queued signal/slot 调用,需要依靠接收方线程的事件循环来完成调度。
connect() 函数的第五个参数(连接类型):
| 连接类型 | 行为说明 | 适用场景 |
|---|---|---|
Qt::AutoConnection |
自动选择:若发送者和接收者在同一线程,等价于 DirectConnection;否则等价于 QueuedConnection(默认值) |
默认值 |
Qt::DirectConnection |
信号发射时立即同步调用槽函数(在发送者线程执行),类似直接调用槽函数 | 同线程内高效通信,无延迟 |
Qt::QueuedConnection |
信号被封装为事件放入接收者线程的事件循环,异步调用槽函数(在接收者线程执行)。 | 跨线程通信(如子线程通知主线程更新 UI) |
Qt::BlockingQueuedConnection |
类似 QueuedConnection,但接收者线程会阻塞,直到槽函数执行完成(必须确保发送者和接收者不在同一线程,否则死锁) |
子线程需等待主线程处理结果后再继续 |
Qt::UniqueConnection |
确保连接唯一(若已存在相同连接,则不重复创建),需与其他类型组合(如 AutoConnection) |
防止重复连接调用多次槽函数 |
即使没有跨线程,QueuedConnection 和 DirectConnection 的执行时机仍然不同,前者需要入队等待,后者立即调用。
MOC (Meta-Object Compiler)
元对象编译器,实际是一个代码生成工具。
因为 Qt 的元对象系统需要一些额外信息,而标准 C++ 编译器本身不会因为写了 Q_OBJECT 就自动生成这些东西。
在编译前,moc 会解析这些继承自 QObject,并包含 Q_OBJECT 宏类的头文件,为带有 signals 和 slots 关键字的函数以及其他元对象特性生成一个额外的 C++ 源文件。这个源文件包含了每个信号的实现体和一个静态的 QMetaObject 实例,该实例存储了类的元信息,比如信号和槽的字符串表。当调用 connect 函数将一个信号连接到一个槽时,实际上是在一个全局的哈希表中注册了这个连接,表中记录了发送者对象、信号签名、接收者对象和槽签名。最后,当你使用 emit 关键字发出一个信号时,QObject 的 qt_metacall 或相关函数会被调用,它会根据传入的信号索引在元对象中查找所有连接到该信号的槽,并依次调用它们。
1 | 声明 Q_OBJECT |
对象树
Qt 的对象树是一种基于父子关系的层级结构,用于管理所有继承自 QObject 的对象。当创建一个 QObject 并为其指定一个父对象时,该对象就会被自动添加到父对象的子对象列表中。
这种机制最核心的优势在于自动化内存管理,当父对象被销毁时,Qt 会自动遍历并销毁其所有的子对象。我们在开发中只需要手动管理顶层对象,拥有的所有子对象都会被清理。
举例
QString、QObject 是类型;Q_OBJECT、Q_PROPERTY、Q_INVOKABLE 是 Qt 提供的声明宏;READ、WRITE、NOTIFY 是属性声明里的标记。
1 | // 播放器对象:提供音量属性和播放操作。 |
Q_PROPERTY:对外公布一个属性
1 | // 属性名、读取方法、写入方法以及变化通知。 |
| 部分 | 含义 |
|---|---|
int volume |
属性类型是 int,名字是 volume |
READ volume |
读取属性时调用 volume() |
WRITE setVolume |
写入属性时调用 setVolume() |
NOTIFY volumeChanged |
属性变化通过这个信号通知 |
假设 player 已经暴露给 QML:
1 | Text { |
更新链是:
设置音量 → 修改 m_volume → 发出 volumeChanged → QML 重新读取属性 → 文字更新。
如果只修改变量、不发通知,界面绑定通常不知道它变了。
Q_INVOKABLE:让方法能被 QML 调用
普通 public 方法可以被 C++ 调用,但不一定能被 QML 调用。加上 Q_INVOKABLE 后,该方法会进入元对象系统。
player 怎么暴露给 QML?
setInitialProperties() 注入方式,用有类型的 required property + 显式注入,更容易看清组件需要哪些对象。
1 | // 创建应用。 |
对应的 Main.qml:
1 | import QtQuick |
| 步骤 | 解决的问题 |
|---|---|
qmlRegisterUncreatableType<Player>() |
让 QML 认识 Player 类型,但不能自行创建它 |
setInitialProperties() |
把 C++ 创建的具体对象传给根 QML 属性 |
QML 没有复制一个播放器,只是通过引用访问原来的 C++ 对象。player.volume 最终调用 C++ getter,player.play() 最终调用 C++ 方法。
传入根窗口的主要是 app 根业务对象,再由它提供子模块;原理相同。子组件也可以继续显式接收依赖:
1 | // 父组件将已有对象传给子组件,不创建新播放器。 |

