想说在前面的两句话。
- 信号槽是 Qt 框架引以为豪的机制之一。熟练使用和理解信号槽,能够设计出解耦的非常漂亮的程序,有利于增强我们的程序设计能力。
- 信号与槽是Qt学习的重点,但不是难点。
本篇介绍Qt5中信号槽中connect函数的使用方法,以及connect第五个参数Qt::ConnectionType
的使用。
Qt中信号与槽与经典的观察者模式非常相似,首先我们复习下观察者模式,有一个报纸类Newspaper,有一个订阅者类Subscriber。
Subscriber可以订阅Newspaper。这样,当Newspaper有了新的内容的时候,Subscriber可以立即得到通知。
在这个例子中,观察者是Subscriber,被观察者是Newspaper。在经典的实现代码中,观察者会将自身注册到被观察者的一个容器中(比如subscriber.registerTo(newspaper))。
被观察者发生了任何变化的时候,会主动遍历这个容器,依次通知各个观察者(newspaper.notifyAllSubscribers()) 但Qt的信号槽使用了额外的处理来实现,并不是 GoF 经典的观察者模式的实现方式。
按F1查看Qt助手,可以看到connect
有五个函数重载,分别如下:
//1.QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection)
QLabel *label = new QLabel;
QScrollBar *scrollBar = new QScrollBar;
QObject::connect(scrollBar, SIGNAL(valueChanged(int)), label, SLOT(setNum(int)));
//2.QObject::connect(const QObject *sender, const QMetaMethod &signal, const QObject *receiver, const QMetaMethod &method, Qt::ConnectionType type = Qt::AutoConnection)
//3.QObject::connect(const QObject *sender, const char *signal, const char *method, Qt::ConnectionType type = Qt::AutoConnection) const
//4.QObject::connect(const QObject *sender, PointerToMemberFunction signal, const QObject *receiver, PointerToMemberFunction method, Qt::ConnectionType type = Qt::AutoConnection)
QLabel *label2 = new QLabel;
QLineEdit *lineEdit = new QLineEdit;
QObject::connect(lineEdit, &QLineEdit::textChanged, label2, &QLabel::setText);
//5.QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
void someFunction();
QPushButton *button = new QPushButton;
QObject::connect(button, &QPushButton::clicked, someFunction);
第一个,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 表达式。
其中第1,4,5形式都是比较常用的写法。
通过connect
函数连接信号与槽,这很简单,下面我们看下平时写代码中很少用到第五个参数,也就是连接类型,因为这个参数有默认传参,所以我们几乎不用管它,用默认的Qt::AutoConnection
即可。但有些特殊情况还是需要设置不同的连接类型的,下面我们看一下。
ConnectionType
的枚举
enum ConnectionType {
AutoConnection,
DirectConnection,
QueuedConnection,
BlockingQueuedConnection,
UniqueConnection = 0x80
};
Qt::AutoConnection
: 默认值,使用这个值则连接类型会在信号发送时决定。如果接收者和发送者在同一个线程,则自动使用Qt::DirectConnection类型。如果接收者和发送者不在一个线程,则自动使用Qt::QueuedConnection类型。
Qt::DirectConnection
:槽函数会在信号发送的时候直接被调用,槽函数运行于信号发送者所在线程。效果看上去就像是直接在信号发送位置调用了槽函数。这个在多线程环境下比较危险,可能会造成奔溃。
Qt::QueuedConnection
:槽函数在控制回到接收者所在线程的事件循环时被调用,槽函数运行于信号接收者所在线程。发送信号之后,槽函数不会立刻被调用,等到接收者的当前函数执行完,进入事件循环之后,槽函数才会被调用。多线程环境下一般用这个。
Qt::BlockingQueuedConnection
:槽函数的调用时机与Qt::QueuedConnection一致,不过发送完信号后发送者所在线程会阻塞,直到槽函数运行完。接收者和发送者绝对不能在一个线程,否则程序会死锁。在多线程间需要同步的场合可能需要这个。
Qt::UniqueConnection
:这个flag可以通过按位或(|)与以上四个结合在一起使用。当这个flag设置时,当某个信号和槽已经连接时,再进行重复的连接就会失败。也就是避免了重复连接。
如果有过window下多线程开发经验,就会知道子线程一般是不给直接操作UI的,那咋办?像MFC都是通过API函数::SendMessage
,子线程给UI线程发消息,把这个消息投递到UI的消息队列里,等UI消息执行这个消息的时候,完成更新UI动作。
了解这个原理后,在看一下以上ConnectionType枚举的介绍,好像能明白一点。子线程想要更新UI,如果在子线程中采用DirectConnection
连接,则更新UI的动作就是在子线程完成,这种操作虽然可以,但是有潜在危险,极不推荐。这种操作类似.net中开启跨线程调用是一个道理,都是不建议使用的。
//指定不再捕获对错误线程的调用
Control.CheckForIllegalCrossThreadCalls = false;
下面我们使用QThread开启子线程,然后通过打印当前线程的id来具体看一下不同连接类型的区别,帮助理解
#ifndef CHILDTHREAD_H
#define CHILDTHREAD_H
#include
typedef struct tagMSG
{
QThread* thread; //当前线程
QString process_status; //完成状态
} MSG;
class ChildThread : public QThread
{
Q_OBJECT
public:
explicit ChildThread();
~ChildThread();
protected:
void run() override;
signals:
void send2UI(const MSG& msg);
};
#endif // CHILDTHREAD_H
childthread.cpp
#include "childthread.h"
#include
ChildThread::ChildThread()
{
}
ChildThread::~ChildThread()
{
}
void ChildThread::run()
{
int count = 0;
MSG msg;
msg.thread = QThread::currentThread();
msg.process_status = "waiting...";
while(count++<5)
{
emit send2UI(msg);
QThread::sleep(1);
}
msg.thread = QThread::currentThread();
msg.process_status = "finished";
emit send2UI(msg);
}
在主对话框中,添加一个按钮,并实现槽函数如下:
void Dialog::on_pushButtonChildThread_clicked()
{
ChildThread* ch = new ChildThread;
connect(ch, SIGNAL(send2UI(MSG)), this, SLOT(onShowInfo(MSG)), Qt::AutoConnection);
ch->start();
qDebug()<<"UI Thread:"<<QThread::currentThreadId();
}
void Dialog::onShowInfo(const MSG& msg)
{
qDebug()<<"childThread:"<<msg.thread->currentThreadId();
ui->lineEdit1->setText(msg.process_status);
}
连接类型为AutoConnection的运行结果:
DirectConnection的运行结果:
QueuedConnection的运行结果同AutoConnection
不知道我描述的是不是很清楚,如果不清楚还是没有理解,建议动手写,去理解下。