信号与槽 signals and slots
所谓信号槽,实际就是观察者模式。当某个事件发生之后,比如,按钮检测到自己被点击了一下,它就会发出一个信号(signal)。这种发出是没有目的的,类似广播。如果有对象对这个信号感兴趣,它就会使用连接(connect)函数,意思是,用自己的一个函数(成为槽(slot))来处理这个信号。也就是说,当信号发出时,被连接的槽函数会自动被回调。这就类似观察者模式:当发生了感兴趣的事件,某一个操作就会被自动触发。(这里提一句,Qt
的信号槽使用了额外的处理来实现,并不是 GoF 经典的观察者模式的实现方式。)
实践
int main(int argc, char *argv[]) { QApplication app(argc, argv); // MainWindow w; // w.setWindowTitle("signal_and_slot"); QPushButton button("Quit"); QObject::connect(&button, &QPushButton::clicked, &app, &QApplication::quit); /*qt5使用lambda表达式写法*/ QObject::connect(&button, &QPushButton::clicked, [](bool) { qDebug() << "You clicked me!"; }); button.show(); // w.show(); return app.exec(); }
解释
将按钮button的QPushButton::clicked 事件(信号)跟应用app对象的QApplication::quit事件(槽)绑定在一块,按钮的点击->应用退出
形式
以qt5中 QObject::connect()为例子
// !!! Qt 5
connect(sender, signal,
receiver, slot);
这是我们最常用的形式。connect()一般会使用前面四个参数,第一个是发出信号的对象,第二个是发送对象发出的信号,第三个是接收信号的对象,第四个是接收对象在接收到信号之后所需要调用的函数。也就是说,当
sender 发出了 signal 信号之后,会自动调用 receiver 的 slot 函数。
qt5
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);
第一个,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 表达式。
Qt 4 的connect()函数与 Qt 5 最大的区别在于,Qt 4 的 signal 和 slot 只有const char *这么一种形式。
bool connect(const QObject *, const char *, const QObject *, const char *, Qt::ConnectionType); bool connect(const QObject *, const QMetaMethod &, const QObject *, const QMetaMethod &, Qt::ConnectionType); bool connect(const QObject *, const char *, const char *, Qt::ConnectionType) const
如果我们将上面的代码修改为 Qt 4 的,则应该是这样的:
QObject::connect(&button, SIGNAL(clicked()), &app, SLOT(quit()));
我们使用了SIGNAL和SLOT这两个宏,将两个函数名转换成了字符串。注意,即使quit()是QApplication的 static
函数,也必须传入一个对象指针。这也是 Qt 4 的信号槽语法的局限之处。另外,注意到connect()函数的 signal 和 slot
都是接受字符串,因此,不能将全局函数或者 Lambda 表达式传入connect()。一旦出现连接不成功的情况,Qt 4
是没有编译错误的(因为一切都是字符串,编译期是不检查字符串是否匹配),而是在运行时给出错误。这无疑会增加程序的不稳定性。
自定义信号槽
信号槽不是 GUI 模块提供的,而是 Qt 核心特性之一。因此,我们可以在普通的控制台程序使用信号槽。
经典的观察者模式在讲解举例的时候通常会举报纸和订阅者的例子。有一个报纸类Newspaper,有一个订阅者类Subscriber。Subscriber可以订阅Newspaper。这样,当Newspaper有了新的内容的时候,Subscriber可以立即得到通知。在这个例子中,观察者是Subscriber,被观察者是Newspaper。在经典的实现代码中,观察者会将自身注册到被观察者的一个容器中(比如subscriber.registerTo(newspaper))。被观察者发生了任何变化的时候,会主动遍历这个容器,依次通知各个观察者(newspaper.notifyAllSubscribers())。
实践
//!!! Qt5 #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,Reader类 都继承QObject 才有信号槽能力 且 添加宏Q_OBJECT ,任何QObject都需要Q_OBJECT这个宏
发射方Newspaper 定义Signal 表示信号函数 信号函数newPaper无需 实现 emit 关键字表示发射 当send时候发射newPaper信号 受访问控制影响 信号函数不能定义为private,protect
接收方Reader 定义接受函数 receiveNewspaper
main()函数中,我们首先创建了Newspaper和Reader两个对象,然后使用QObject::connect()函数。这个函数我们上一节已经详细介绍过,这里应该能够看出这个连接的含义。然后我们调用Newspaper的send()函数。这个函数只有一个语句:发出信号。由于我们的连接,当这个信号发出时,自动调用 reader 的槽函数,打印出语句。
自定义信号槽 条件
- 发送者和接收者都需要是QObject的子类(当然,槽函数是全局函数、Lambda 表达式等无需接收者的时候除外);
- 使用 signals 标记信号函数,信号是一个函数声明,返回 void,不需要实现函数代码;
- 槽函数是普通的成员函数,作为成员函数,会受到public、private、protected 的影响;
- 使用 emit 在恰当的位置发送信号;
- 使用QObject::connect()函数连接信号和槽。
信号槽qt4和qt5的区别
有重载的信号
如果信号有重载,比如我们向Newspaper类增加一个新的信号:
void newPaper(const QString &name, const QDate &date);
此时如果还是按照前面的写法,编译器会报出一个错误:由于这个函数(注意,信号实际也是一个普通的函数)有重载,因此不能用一个取址操作符获取其地址。
qt4 我们使用SIGNAL和SLOT两个宏来连接信号槽。
QObject::connect(&newspaper, SIGNAL(newPaper(QString, QDate)), &reader, SLOT(receiveNewspaper(QString, QDate)));
在 Qt 4 中不存在我们所说的错误,因为 Qt 4 的信号槽连接是带有参数的。
归根结底,这个错误是因为函数重载,编译器不知道要取哪一个函数的地址,而我们显式指明一个函数就可以了。
Qt5 解决方案
显式指明调用的函数
void (Newspaper:: *newPaperNameDate)(const QString &, const QDate &) = &Newspaper::newPaper; QObject::connect(&newspaper, newPaperNameDate, &reader, &Reader::receiveNewspaper);
或者好看点写法 ,但是如果你改变了信号的类型,那么你就会有一个潜在的运行时错误。
QObject::connect(&newspaper, (void (Newspaper:: *)(const QString &, const QDate &))&Newspaper::newPaper, &reader, &Reader::receiveNewspaper);
最正规的写法 静态转换 如果参数改变 会提示编译错误
QObject::connect(&newspaper, static_cast<void (Newspaper:: *)(const QString &, const QDate &)>(&Newspaper::newPaper), &reader, &Reader::receiveNewspaper);
带有默认参数的槽函数
Qt
允许信号和槽的参数数目不一致:槽函数的参数数目可以比信号的参数少。这是因为,我们信号的参数实际是作为一种返回值。正如普通的函数调用一样,我们可以选择忽略函数返回值,但是不能使用一个并不存在的返回值。如果槽函数的参数数目比信号的多,在槽函数中就使用到这些参数的时候,实际这些参数并不存在(因为信号的参数比槽的少,因此并没有传过来),函数就会报错。这种情况往往有两个原因:一是槽的参数就是比信号的少,此时我们可以像前面那种写法直接连接。另外一个原因是,信号的参数带有默认值。比如
void QPushButton::clicked(bool checked = false)
槽函数的参数可以比信号的多
// Newspaper signals: void newPaper(const QString &name); // Reader void receiveNewspaper(const QString &name, const QDate &date = QDate::currentDate());
虽然Reader::receiveNewspaper()的参数数目比Newspaper::newPaper()多,但是由于Reader::receiveNewspaper()后面一个参数带有默认值,所以该参数不是必须提供的。但是,如果你按照前面的写法,比如如下的代码:
QObject::connect(&newspaper, static_cast<void (Newspaper:: *)(const QString &)>(&Newspaper::newPaper), &reader, static_cast<void (Reader:: *)(const QString &, const QDate & =QDate::currentDate())>(&Reader::receiveNewspaper));
你会得到一个断言错误:
The slot requires more arguments than the signal provides.
我们不能在函数指针中使用函数参数的默认值。这是 C++ 语言的限制:参数默认值只能使用在直接地函数调用中。当使用函数指针取其地址的时候,默认参数是不可见的!
当然,此时你可以选择 Qt 4 的连接语法。如果你还是想使用 Qt 5 的新语法,目前的办法只有一个:Lambda 表达式。不要担心你的编译器不支持 Lambda 表达式,因为在你使用 Qt 5 的时候,能够支持 Qt 5 的编译器都是支持 Lambda 表达式的。于是,我们的代码就变成了:
QObject::connect(&newspaper, static_cast<void (Newspaper:: *)(const QString &)>(&Newspaper::newPaper), [=](const QString &name) { /* Your code here. */ });