一、初识信号槽
所谓信号槽,实际就是观察者模式。当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal)。这种发出是没有目的的,类似广播。如果有对象对这个信号感兴趣,它就会使用连接(connect)函数,意思是,用自己的一个函数(成为槽(slot))来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自动被回调。这就类似观察者模式:当发生了感兴趣的事件,某一个操作就会被自动触发。
#include <QApplication>
#include <QPushButton>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QPushButton button("Quit");
QObject::connect(&button, &QPushButton::clicked, &QApplication::quit);
button.show();
return app.exec();
}
以上面这段代码为例体验信号槽的使用

点击Quit即可退出。按钮在 Qt 中被称为
QPushButton
。
下面了解一下
QObject::connect()
这个函数。
connect()
函数最常用的一般形式:
connect(sender, signal,
receiver, slot);
connect()
一般会使用前面四个参数,第一个是发出信号的对象,第二个是发送对象发出的信号,第三个是接收信号的对象,第四个是接收对象在接收到信号之后所需要调用的函数。也就是说,当 sender 发出了 signal 信号之后,会自动调用 receiver 的 slot 函数。
在 Qt 5 中,
QObject::connect()
有五个重载:
QMetaObject::Connection connect(const QObject *, const char *,
const QObject *, const char *,
Qt::ConnectionType);
QMetaObject::Connection connect(const QObject *, const QMetaMethod &,
const QObject *, const QMetaMethod &,
Qt::ConnectionType);
QMetaObject::Connection connect(const QObject *, const char *,
const char *,
Qt::ConnectionType) const;
QMetaObject::Connection connect(const QObject *, PointerToMemberFunction,
const QObject *, PointerToMemberFunction,
Qt::ConnectionType)
QMetaObject::Connection connect(const QObject *, PointerToMemberFunction,
Functor);
第一个,sender 类型是
const QObject *
,signal 的类型是
const char *
,receiver 类型是
const QObject *
,slot 类型是
const char *
。这个函数将 signal 和 slot 作为字符串处理。第二个,sender 和 receiver 同样是
const QObject *
,但是 signal 和 slot 都是
const QMetaMethod &
。我们可以将每个函数看做是
QMetaMethod
的子类。因此,这种写法可以使用
QMetaMethod
进行类型比对。第三个,sender 同样是
const QObject *
,signal 和 slot 同样是
const char *
,但是却缺少了 receiver。这个函数其实是将 this 指针作为 receiver。第四个,sender 和 receiver 也都存在,都是
const QObject *
,但是 signal 和 slot 类型则是
PointerToMemberFunction
。看这个名字就应该知道,这是指向成员函数的指针。第五个,前面两个参数没有什么不同,最后一个参数是
Functor
类型。这个类型可以接受 static 函数、全局函数以及 Lambda 表达式。
由此我们可以看出,
connect()
函数,sender 和 receiver 没有什么区别,都是
QObject
指针;主要是 signal 和 slot 形式的区别。具体到我们的示例,我们的
connect()
函数显然是使用的第五个重载,最后一个参数是
QApplication
的 static 函数
quit()
。也就是说,当我们的 button 发出了
clicked()
信号时,会调用
QApplication
的
quit()
函数,使程序退出。
信号槽要求信号和槽的参数类型一致。如果不一致,允许的情况是,槽函数的参数可以比信号的少,即便如此,槽函数存在的那些参数的顺序也必须和信号的前面几个一致起来。
二、自定义信号槽
Qt 的信号槽机制并不仅仅是使用系统提供的那部分,还会允许我们自己设计自己的信号和槽。
下面是通过信号槽实现观察者模式的一个例子:
经典的观察者模式在讲解举例的时候通常会举报纸和订阅者的例子。有一个报纸类
Newspaper
,有一个订阅者类
Subscriber
。
Subscriber
可以订阅
Newspaper
。这样,当
Newspaper
有了新的内容的时候,
Subscriber
可以立即得到通知。在这个例子中,观察者是
Subscriber
,被观察者是
Newspaper
。在经典的实现代码中,观察者会将自身注册到被观察者的一个容器中(比如
subscriber.registerTo(newspaper)
)。被观察者发生了任何变化的时候,会主动遍历这个容器,依次通知各个观察者(
newspaper.notifyAllSubscribers()
)。
#include <QObject>
// newspaper.h
class Newspaper : public QObject
{
Q_OBJECT
public:
Newspaper(const QString & name) :
m_name(name)
{
}
void send()
{
emit newPaper(m_name);
}
signals:
void newPaper(const QString &name);
private:
QString m_name;
};
// reader.h
#include <QObject>
#include <QDebug>
class Reader : public QObject
{
Q_OBJECT
public:
Reader() {}
void receiveNewspaper(const QString & name)
{
qDebug() << "Receives Newspaper: " << name;
}
};
// main.cpp
#include <QCoreApplication>
#include "newspaper.h"
#include "reader.h"
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
Newspaper newspaper("Newspaper A");
Reader reader;
QObject::connect(&newspaper, &Newspaper::newPaper,
&reader, &Reader::receiveNewspaper);
newspaper.send();
return app.exec();
}
首先看
Newspaper
这个类。这个类继承了
QObject
类。只有继承了
QObject
类的类,才具有信号槽的能力。所以,为了使用信号槽,必须继承
QObject
。凡是
QObject
类(不管是直接子类还是间接子类),都应该在第一行代码写上
Q_OBJECT
。不管是不是使用信号槽,都应该添加这个宏。注意,这个宏将由 moc(我们会在后面章节中介绍 moc。这里你可以将其理解为一种预处理器,是比 C++ 预处理器更早执行的预处理器。) 做特殊处理,不仅仅是宏展开这么简单。moc 会读取标记了 Q_OBJECT 的头文件,生成以 moc_ 为前缀的文件,比如 newspaper.h 将生成 moc_newspaper.cpp。你可以到构建目录查看这个文件,看看到底增加了什么内容。注意,由于 moc 只处理头文件中的标记了
Q_OBJECT
的类声明,不会处理 cpp 文件中的类似声明。因此,如果我们的
Newspaper
和
Reader
类位于 main.cpp 中,是无法得到 moc 的处理的。解决方法是,我们手动调用 moc 工具处理 main.cpp,并且将 main.cpp 中的
#include "newspaper.h"
改为
#include "moc_newspaper.h"
就可以了。不过,这是相当繁琐的步骤,为了避免这样修改,我们还是将其放在头文件中。
Newspaper
类的 public 和 private 代码块都比较简单,只不过它新加了一个 signals。signals 块所列出的,就是该类的信号。信号就是一个个的函数名,返回值是 void(因为无法获得信号的返回值,所以也就无需返回任何值),参数是该类需要让外界知道的数据。
Reader
类更简单。因为这个类需要接受信号,所以我们将其继承了
QObject
,并且添加了
Q_OBJECT
宏。后面则是默认构造函数和一个普通的成员函数。Qt 5 中,任何成员函数、static 函数、全局函数和 Lambda 表达式都可以作为槽函数。与信号函数不同,槽函数必须自己完成实现代码。槽函数就是普通的成员函数,因此作为成员函数,也会受到 public、private 等访问控制符的影响。(我们没有说信号也会受此影响,事实上,如果信号是 private 的,这个信号就不能在类的外面连接,也就没有任何意义。)
main()
函数中,我们首先创建了
Newspaper
和
Reader
两个对象,然后使用
QObject::connect()
函数。这个函数我们上一节已经详细介绍过,这里应该能够看出这个连接的含义。然后我们调用
Newspaper
的
send()
函数。这个函数只有一个语句:发出信号。由于我们的连接,当这个信号发出时,自动调用 reader 的槽函数,打印出语句。
下面总结一下自定义信号槽需要注意的事项:
- 发送者和接收者都需要是
的子类(当然,槽函数是全局函数、Lambda 表达式等无需接收者的时候除外);QObject
- 使用 signals 标记信号函数,信号是一个函数声明,返回 void,不需要实现函数代码;
- 槽函数是普通的成员函数,作为成员函数,会受到 public、private、protected 的影响;
- 使用 emit 在恰当的位置发送信号;
- 使用
函数连接信号和槽。QObject::connect()