Qt 信号和槽
1. 信号与槽的概念
Qt信号与槽通信机制能够完成任意两个Qt对象之间的通信,其中,信号会在某个特定的情况或动作下被触发,槽等同于接收并处理信号的函数。使用信号与槽机制进行通信的对象必须是QObject的子类对象,且类的声明中必须使用 Q_OBJECT 宏。
2. 信号
signals:
void SignalAgeChanged(int);
signals为Qt关键字,不是C++关键字,它指出从此处进入信号声明区,信号函数中的参数,就是对象之间通信时交换的数据。
信号函数定义的注意点:
(1)返回值是void类型,因为触发信号函数的目的是执行与其绑定的槽函数,无须信号函数返回任何值;
(2)程序设计者只能声明而不能实现信号函数。信号函数的实现由Qt的MOC工具在程序编译时完成;
(3)信号函数被MOC工具自动设置为protected,因而只有包含一个信号函数的那个类以及其派生类才能使用该信号函数;
(4)信号函数的参数个数、类型由程序设计者自由设定,这些参数的职责是封装类的状态信息,并将信息传递给槽函数;
(5)只有QObject及其派生类才可以声明信号函数;
3. 槽
public slots:
void SlotAgeChanged(int nAge);
槽(Slot)就是对信号响应的函数。槽函数和普通C++成员函数一样,可以定义在类的任何区域(public、protected或private),可以具有任何参数,也可以被直接调用。
槽函数的特点:
(1)槽函数可以有参数,但是参数不能有默认值;
(2)槽函数的返回值为void,因为信号和槽的机制是单向的,即信号被发送后,与其绑定的槽函数会被执行,但不要求槽函数返回任何执行结果;
(3)只有QObject及其派生类才可以定义槽函数;
(4)槽函数的访问权限(public、protected或private)不影响QObject::connect()函数关联信号和槽,只要信号和槽绑定成功,即使槽函数的访问权限为private也会被执行;
4. 信号和槽的关联
信号与槽的关联方式:
- 一对一
- 一对多
- 多对一
- 信号关联信号
// 采用函数名称的形式关联信号槽
QMetaObject::Connection QObject::connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type = Qt::AutoConnection);
// 如果信号和槽函数有参数,需要把形参也写上
connect(sender, SIGNAL(signal_function(QString)), receiver, SLOT(slot_function(QString)));
// 采用函数指针的形式关联信号槽
QMetaObject::Connection QObject::connect(const QObject *sender, PointerToMemberFunction signal, const QObject *receiver, PointerToMemberFunction method, Qt::ConnectionType type = Qt::AutoConnection);
// 不用写函数的形参
connect(sender, &SenderClass::signal_function), receiver, &ReceiverClass::slot_function);
建议采用第二中函数指针的写法,因为当信号和槽的函数参数较多时,第一种方法要写很多形参代码,不仅代码多,还容易出错;
// .h
#ifndef STUDENT_H
#define STUDENT_H
#include <QObject>
class Student : public QObject
{
Q_OBJECT
public:
explicit Student(QObject *parent = nullptr);
~Student() = default;
public:
void SetAge(int nAge);
int GetAge();
public slots:
// 没有定义成槽函数的形式,只是普通类成员函数
void AgeChanged(int nAge);
signals:
void SignalAgeChanged(int);
private:
int m_nAge;
};
#endif // STUDENT_H
// .cpp
#include "Student.h"
#include <QDebug>
Student::Student(QObject *parent) : QObject(parent), m_nAge(15)
{
}
void Student::SetAge(int nAge)
{
m_nAge = nAge;
emit SignalAgeChanged(nAge);
}
int Student::GetAge()
{
return m_nAge;
}
void Student::AgeChanged(int nAge)
{
qDebug() << "Age changed = " << nAge;
}
#include <QCoreApplication>
#include "Student.h"
// 测试
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
Student stu;
QObject::connect(&stu, &Student::SignalAgeChanged, &stu, &Student::AgeChanged);
stu.SetAge(10);
return a.exec();
}
connect函数的第五个参数,默认采用Qt::AutoConnection的方式,此方式下Qt会根据信号员对象和接收对象所属的线程来处理;
常量 | 值 | 含义 |
---|---|---|
Qt::AutoConnection | 0 | (Default) If the receiver lives in the thread that emits the signal, Qt::DirectConnection is used. Otherwise, Qt::QueuedConnection is used. The connection type is determined when the signal is emitted. |
Qt::DirectConnection | 1 | The slot is invoked immediately when the signal is emitted. The slot is executed in the signalling thread. |
Qt::QueuedConnection | 2 | The slot is invoked when control returns to the event loop of the receiver’s thread. The slot is executed in the receiver’s thread. |
Qt::BlockingQueuedConnection | 3 | Same as Qt::QueuedConnection, except that the signalling thread blocks until the slot returns. This connection must not be used if the receiver lives in the signalling thread, or else the application will deadlock. |
Qt::UniqueConnection | 0x80 | This is a flag that can be combined with any one of the above connection types, using a bitwise OR. When Qt::UniqueConnection is set, QObject::connect() will fail if the connection already exists (i.e. if the same signal is already connected to the same slot for the same pair of objects). This flag was introduced in Qt 4.6. |
5. 信号和槽传值的问题
- 在同一个线程中
当信号和槽都在同一个线程中时,值传递参数和引用传递参数有区别: 值传递会复制对象;引用传递不会复制对象;
还是以之前的示例为程序,将信号与槽的值参数改为引用:
#ifndef STUDENT_H
#define STUDENT_H
#include <QObject>
class Student : public QObject
{
Q_OBJECT
public:
explicit Student(QObject *parent = nullptr);
~Student() = default;
public:
void SetAge(int nAge);
int GetAge();
public slots:
// 没有定义成槽函数的形式,只是普通类成员函数
void AgeChanged(int& nAge); // 信号和槽都改成了引用
signals:
void SignalAgeChanged(int&); // 信号和槽都改成了引用
private:
int m_nAge;
};
#endif // STUDENT_H
#include "Student.h"
#include <QDebug>
Student::Student(QObject *parent) : QObject(parent), m_nAge(15)
{
}
void Student::SetAge(int nAge)
{
m_nAge = nAge;
emit SignalAgeChanged(m_nAge);
}
int Student::GetAge()
{
return m_nAge;
}
void Student::AgeChanged(int& nAge)
{
qDebug() << "Age changed = " << nAge;
nAge = 20; // 将数据改为 20
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
Student stu;
QObject::connect(&stu, &Student::SignalAgeChanged, &stu, &Student::AgeChanged);
stu.SetAge(10);
qDebug() << "Age = " << stu.GetAge(); // 打印出来的值为 20
return a.exec();
}
在槽函数中,重新设置了值,最后结果变成了在槽函数中设置的值。
- 不在同一个线程中
当信号和槽不在同一个线程中时,分两种情况:
- connect时使用AutoConnection(跨线程默认是QueuedConnection):值传递参数和引用传递参数没有区别,都会复制对象;
- connect时使用DirectConnection,测试结果和在同一线程中的结果相同;
// Student类
#ifndef STUDENT_H
#define STUDENT_H
#include <QObject>
class Student : public QObject
{
Q_OBJECT
public:
explicit Student(QObject *parent = nullptr);
~Student() = default;
public:
void SetName(QString strName);
QString GetName();
signals:
void SignalNameChanged(const QString&);
private:
QString m_strName;
};
#include "Student.h"
#include <QDebug>
Student::Student(QObject *parent) : QObject(parent)
{
}
void Student::SetName(QString strName)
{
m_strName = strName;
emit SignalNameChanged(m_strName);
}
QString Student::GetName()
{
return m_strName;
}
// Worker类
#endif // STUDENT_H
#ifndef MYWORKER_H
#define MYWORKER_H
#include <QObject>
#include <Windows.h>
class MyWorker : public QObject
{
Q_OBJECT
public:
explicit MyWorker(HANDLE hEvent, QObject *parent = nullptr);
public slots:
void SlotNameChanged(const QString& strName);
private:
HANDLE m_hEvent;
};
#endif // MYWORKER_H
#include "MyWorker.h"
MyWorker::MyWorker(HANDLE hEvent, QObject *parent) : QObject(parent), m_hEvent(hEvent)
{
}
void MyWorker::SlotNameChanged(const QString& strName)
{
QString& strNameTemp = const_cast<QString&>(strName);
strNameTemp = QString("Zhangsan"); // 设置为 Zhangsan
SetEvent(m_hEvent);
}
// 使用 DirectConnection
#include <QCoreApplication>
#include <Windows.h>
#include "Student.h"
#include <QDebug>
#include <QThread>
#include <memory>
#include "MyWorker.h"
using std::unique_ptr;
void Test()
{
Student stu;
HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
unique_ptr<MyWorker> upWorker = std::make_unique<MyWorker>(hEvent);
unique_ptr<QThread> upThread = std::make_unique<QThread>();
upWorker->moveToThread(upThread.get());
QObject::connect(&stu, &Student::SignalNameChanged, upWorker.get(), &MyWorker::SlotNameChanged, Qt::DirectConnection);
upThread->start();
ResetEvent(hEvent);
stu.SetName(QString("Wangwu"));
DWORD nRtn = WaitForSingleObject(hEvent, 5000);
qDebug() << "Return = " << nRtn;
qDebug() << "Name = " << stu.GetName(); // 输出 Zhangsan
upThread->quit();
upThread->wait();
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
Test();
return a.exec();
}
// 使用 AutoConnection
#include <QCoreApplication>
#include <Windows.h>
#include "Student.h"
#include <QDebug>
#include <QThread>
#include <memory>
#include "MyWorker.h"
using std::unique_ptr;
void Test()
{
Student stu;
HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
unique_ptr<MyWorker> upWorker = std::make_unique<MyWorker>(hEvent);
unique_ptr<QThread> upThread = std::make_unique<QThread>();
upWorker->moveToThread(upThread.get());
QObject::connect(&stu, &Student::SignalNameChanged, upWorker.get(), &MyWorker::SlotNameChanged);
upThread->start();
ResetEvent(hEvent);
stu.SetName(QString("Wangwu"));
DWORD nRtn = WaitForSingleObject(hEvent, 5000);
qDebug() << "Return = " << nRtn;
qDebug() << "Name = " << stu.GetName(); // 输出 Wangwu
upThread->quit();
upThread->wait();
}
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
Test();
return a.exec();
}