Qt信号-槽

这篇文档从使用到实现去讲信号-槽,适合小白到使用一两年的。
对于Qt使用而言,信号-槽是我们津津乐道的一个功能,那我们先来看我们会怎么使用它。

初步认识

Qt的信号-槽的写法比较简单。
1、如果一个类需要使用信号-槽,那么需要这个类继承从QObject继承,并且在类中调用使用宏 Q_OBJECT

如果在非QObject的子类中使用Q_OBJECT,将编译报错:

error: Class contains Q_OBJECT macro but does not inherit from QObject

2、做关联操作,通常我们使用以下方式。

方式1:在Qt5中引入,是现在的一般使用方式。这种方式可以明确信号和槽所属的类。

QObject::connect(
  const QObject* sender, const QMetaMethod & signal,
  const QObject* receiver, const QMetaMethod & method, 
  Qt::ConnectionType type = Qt::AutoConnection
)
demo:
connect(this, &Test::sendSignals, this, &Test::doSlots);

方式2:在Qt5之前就引入,现在一般不使用。但在特殊场景下,还是需要。如类中出现“函数重载”【函数名相同,但参数不同】

QObject::connect(
  const QObject* sender, const char * signal, 
  const QObject * receiver, const char * method, 
  Qt::ConnectionTypetype = Qt::AutoConnection
)
demo:
connect(this, SIGNAL(sendSignals(int)), this, SLOT(doSlots(int)));

方法3:是方法2的特殊情况,关联全局函数。

connect(
  const QObject *sender, const char *signal,
  const char *member, Qt::ConnectionType type = Qt::AutoConnection
)

常规使用

通常我们使用的信号-槽,信号是控件已经定义好的,而槽是我们代码实现的。如点击按钮弹出提示框。
槽函数标志:

slots
或者
Q_SLOTS

代码:

class Widget : public QWidget
{
    Q_OBJECT
public:
    Widget(QWidget *parent = nullptr)
        :QWidget(parent)
    {
        QPushButton *pushBtn = new QPushButton(QString("test"), this);
        connect(pushBtn, &QPushButton::released, this, &Widget::doMessage);
    }

private slots: // 定义槽
    void doMessage()
    {
        QMessageBox::information(this, QStringLiteral("提示"), QStringLiteral("点击成功"));
    }
};

上述代码实现了一个按钮点击,弹出【点击成功】的提示。

自定义信号

当时当要做一些复杂操作,如自定义控件时。需要定义信号,发送信号。
信号标志:

signals
或者
Q_SIGNALS

发送信号:

emit
或者
Q_EMIT

代码:

class test : public QObject
{
    Q_OBJECT
public:
    test() {}
    virtual ~test() {}
public slots: // 定义槽
    void doTest()
    {
        QMessageBox::information(nullptr, QStringLiteral("提示"), QStringLiteral("doTest"));
    }
signals: // 定义信号
    void testSignal();
};

class Widget : public QWidget
{
    Q_OBJECT
public:
    Widget(QWidget *parent = nullptr)
        :QWidget(parent), pTest(new test)
    {
        QPushButton *pushBtn = new QPushButton(QString("test"), this);
        connect(pushBtn, &QPushButton::released, this, &Widget::doMessage);
        connect(pTest, &test::testSignal, pTest, &test::doTest);
    }
    virtual ~Widget() { delete pTest; }

private slots: // 定义槽
    void doMessage()
    {
        QMessageBox::information(this, QStringLiteral("提示"), QStringLiteral("点击成功"));
        emit pTest->testSignal(); // 发送信号
    }
private:
    test *pTest;
};

上述代码实现了一个按钮点击,弹出【点击成功】的提示;点击确定后,发送【testSignal】信号,弹出【doTest】提示。


深入使用

在可以完成基本使用后,还需要了解一下特殊的使用方式。

最后一个参数 Qt::ConnectionType

在初步认识中,了解到Qt的信号-槽简单使用。但是最后一个参数一直使用默认值。现在要了解最后一个参数的用处。

 Qt::ConnectionType type = Qt::AutoConnection

