元对象系统(Meta-Object System)

元对象系统 = 描述一个 QObject 对象自身信息,并提供信号槽、属性、运行时类型信息、动态调用等能力的机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person : public QObject
{
Q_OBJECT

public:
Person(QObject *parent = nullptr);

signals:
void ageChanged(int age);

public slots:
void setAge(int age);
};

必须继承自 QObject,并声明 Q_OBJECT 宏,QT 会给 Person 生成额外的元信息。
比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Person
│
├── 类名:"Person"
│
├── 父类:QObject
│
├── Signal
│ └── ageChanged(int)
│
├── Slot
│ └── setAge(int)
│
└── Property
└── ...

于是 Qt 可以实现很多普通 C++ 不直接提供的功能。
比如可以在运行时得到类名:

1
p->metaObject()->className();

还有信号槽机制,Signal / Slot:

1
2
connect(sender, &Sender::dataReady,
receiver, &Receiver::handleData);

信号/槽

定义信号和槽

  • 信号(Signal):在类中使用 signals: 标签声明信号函数。信号函数不需要实现,只需声明即可。
    1
    2
    signals:
    void buttonClicked();
  • 槽(Slot):使用 slots: 标签声明普通成员函数作为槽函数,也可以是静态函数、Lambda 表达式等。
    1
    2
    public slots:
    void handleButtonClick();

连接信号和槽

通过 QObject::connect() 函数绑定信号与槽。当信号被触发时,对应的槽函数就会自动执行。
信号和槽是多对多的关系。

1
2
3
4
5
6
7
QMetaObject::Connection connect(
const QObject *sender, // 信号发送者(不能为nullptr)
PointerToMemberFunction signal, // 发送者的信号(成员函数指针,如&A::signalFunc)
const QObject *receiver, // 槽函数的接收者(默认this,可为nullptr,若槽是全局函数)
PointerToMemberFunction method, // 接收者的槽函数(成员函数指针,如&B::slotFunc)
Qt::ConnectionType type = Qt::AutoConnection // 连接类型
);

发出信号

当某个事件发生时(如用户点击按钮、数据更新完成等),通过调用 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
2
3
4
5
6
7
8
9
10
声明 Q_OBJECT
↓
告诉 Qt:
“这个类需要元对象功能”
↓
moc 扫描
↓
生成 C++ 代码
↓
实现元对象系统

对象树

Qt 的对象树是一种基于父子关系的层级结构,用于管理所有继承自 QObject 的对象。当创建一个 QObject 并为其指定一个父对象时,该对象就会被自动添加到父对象的子对象列表中。

这种机制最核心的优势在于自动化内存管理,当父对象被销毁时,Qt 会自动遍历并销毁其所有的子对象。我们在开发中只需要手动管理顶层对象,拥有的所有子对象都会被清理。

举例

QString、QObject 是类型;Q_OBJECT、Q_PROPERTY、Q_INVOKABLE 是 Qt 提供的声明宏;READ、WRITE、NOTIFY 是属性声明里的标记。

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
// 播放器对象:提供音量属性和播放操作。
class Player : public QObject {
Q_OBJECT // 启用 Qt 元对象能力,例如属性、信号和方法查询。

// 向 Qt 暴露名为 volume 的属性,类型是 int。
Q_PROPERTY(int volume READ volume WRITE setVolume NOTIFY volumeChanged)

public:
// 读取音量。
int volume() const { return m_volume; }

// 修改音量;实际变化时通知界面。
void setVolume(int value) {
if (m_volume == value) return;
m_volume = value;
emit volumeChanged();
}

// 允许 QML 调用这个 C++ 方法。
Q_INVOKABLE void play() {
// 在这里调用实际播放逻辑。
}

signals:
// 声明音量变化信号,不需要自己编写函数体。
void volumeChanged();

private:
int m_volume = 50; // 保存实际音量值。
};

Q_PROPERTY:对外公布一个属性

1
2
// 属性名、读取方法、写入方法以及变化通知。
Q_PROPERTY(int volume READ volume WRITE setVolume NOTIFY volumeChanged)
部分 含义
int volume 属性类型是 int,名字是 volume
READ volume 读取属性时调用 volume()
WRITE setVolume 写入属性时调用 setVolume()
NOTIFY volumeChanged 属性变化通过这个信号通知

假设 player 已经暴露给 QML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Text {
// 建立绑定:音量变化时,文字自动重新计算。
text: "音量:" + player.volume
}

Button {
text: "播放"
// 调用 C++ 的 Q_INVOKABLE 方法。
onClicked: player.play()
}

Button {
text: "静音"
// 写属性,实际调用 C++ 的 setVolume(0)。
onClicked: player.volume = 0
}

更新链是:
设置音量 → 修改 m_volume → 发出 volumeChanged → QML 重新读取属性 → 文字更新。
如果只修改变量、不发通知,界面绑定通常不知道它变了。

Q_INVOKABLE:让方法能被 QML 调用
普通 public 方法可以被 C++ 调用,但不一定能被 QML 调用。加上 Q_INVOKABLE 后,该方法会进入元对象系统。

player 怎么暴露给 QML?

setInitialProperties() 注入方式,用有类型的 required property + 显式注入,更容易看清组件需要哪些对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建应用。
QGuiApplication app(argc, argv);

// 注册类型信息,供 QML 使用 Player 类型;实例由 C++ 创建。
qmlRegisterUncreatableType<Player>(
"Btv", 1, 0, "Player", "播放器由 C++ 创建");

// 先创建播放器,再创建引擎,确保播放器比 QML 对象更晚析构。
Player player;
QQmlApplicationEngine engine;

// 给根 QML 对象的 player 属性传入 C++ 对象指针。
engine.setInitialProperties({
{"player", QVariant::fromValue(&player)}
});

// 加载界面,创建根对象时应用上述初始属性。
engine.load(QUrl(QStringLiteral("qrc:/Main.qml")));

return app.exec();

对应的 Main.qml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import QtQuick
import QtQuick.Controls
import Btv 1.0

ApplicationWindow {
id: root
visible: true

// 声明必须由外部传入的播放器依赖。
required property Player player

Button {
// 读取同一个 C++ 对象的属性。
text: "播放,音量:" + root.player.volume

// 调用同一个 C++ 对象的方法。
onClicked: root.player.play()
}
}
步骤 解决的问题
qmlRegisterUncreatableType<Player>() 让 QML 认识 Player 类型,但不能自行创建它
setInitialProperties() 把 C++ 创建的具体对象传给根 QML 属性

QML 没有复制一个播放器,只是通过引用访问原来的 C++ 对象。player.volume 最终调用 C++ getter,player.play() 最终调用 C++ 方法。
传入根窗口的主要是 app 根业务对象,再由它提供子模块;原理相同。子组件也可以继续显式接收依赖:

1
2
3
4
// 父组件将已有对象传给子组件,不创建新播放器。
PlayerControls {
player: root.player
}