适合人群: 已掌握 QML 基础,想理解 Qt 对象系统通信原理的开发者
前言
按钮点击触发动作、输入框内容变化更新界面、后台数据完成加载通知 UI 刷新------这些"某件事发生时,另一件事跟着响应"的场景,在 Qt 中统一由信号与槽机制处理。
信号与槽不只是 QML 的概念,它是整个 Qt 框架的核心通信机制,C++ 和 QML 都建立在它之上。理解它,才能真正读懂 Qt 的运作方式。
一、为什么需要信号与槽?
传统的 UI 编程用回调函数(callback)处理事件:
scss
// 传统回调方式(伪代码)
button->setOnClickCallback([](void* data) {
doSomething(data);
});
回调函数的问题:
- 发送方必须知道接收方的具体函数指针
- 类型不安全,容易传错参数
- 一对多通知非常繁琐
- 对象销毁后回调仍可能被调用,导致崩溃
Qt 的信号与槽解决了这些问题:
kotlin
// Qt 信号槽方式
connect(button, &QPushButton::clicked, this, &MyClass::onButtonClicked);
- 发送方只需发出信号,不关心谁在监听
- 编译时类型检查,参数类型不匹配直接报错
- 一个信号可以连接多个槽
- 对象销毁时连接自动断开,不会产生悬空指针
二、Qt 对象系统:MOC 的作用
信号与槽是 C++ 语言本身不支持的特性,Qt 通过 元对象编译器(MOC,Meta-Object Compiler) 来实现它。
2.1 MOC 的工作流程
markdown
你写的 .h 文件(含 Q_OBJECT 宏)
↓ MOC 处理
moc_xxx.cpp(自动生成的元对象代码)
↓ 普通 C++ 编译器
最终可执行文件
MOC 扫描带有 Q_OBJECT 宏的类,自动生成支持信号槽、属性系统、运行时类型信息所需的代码。
2.2 Q_OBJECT 宏
每个需要使用信号槽的 C++ 类都必须:
- 继承自
QObject(直接或间接) - 在类声明的第一行加上
Q_OBJECT宏
arduino
#include <QObject>
class Counter : public QObject
{
Q_OBJECT // 必须放在 private 区域第一行
public:
explicit Counter(QObject *parent = nullptr);
int value() const { return m_value; }
public slots:
void setValue(int value);
void increment() { setValue(m_value + 1); }
signals:
void valueChanged(int newValue); // 只声明,不实现
private:
int m_value = 0;
};
signals 块中只写声明,不需要实现------MOC 自动生成信号的发射代码。
三、C++ 中的信号与槽
3.1 定义信号和槽
arduino
// counter.h
#pragma once
#include <QObject>
class Counter : public QObject
{
Q_OBJECT
public:
explicit Counter(QObject *parent = nullptr);
int value() const { return m_value; }
public slots:
// 槽函数:普通成员函数,加上 slots 关键字
void setValue(int value) {
if (value == m_value) return; // 防止循环触发
m_value = value;
emit valueChanged(m_value); // 发射信号
}
signals:
// 信号:只声明,参数就是传递的数据
void valueChanged(int newValue);
private:
int m_value = 0;
};
3.2 连接信号与槽
QObject::connect() 是建立连接的核心函数:
arduino
// main.cpp
#include "counter.h"
#include <QDebug>
int main()
{
Counter a, b;
// 函数指针语法(Qt 5+ 推荐,编译时类型检查)
QObject::connect(&a, &Counter::valueChanged,
&b, &Counter::setValue);
// 当 a 的值改变时,b 自动同步
a.setValue(10);
qDebug() << b.value(); // 输出:10
return 0;
}
3.3 连接到 Lambda 函数
槽不一定是成员函数,可以直接连接到 Lambda:
ini
Counter counter;
QObject::connect(&counter, &Counter::valueChanged,
[](int value) {
qDebug() << "值变为:" << value;
});
counter.setValue(42); // 输出:值变为:42
3.4 一信号多槽
一个信号可以连接到多个槽,发射时所有槽按连接顺序依次调用:
php
Counter counter;
QLabel *label1 = new QLabel;
QLabel *label2 = new QLabel;
// 同一信号连接两个槽
QObject::connect(&counter, &Counter::valueChanged,
label1, [label1](int v) { label1->setText(QString::number(v)); });
QObject::connect(&counter, &Counter::valueChanged,
label2, [label2](int v) { label2->setText("值:" + QString::number(v)); });
counter.setValue(99); // label1 和 label2 都会更新
3.5 断开连接
arduino
// 断开特定连接
QObject::disconnect(&a, &Counter::valueChanged,
&b, &Counter::setValue);
// 断开某对象的所有连接
QObject::disconnect(&a, nullptr, nullptr, nullptr);
四、Qt 内存管理:对象树
Qt 通过父子对象树管理内存,这与信号槽系统密切相关。
4.1 父子关系
ini
QWidget *window = new QWidget(); // 根对象,没有父级
QPushButton *btn = new QPushButton(window); // 父级是 window
QLabel *label = new QLabel(window); // 父级是 window
规则:父对象销毁时,所有子对象自动销毁。
javascript
{
QWidget *window = new QWidget();
QPushButton *btn = new QPushButton(window); // btn 的父是 window
// ...
delete window; // btn 也被自动删除,不会内存泄漏
}
4.2 对象树与信号槽的协作
当一个 QObject 被销毁时,Qt 自动:
- 发射
destroyed()信号 - 断开所有与该对象相关的信号槽连接
这保证了不会出现"槽函数引用了已销毁对象"的崩溃问题:
ini
Counter *counter = new Counter();
QLabel *label = new QLabel();
QObject::connect(counter, &Counter::valueChanged,
label, [label](int v) {
label->setText(QString::number(v));
});
delete label; // label 销毁,连接自动断开
counter->setValue(5); // 安全,不会崩溃
4.3 在 QML 中的对象生命周期
QML 对象的父子关系由可视层级决定:
arduino
Rectangle { // 父对象
id: container
Rectangle { // 子对象,container 销毁时一并销毁
id: child
}
Component.onCompleted: {
console.log("container 加载完成")
}
Component.onDestruction: {
console.log("container 即将销毁")
}
}
五、QML 中的信号与槽
5.1 QML 内置信号
Qt Quick 的每个属性变化都自动生成对应的信号,命名规则是 属性名 + Changed:
less
Rectangle {
id: box
width: 200
// 监听 width 变化
onWidthChanged: console.log("宽度变为:" + width)
// 监听 visible 变化
onVisibleChanged: console.log("可见性:" + visible)
}
5.2 自定义信号
less
Rectangle {
id: card
width: 200; height: 100
// 声明自定义信号(可以带参数)
signal clicked()
signal dataChanged(string key, var value)
MouseArea {
anchors.fill: parent
onClicked: {
card.clicked() // 发射无参信号
card.dataChanged("status", "active") // 发射带参信号
}
}
}
在父对象中响应:
javascript
Card {
onClicked: console.log("卡片被点击")
onDataChanged: function(key, value) {
console.log(key + " = " + value)
}
}
5.3 Connections 元素
当需要在对象外部监听信号,或需要动态管理连接时,使用 Connections:
arduino
import QtQuick
Rectangle {
id: sender
signal messageSent(string text)
}
// 在另一个地方监听 sender 的信号
Connections {
target: sender // 监听的目标对象
function onMessageSent(text) {
console.log("收到消息:" + text)
}
}
Connections 的实际应用------监听全局单例的信号:
scss
// AppState.qml(单例)
pragma Singleton
import QtQuick
QtObject {
signal userLoggedIn(string userName)
signal userLoggedOut()
}
javascript
// 在任意组件中监听
Connections {
target: AppState
function onUserLoggedIn(userName) {
welcomeText.text = "欢迎," + userName
}
function onUserLoggedOut() {
welcomeText.text = "请登录"
}
}
5.4 connect() 方法
QML 中也可以用 JavaScript 风格的 connect() 动态建立连接:
csharp
Rectangle {
id: buttonA
signal tapped()
}
Rectangle {
id: buttonB
signal tapped()
function onAnyTapped() {
console.log("有按钮被点击了")
}
Component.onCompleted: {
// 动态连接两个信号到同一个函数
buttonA.tapped.connect(onAnyTapped)
buttonB.tapped.connect(onAnyTapped)
}
}
断开连接:
scss
buttonA.tapped.disconnect(onAnyTapped)
六、C++ 信号与 QML 槽的跨语言连接
Qt 最强大的能力之一是 C++ 后端与 QML 前端之间的信号槽连接。
6.1 C++ 信号 → QML 响应
定义 C++ 类(后端):
arduino
// backend.h
#pragma once
#include <QObject>
#include <QString>
class Backend : public QObject
{
Q_OBJECT
public:
explicit Backend(QObject *parent = nullptr);
public slots:
void fetchData(); // QML 可以调用这个函数
signals:
void dataReady(const QString &data); // 数据准备好时发射
void errorOccurred(const QString &msg); // 出错时发射
};
arduino
// backend.cpp
#include "backend.h"
#include <QTimer>
void Backend::fetchData()
{
// 模拟异步操作:500ms 后返回数据
QTimer::singleShot(500, this, [this]() {
emit dataReady("从服务器获取的数据内容");
});
}
在 main.cpp 中暴露给 QML:
arduino
#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QQmlContext>
#include "backend.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
Backend backend;
// 将 C++ 对象注册为 QML 上下文属性
engine.rootContext()->setContextProperty("backend", &backend);
engine.load(QUrl("qrc:/Main.qml"));
return app.exec();
}
在 QML 中响应 C++ 信号:
arduino
import QtQuick
import QtQuick.Controls
ApplicationWindow {
width: 360; height: 300
visible: true
// 监听 C++ backend 的信号
Connections {
target: backend
function onDataReady(data) {
resultText.text = data
loadingIndicator.visible = false
}
function onErrorOccurred(msg) {
resultText.text = "错误:" + msg
resultText.color = "red"
}
}
Column {
anchors.centerIn: parent
spacing: 16
width: 280
BusyIndicator {
id: loadingIndicator
anchors.horizontalCenter: parent.horizontalCenter
visible: false
}
Text {
id: resultText
width: parent.width
text: "点击按钮获取数据"
wrapMode: Text.Wrap
horizontalAlignment: Text.AlignHCenter
font.pixelSize: 15
}
Button {
anchors.horizontalCenter: parent.horizontalCenter
text: "获取数据"
onClicked: {
loadingIndicator.visible = true
resultText.text = "加载中..."
backend.fetchData() // 调用 C++ 函数
}
}
}
}
七、综合示例:计时器应用
用信号槽实现一个完整的秒表,涵盖 QML 自定义信号、Connections、状态管理:
arduino
import QtQuick
import QtQuick.Controls
ApplicationWindow {
id: window
width: 360; height: 500
visible: true
title: "秒表"
// 自定义计时器组件(内联)
component Stopwatch: QtObject {
id: sw
property int elapsed: 0 // 已过毫秒数
property bool running: false
signal started()
signal stopped(int totalMs)
signal reset()
function start() {
running = true
timer.start()
started()
}
function stop() {
running = false
timer.stop()
stopped(elapsed)
}
function doReset() {
running = false
timer.stop()
elapsed = 0
reset()
}
Timer {
id: timer
interval: 10 // 每 10ms 触发一次
repeat: true
onTriggered: sw.elapsed += 10
}
}
Stopwatch {
id: stopwatch
onStarted: statusText.text = "计时中..."
onStopped: function(totalMs) {
statusText.text = "已停止,共 " + (totalMs / 1000).toFixed(2) + " 秒"
}
onReset: statusText.text = "已重置"
}
// 格式化显示
function formatTime(ms) {
var minutes = Math.floor(ms / 60000)
var seconds = Math.floor((ms % 60000) / 1000)
var millis = Math.floor((ms % 1000) / 10)
return pad(minutes) + ":" + pad(seconds) + "." + pad(millis)
}
function pad(n) {
return n < 10 ? "0" + n : "" + n
}
Column {
anchors.centerIn: parent
spacing: 24
width: 280
// 时间显示
Rectangle {
width: parent.width
height: 120
radius: 16
color: stopwatch.running ? "#1A2332" : "#f5f5f5"
Behavior on color {
ColorAnimation { duration: 300 }
}
Text {
anchors.centerIn: parent
// 绑定到 elapsed,自动实时更新
text: formatTime(stopwatch.elapsed)
font.pixelSize: 42
font.family: "monospace"
color: stopwatch.running ? "white" : "#333"
font.bold: true
Behavior on color {
ColorAnimation { duration: 300 }
}
}
}
// 状态文字
Text {
id: statusText
anchors.horizontalCenter: parent.horizontalCenter
text: "准备就绪"
font.pixelSize: 14
color: "#888"
}
// 控制按钮
RowLayout {
width: parent.width
spacing: 12
Button {
Layout.fillWidth: true
text: stopwatch.running ? "暂停" : "开始"
highlighted: !stopwatch.running
onClicked: stopwatch.running ? stopwatch.stop() : stopwatch.start()
}
Button {
Layout.fillWidth: true
text: "重置"
enabled: stopwatch.elapsed > 0
onClicked: stopwatch.doReset()
}
}
}
}
八、常见问题
Q:信号与 JavaScript 函数调用有什么区别?
直接调用函数是同步的、紧耦合的 ------调用方必须知道被调用方的存在。信号是松耦合的------发射方不知道也不关心谁在监听,可以没有监听者,也可以有多个监听者。
Q:emit 关键字是必须的吗?
在 C++ 中,emit 只是一个空宏(展开为空),语义上是可选的,直接调用信号函数也能发射。但强烈建议保留 emit,它让代码读者一眼看出这里在发射信号,而不是调用普通函数。
Q:槽函数可以有返回值吗?
槽函数本身可以有返回值,但通过 connect() 触发的槽,返回值会被忽略。如果需要获取返回值,应该直接调用函数而不是通过信号槽。
Q:信号可以连接到另一个信号吗?
可以,信号可以直接连接到另一个信号,形成信号链:
less
connect(sender, &Sender::signal1, receiver, &Receiver::signal2);
// sender 发射 signal1 时,receiver 自动发射 signal2
总结
| 概念 | 要点 |
|---|---|
Q_OBJECT |
启用元对象系统,必须继承 QObject 并添加此宏 |
signals |
声明信号,只写声明不写实现,MOC 自动生成 |
slots |
声明槽函数,本质是普通成员函数 |
emit |
发射信号,触发所有连接的槽 |
connect() |
建立信号槽连接,支持函数指针和 Lambda |
disconnect() |
断开连接 |
| 对象树 | 父对象销毁时子对象自动销毁,连接自动断开 |
QML signal |
声明自定义信号,on + 信号名 处理 |
Connections |
在对象外部监听信号,支持动态目标 |
| 跨语言连接 | C++ 信号可在 QML 中用 Connections 响应 |