看官方文档和翻译:

enum Qt::ConnectionType
This enum describes the types of connection that can be used between signals and slots. In particular, it determines whether a particular signal is delivered to a slot immediately or queued for delivery at a later time.
这个枚举类型列举了信号和槽之间的关系;特别是,它决定了一个信号是立刻传递到槽,还是在队列中等待。

Constant Value Description
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关系。否则,使用Qt::QueuedConnection关系。这个链接关系在信号发送后确定。
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::QueuedConnection类型 ,特殊的地方是:发送信号的线程将阻塞,直到槽函数返回。如果发送中和接受者在一个线程是,这个类型不能使用,否则程序将死锁。
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.这是一个标志符。任何类型都可以使用按位与连接这个标志。如果已经存在一个相同的连接时,再使用一个标志添加一个新链接。新链接将添加失败。这边标志在Qt4.6中引入。

通过阅读上官网文档,了解到这个参数有两个作用:
1. 处理多线程情况下的信号槽
2. 避免多个相同关联

槽函数的参数

槽函数,做为一个函数,当然要是有参数的,这个机制信号-槽之间转到参数。上述情况,已经了解到,信号槽机制可以跨线程的使用。

槽函数的参数在跨线程的情况下也会用,需要特殊注意的是:在跨线程时能支持一般Qt类型的专递;但是对于自定义数据的情况,需要进行类型注册。

#include  // 需要头文件
使用1:
qRegisterMetaType("type") // type是要注册的类型
如果要注册引用:
qRegisterMetaType("type &")

代码:

#include 
#include 
#include 
#include 
#include 
#include 
#include 

struct ThreadInfo
{
    int threadId;
    int processId;
};

class TextThread : public QThread
{
    Q_OBJECT
public:
    TextThread()
        : QThread()
    {
        m_Info.threadId = 1;
        m_Info.processId = 2;
        m_stop = false;
    }

    void run()
    {
        while(!m_stop)
        {
            emit sendText(m_Info);
            sleep(5);
        }
    }

    void stop()
    {
        m_stop = true;
    }
signals:
    void sendText(ThreadInfo info);
private:
    ThreadInfo m_Info;
    bool m_stop;
};

class Widget : public QWidget
{
    Q_OBJECT
public:
    Widget(QWidget *parent = nullptr)
        :QWidget(parent)
    {
    }
    virtual ~Widget() {}

public slots: // 定义槽
    void doMessage(ThreadInfo info)
    {
        QMessageBox::information(
                    this,
                    QString("txt"),
                    QString("processId %1; threadId %2")
                    .arg(info.processId)
                    .arg(info.threadId)
                    );
    }
};

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget w;
    QPushButton *pBtn = new QPushButton("stop", &w);

    qRegisterMetaType("ThreadInfo");

    TextThread oThread;
    QObject::connect(&oThread, &TextThread::sendText,
                     &w, &Widget::doMessage, Qt::QueuedConnection);

    QObject::connect(
                pBtn, &QPushButton::released,
                [&]()
                {
                    qDebug()<< QStringLiteral("lambda 执行");
                    oThread.stop();
                    oThread.wait();
                    qDebug()<< QStringLiteral("lambda 完成");
                }
    );

    w.show();
    oThread.start();
    return a.exec();
}

对于信号和槽的参数关系,要注意一个细节:

    1. 信号的参数个数要不少于槽。
    1. 信号和槽之间对应的参数,类型要一致。

ps:这部分涉及到元类型系统,这里只是介绍一部分用法。详细情况,请移步到元类型系统部分。


一个信号多个槽的触发情况

一个信号,多个相同槽

class Widget : public QWidget
{
    Q_OBJECT

public:
    Widget(QWidget *parent = nullptr)
        : QWidget(parent)
    {
        QPushButton *pushBtn = new QPushButton(QString("test"), this);
        connect(pushBtn, &QPushButton::released, this, &Widget::doMessage);
        connect(pushBtn, &QPushButton::released, this, &Widget::doMessage);
    }
    virtual ~Widget() {}

private slots:
    void doMessage()
    {
        QMessageBox::information(this, QStringLiteral("提示"), QStringLiteral("点击成功"));
    }
};

