QT入门学习之信号与槽(补充)

由上一节知使用connect()可以让我们连接系统提供的信号和槽。Qt 的信号槽机制并不仅仅是使用系统提供的那部分,还会允许我们自己设计自己的信号和槽。信号槽不是 GUI 模块提供的,而是 Qt 核心特性之一。因此,我们可以在普通的控制台程序使用信号槽。

我们以经典的观察者模式的报纸和阅读者实验来学习自定义信号与槽
注:该部分内容原内容出自《Qt学习之路2》这本经典书籍的第5节、第16节

1. 实验理论原理

有一个报纸类Newspaper,有一个订阅者类Subscriber。Subscriber可以订阅Newspaper。这样,当Newspaper有了新的内容的时候,Subscriber可以立即得到通知。在这个例子中,观察者是Subscriber,被观察者是Newspaper。在经典的实现代码中,观察者会将自身注册到被观察者的一个容器中(比如subscriber.registerTo(newspaper))。被观察者发生了任何变化的时候,会主动遍历这个容器,依次通知各个观察者。
使用Qt的信号与槽机制来实现这个实现功能。

2. 代码实现及运行结果

创建一个Qt Console Application控制台项目demo5,当报纸有新内容更新,发射信号给阅读者,与读者打印出接收到的信息。创建完成项目起初只有main.cpp文件和demo5.pro文件。后续的newspaper.h文件、reader.h文件是新建添加到项目工程中的。
1)newspaper.h文件中功能代码实现,如下图:
在这里插入图片描述
2)reader.h文件中功能代码实现,如下图:
在这里插入图片描述
3)main.cpp文件功能代码实现及完整项目运行结果,如下图:
在这里插入图片描述
注1:类Newspaper和类Reader都是继承于QObject类,只有继承于QObject类的类,才具有信号槽的能力。
注2:凡是QObject类(不管是直接子类还是间接子类),都应该在第一行代码写上Q_OBJECT。不管是不是使用信号槽,都应该添加这个宏。这个宏的展开将为我们的类提供信号槽机制、国际化机制以及 Qt 提供的不基于 C++ RTTI 的反射能力。
注3:emit 是 Qt 对 C++ 的扩展,是一个关键字(其实也是一个宏)。emit 的含义是发出,也就是发出信号。

综上所述:

  • 发送者和接收者都需要是QObject的子类(当然,槽函数是全局函数、Lambda 表达式等无需接收者的时候除外);
  • 使用 signals 标记信号函数,信号是一个函数声明,返回 void,不需要实现函数代码;
  • 槽函数是普通的成员函数,作为成员函数,会受到 public、private、protected 的影响;
  • 使用 emit 在恰当的位置发送信号;
  • 使用QObject::connect()函数连接信号和槽。
    ==========================================================================================================================================================================
    Qt 5 引入了信号槽的新语法:使用函数指针能够获得编译期的类型检查。
    在main()函数中,我们使用connect()函数将newspaper对象的newPaper()信号与reader对象的receiveNewspaper()槽函数联系起来。当newspaper发出这个信号时,reader相应的槽函数就会自动被调用。这里我们使用了取址操作符,取到Newspaper::newPaper()信号的地址,同样类似的取到了Reader::receiveNewspaper()函数地址。编译器能够利用这两个地址,在编译期对这个连接操作进行检查,如果有个任何错误(包括对象没有这个信号,或者信号参数不匹配等),编译时就会发现。

有重载的信号
如果信号有重载,比如我们向Newspaper类增加一个新的信号:
在这里插入图片描述
此时如果还是按照前面的写法,编译器会报出一个错误:由于这个函数(注意,信号实际也是一个普通的函数)有重载,因此不能用一个取址操作符获取其地址。回想一下 Qt 4 中的处理。在 Qt 4 中,我们使用SIGNAL和SLOT两个宏来连接信号槽。如果有一个带有两个参数的信号,像上面那种,那么,我们就可以使用下面的代码:
在这里插入图片描述
注意,我们临时增加了一个receiveNewspaper()函数的重载,以便支持两个参数的信号。在 Qt 4 中不存在我们所说的错误,因为 Qt 4 的信号槽连接是带有参数的。因此,Qt 能够自己判断究竟是哪一个信号对应了哪一个槽。

对此,我们也给出了一个解决方案,使用一个函数指针来指明到底是哪一个信号:
在这里插入图片描述
这样,我们使用了函数指针newspaperNameDate声明一个带有QString和QDate两个参数,返回值是 void 的函数,将该函数作为信号,与Reader::receiveNewspaper()槽连接起来。这样,我们就回避了之前编译器的错误。归根结底,这个错误是因为函数重载,编译器不知道要取哪一个函数的地址,而我们显式指明一个函数就可以了。
推荐写法:
QObject::connect(&newspaper,static_cast<void (Newspaper:: )(const QString &, const QDate &)>(&Newspaper::newPaper), &reader,&Reader::receiveNewspaper);

对比上面两种写法。第一个使用的是 C 风格的强制类型转换。此时,如果你改变了信号的类型,那么你就会有一个潜在的运行时错误。例如,如果我们把(const QString &, const QDate &)两个参数修改成(const QDate &, const QString &),C 风格的强制类型转换就会失败,并且这个错误只能在运行时发现。而第二种则是 C++ 推荐的风格,当参数类型改变时,编译器会检测到这个错误。

注意,这里我们只是强调了函数参数的问题。如果前面的对象都错了呢?比如,我们写的newspaper对象并不是一个Newspaper,而是Newspaper2?此时,编译器会直接失败,因为connect()函数会去寻找sender->*signal,如果这两个参数不满足,则会直接报错。

带有默认参数的槽函数
Qt 允许信号和槽的参数数目不一致:槽函数的参数数目可以比信号的参数少。这是因为,我们信号的参数实际是作为一种返回值。正如普通的函数调用一样,我们可以选择忽略函数返回值,但是不能使用一个并不存在的返回值。如果槽函数的参数数目比信号的多,在槽函数中就使用到这些参数的时候,实际这些参数并不存在(因为信号的参数比槽的少,因此并没有传过来),函数就会报错。这种情况往往有两个原因:一是槽的参数就是比信号的少,此时我们可以像前面那种写法直接连接。另外一个原因是,信号的参数带有默认值。比如:void QPushButton::clicked(bool checked = false){}

一种情况,槽函数的参数可以比信号的多,那就是槽函数的参数带有默认值
signals:
void newPaper(const QString &name);
// Reader.h
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));
得到一个断言错误:
在这里插入图片描述
我们不能在函数指针中使用函数参数的默认值。这是 C++ 语言的限制:参数默认值只能使用在直接地函数调用中。当使用函数指针取其地址的时候,默认参数是不可见的!

猜你喜欢

转载自blog.csdn.net/cainiaoxiakexing/article/details/88663448