一、前言
Qt成为我们今天拥有的灵活而舒适的工具,除了友好和能够快速开发设计师界面,信号槽机制是最大的核心特征,也是区别于其他开发框架最大的优势。
Qt的信号槽作用于两个对象之间的通信。当一个对象发生了改变,它希望其他关心它的对象能够了解到这些变化。比如一个编辑框A的数据发生了改变,而编辑框B的需要根据A的值发生数据变化,这时候我们使用信号槽机制,A产生数据变化信号,B响应信号做自身数据做改变。
我们用来表达算法的语法会显著影响代码的可读性和可维护性。Qt的信号和槽的语法在实践中被证明是非常成功的。语法直观,使用简单,易于阅读。这可以帮助程序员从一开始就正确地进行设计,甚至不必考虑设计模式。这满足安全和高效的图形用户界面编程,Qt希望我们构建更干净、更安全、更符合UNIX精神的系统。
二、Signals and Slots
要实现对象之间的通讯方法有很多。
1)初学者可能会通过定时器检测目标对象的属性变化而动作,这类方法缺点很明显,代码乱、耗资源、效率低、无法直接传递多个参数等等。
2)资深的程序员会使用回调callback机制,这是一种函数指针的用法,想让别人的代码执行自己的代码。把自身需要执行的代码写在一个函数里面,让目标对象去调用此函数,实现对象间的通讯。但是如果多对象类都想要关心目标对象的状态变化,此时需要维护一个列表,以存放多个回调函数的地址(函数指针),对于每一个被关注的对象,都需要做类似的工作,并且对象需要动态销毁的时候,还需要关注这些函数指针的回收,这样的设计效率低且不灵活。
3)信号槽机制类似于设计模式中的观察者模式(当一个对象状态发生改变的时候,所有依赖于它的对象都得到通知并被自动更新)。被观察者发出信号Signals,观察者监听信号,设计槽函数Slots关联信号实现动作。
值得一提的是,信号槽与回调函数的区别,是面试中经常出现的问题。
信号槽优点
1)松耦合性
目标对象不需要知道哪个对象关心自己,发出的信号类似于广播,如果有人对信号感兴趣,使用connect连接信号与自身的处理槽函数即可。而回调函数需要目标对象去处理对自己感兴趣的全部对象的函数指针。耦合性明显Qt信号槽机制更优。
2)类型安全
需要关联的信号槽的签名需要是相同的,即参数型号、参数个数必须一致。如果不一致的话编译器会报错。
3)灵活性
信号槽机制支持一个信号多个槽响应,也可以一个槽响应多个信号,还可以直接信号触发信号(适用于逻辑层信号夹带数据不需要处理直接转发)。
4)内存安全
信号槽机制在UI编程上具备优势,能够帮助程序员环节内存泄漏问题,当应用程序创建了一个具有父窗口部件的对象时,该对象被加入父窗口的child列表,当父窗口被销毁时,child列表中的对象被一一删除,子类释放的顺序与构造顺序相反。
信号槽缺点
1)速度较慢。
与回调函数相比,信号和槽机制运行速度比直接调用非虚函数慢10倍。照官方说法是需要定位接收信号的对象、需要遍历全部关联槽、需要编组和接祖传递参数。但依旧能够满足大多数应用的需求,因为1秒钟可以出发200万次这样的信号(i586-500机器,1个信号绑定一个槽函数,绑定两个1秒可以触发120万次)。
信号槽连接类型
connect连接信号槽第五个参数为连接类型,通常情况下默认为Qt::AutoConnection,自动设置。
类型 描述 Qt::AutoConnection 如果信号与槽在同一个线程中则为Qt::DirectConnection直连、否则为Qt::QueuedConnection队列。 Qt::DirectConnection 直连,发出信号的时候立刻调用槽函数 Qt::QueuedConnection 队列,信号与槽不同线程时,使用此类型 Qt::BlockingQueuedConnection 阻塞队列,等待槽函数返回,信号和槽不可在同一个线程,否则死锁 Qt::UniqueConnection 一个标识,与上述几种组合使用,避免相同信号槽多次连接
信号槽使用注意事项
1)默认情况下信号槽被多次连接,导致触发信号的时候槽函数就会被多次调用。可以使用Qt::UniqueConnection解决此问题。
2)Qt::BlockingQueuedConnection等于一个阻塞的机制,子线程执行信号必须等待主线程返回才会继续往下走,默认不阻塞。
3)多线程中,信号槽连接类型为Qt::QueuedConnection,所以信号触发的时候不会马上执行槽函数,存在实时性的风险。
4)信号触发的速度过快,而槽函数响应不过来。比如当你做一个视频播放器,摄像头的数据一直在刷新,但是界面上刷新不过来,槽函数处理的速度比不上信号触发的速度,如果这时候使用的是Qt::DirectConnection,导致程序可能异常。
5) 信号槽传递的参数必须是Qt的元对象系统所知道的类型,因为Qt需要复制参数以将它们存储在后台的事件中,否则编译会报错。如果需要使用自定义类型作为参数传递,使用Q_DECLARE_METATYPE()注册它。
6)所有信号的声明都是公有的,所以不能在signals前面加public,private,proteed
7)信号没有返回值,只需要声明不需要定义。
7)自定义类必须直接或间接继承自QObject类,并且开头私有声明包含Q_OBJECT,才能使用信号槽机制。
信号槽连接
1)传统写法-暴露参数
QLabel *label = new QLabel;
QScrollBar *scrollBar = new QScrollBar;
QObject::connect(scrollBar, SIGNAL(valueChanged(int)),
label, SLOT(setNum(int)));
//带连接类型
QObject::connect(scrollBar, SIGNAL(valueChanged(int)),
label, SLOT(setNum(int)),Qt::AutoConnection);
2)进阶写法-隐藏参数
QLabel *label = new QLabel;
QLineEdit *lineEdit = new QLineEdit;
QObject::connect(lineEdit, &QLineEdit::textChanged,
label, &QLabel::setText);
//带连接类型
QObject::connect(lineEdit, &QLineEdit::textChanged,
label, &QLabel::setText,Qt::AutoConnection);
3)直接触发当前函数
void someFunction();
QPushButton *button = new QPushButton;
QObject::connect(button, &QPushButton::clicked, someFunction);
//带连接类型
QObject::connect(button, &QPushButton::clicked, someFunction,
Qt::AutoConnection);
4)Lambda表达式
QByteArray page = ...;
QTcpSocket *socket = new QTcpSocket;
socket->connectToHost("qt-project.org", 80);
QObject::connect(socket, &QTcpSocket::connected, [=] () {
socket->write("GET " + page + "\r\n");
});
//带连接类型
QObject::connect(socket, &QTcpSocket::connected, [=] () {
socket->write("GET " + page + "\r\n");
},Qt::AutoConnection);
识别信号来源
当有多个对象发送信号都通过一个槽来处理的时候,我们就需要在槽中识别出这些信号然后做相应的处理。
多个lineEdit编辑框当内容发送变化的时候,我们统一设计一个槽函数来做处理,使用sender()返回发送信号的指针。
void QMainWindows::textChaned(QString str)
{
QLineEdit* edit = dynamic_cast<QLineEdit*>(sender())
if (edit == ui->Edit_name){
//do something
}
else if (edit == ui->Edit_number){
//do something
}
}