上述代码实现了一个按钮点击,弹出【点击成功】的提示2次。这说明:一个信号,多个相同槽将触发多次。

一个信号,多个不同槽

直连方式Qt::DirectConnection

class test : public QObject
{
    Q_OBJECT
public:
    test() : test1(0), test2(0), test3(0), test4(0) {}
    virtual ~test() {}
public slots: // 定义槽
    void doTest1() {qDebug() << "doTest1 :" << ++test1; }
    void doTest2() {qDebug() << "doTest2 :" << ++test2; }
    void doTest3() {qDebug() << "doTest3 :" << ++test3; }
    void doTest4() {qDebug() << "doTest4 :" << ++test4; qDebug() << ""; }
signals: // 定义信号
    void testSignal();
private:
    int test1;
    int test2;
    int test3;
    int test4;
};

class Widget : public QWidget
{
    Q_OBJECT
public:
    Widget(QWidget *parent = nullptr)
        :QWidget(parent), pTest(new test)
    {
        QPushButton *pushBtn = new QPushButton(QString("test"), this);
        connect(pushBtn, &QPushButton::released, this, &Widget::doMessage);
        connect(pTest, &test::testSignal, pTest, &test::doTest1);
        connect(pTest, &test::testSignal, pTest, &test::doTest2);
        connect(pTest, &test::testSignal, pTest, &test::doTest3);
        connect(pTest, &test::testSignal, pTest, &test::doTest4);
    }
    virtual ~Widget() { delete pTest; }

private slots: // 定义槽
    void doMessage()
    {
        for (int i = 0; i < 100; i++)
             emit pTest->testSignal(); // 发送信号
    }
private:
    test *pTest;
};

执行结果:按顺序执行。
发现:在同一个线程,DirectConnection方式下;槽函数的调用顺序和槽函数关联的顺序相同。

Qt::QueuedConnection方式

class test : public QObject
{
    Q_OBJECT
public:
    test() : test1(0), test2(0), test3(0), test4(0) {}
    virtual ~test() {}
public slots: // 定义槽
    void doTest1() {qDebug() << "doTest1 :" << ++test1; }
    void doTest2() {qDebug() << "doTest2 :" << ++test2; }
    void doTest3() {qDebug() << "doTest3 :" << ++test3; }
    void doTest4() {qDebug() << "doTest4 :" << ++test4; qDebug() << ""; }
signals: // 定义信号
    void testSignal();
private:
    int test1;
    int test2;
    int test3;
    int test4;
};

class Widget : public QWidget
{
    Q_OBJECT
public:
    Widget(QWidget *parent = nullptr)
        :QWidget(parent), pTest(new test)
    {
        QPushButton *pushBtn = new QPushButton(QString("test"), this);
        connect(pushBtn, &QPushButton::released, this, &Widget::doMessage);
        connect(pTest, &test::testSignal, pTest, &test::doTest1);
        connect(pTest, &test::testSignal, pTest, &test::doTest2, Qt::QueuedConnection);
        connect(pTest, &test::testSignal, pTest, &test::doTest3);
        connect(pTest, &test::testSignal, pTest, &test::doTest4, Qt::QueuedConnection);
    }
    virtual ~Widget() { delete pTest; }

private slots: // 定义槽
    void doMessage()
    {
        for (int i = 0; i < 100; i++)
             emit pTest->testSignal(); // 发送信号
    }
private:
    test *pTest;
};

执行结果:1和3先调用,2和4后调用。
发现:在同一个线程,QueuedConnection方式下;槽函数的调用顺序和槽函数关联的顺序相同。

由上两次尝试,得到结论:信号-槽的关联顺序,就是程序尝试调用槽的顺序。槽函数被调用的顺序和具体情况有关(连接方式,多线程)。


多线程与信号槽

由上面的介绍可以得知:相同线程的信号-槽之间的关系推荐使用直连(Qt::DirectConnection),不同线程的信号-槽之间的关系推荐使用队列(Qt::QueuedConnection 或 Qt::BlockingQueuedConnection)。

但是在默认情况下,刚接触很难辨别清楚,如下举3个例子。

Demo1

class Thread : public QThread
{
    Q_OBJECT
public slots:
    void testSlot() {...}
protected:
    void run() 
    {
        QObject *obj = new Object;
        connect(obj, SIGNAL(testSiganl()), this, SLOT(testSlot()));
    }
};

分析:
Thread对象是在主线程创建的,属于主线程
Object对象是在子线程运行中创建的,属于子线程
信号在子线程中发射,属于子线程,槽函数属于主线程,是队列 (Qt::QueuedConnection)

Demo2:

class Thread : public QThread
{
    Q_OBJECT
public slots:
    void testSlot() {...}
    
protected:
    void run() {...}
};
Thread thread;
QObject *obj = new Object;
connect(obj, SIGNAL(testSiganl()), this, SLOT(testSlot()));
thread.start();
emit obj->testSiganl();

分析:
Thread对象是在主线程创建的,属于主线程
Object对象是在主线程创建的,属于主线程
信号在主线程中发射,属于主线程,槽函数属于主线程,是直连 (Qt::DirectConnection)

Demo3:

class Thread : public QThread
{
    Q_OBJECT
signals:
    void testSignal();
    
protected:
    void run() { emit testSignal(); }
};
Thread thread;
QObject *obj = new Object;
connect(&thread, SIGNAL(testSiganl()), obj, SLOT(testSlot()));
thread.start();

分析:
Thread对象是在主线程创建的,属于主线程
Object对象是在主线程创建的,属于主线程
信号在子线程中发射,属于子线程,槽函数属于主线程,是直连 (Qt::QueuedConnection)


关于宏

声明用的宏的定义

#ifndef QT_NO_META_MACROS
# if defined(QT_NO_KEYWORDS)
#  define QT_NO_EMIT
# else
#   ifndef QT_NO_SIGNALS_SLOTS_KEYWORDS
#     define slots
#     define signals public
#   endif
# endif
# define Q_SLOTS
# define Q_SIGNALS public
# define Q_PRIVATE_SLOT(d, signature)
# define Q_EMIT
#ifndef QT_NO_EMIT
# define emit
#endif

解析:
槽:slots、Q_SLOTS的宏是空,需要前置声明 public、protected、private
信号:signals 、Q_SIGNALS 的宏是 public
发送:emit、Q_EMIT的宏是空

不用宏,对照对应的内容,也可以正常使用。

关联用的宏的定义

Q_CORE_EXPORT const char *qFlagLocation(const char *method);
#ifndef QT_NO_META_MACROS
#ifndef QT_NO_DEBUG
# define QLOCATION "\0" __FILE__ ":" QT_STRINGIFY(__LINE__)
# ifndef QT_NO_KEYWORDS
#  define METHOD(a)   qFlagLocation("0"#a QLOCATION)
# endif
# define SLOT(a)     qFlagLocation("1"#a QLOCATION)
# define SIGNAL(a)   qFlagLocation("2"#a QLOCATION)
#else
# ifndef QT_NO_KEYWORDS
#  define METHOD(a)   "0"#a
# endif
# define SLOT(a)     "1"#a
# define SIGNAL(a)   "2"#a
#endif

解析:
用宏进行关联的用法就是调用

QObject::connect(
  const QObject* sender, const char * signal, 
  const QObject * receiver, const char * method, 
  Qt::ConnectionTypetype = Qt::AutoConnection
)
demo:
connect(this, SIGNAL(sendSignals(int)), this, SLOT(doSlots(int)));

结合QMetaObject讨论实现

如果没有信号槽,会怎么实现这种机制?

你可能感兴趣的:(Qt信号-槽)