Qt中的信号和槽

原创

信号和槽是Qt对象间通信的一种机制,是Qt的核心特性,也是Qt与其他应用框架的显著区别。

1. 信号和槽的基本使用

信号和槽的基本使用如下:

  • 使用信号和槽的类必须从QObject类或它的派生类继承,如果多继承,必须把QObject类放在第一位。
  • 派生类在声明的一开始放置Q_OBJECT宏,它帮你定义元对象系统必须定义的一些变量和方法。
  • 使用signals关键字声明信号,不要实现与信号同名的函数。
  • 使用slot关键字声明槽函数,并实现槽函数。
  • 使用关键字emit发送信号。
  • 使用connect函数连接信号和槽函数

下面是一个Qt信号和槽的基本使用,先看一下程序的运行效果,下图所示

当点击Add1按钮后,输入框显示的数字自增1;当点击Sub1按钮后,输入框中显示的数字自减一。

头文件,SignalsAndSlotDemo.h

#ifndef SIGNALSANDSLOT_H
#define SIGNALSANDSLOT_H
#include <QPushButton>
#include <QLabel>
#include <QLineEdit>
#include <QVBoxLayout>
#include "UIBase/UIBaseWindow.h"

class SignalsAndSlotDemo : public UIBaseWindow
{
    Q_OBJECT
public:
    SignalsAndSlotDemo(QWidget *parent = nullptr);
    ~SignalsAndSlotDemo();

private:
    QLineEdit *m_ValueEdit = nullptr;
    QPushButton *m_AddButton = nullptr;
    QPushButton *m_SubButton = nullptr;

signals:
    void subSignals(void);

private slots:
    void onClickedAddButton(void);
    void onClickedSubButton(void);

private:
    void onRecvSubSignals(void);
};

#endif

源文件, SignalsAndSlotDemo.cpp

#include "SignalsAndSlot.h"
#include "UIBase/UIGlobalTool.h"

SignalsAndSlotDemo::SignalsAndSlotDemo(QWidget *parent)
    :UIBaseWindow(parent)
{
    QVBoxLayout *mainLayout = new QVBoxLayout(this);

    m_ValueEdit = new QLineEdit("0");
    m_ValueEdit->setReadOnly(true);
    m_AddButton = new QPushButton(tr("Add 1"));
    m_SubButton = new QPushButton(tr("Sub 1"));

    // 添加按钮阴影
    g_GlobalTool->addShadowEffect(m_AddButton);
    g_GlobalTool->addShadowEffect(m_SubButton);

    QHBoxLayout *bottomLayout = new QHBoxLayout;
    bottomLayout->addWidget(m_AddButton);
    bottomLayout->addWidget(m_SubButton);

    mainLayout->addSpacerItem(new QSpacerItem(10, 30));
    mainLayout->addWidget(m_ValueEdit);
    mainLayout->addLayout(bottomLayout);

    // 链接信号和槽函数
    QObject::connect(m_AddButton, SIGNAL(clicked()), this, SLOT(onClickedAddButton()));
    QObject::connect(m_SubButton, &QPushButton::clicked, this, &SignalsAndSlotDemo::onClickedSubButton);
    QObject::connect(this, &SignalsAndSlotDemo::subSignals, [&](void)->void{
        onRecvSubSignals();
    });
}

SignalsAndSlotDemo::~SignalsAndSlotDemo()
{

}

void SignalsAndSlotDemo::onClickedAddButton(void)
{
    int value = m_ValueEdit->text().toInt();
    QString valueString = QString::number(++value);
    m_ValueEdit->setText(valueString);
}

void SignalsAndSlotDemo::onClickedSubButton(void)
{
    emit subSignals();
}

void SignalsAndSlotDemo::onRecvSubSignals(void)
{
    int value = m_ValueEdit->text().toInt();
    QString valueString = QString::number(--value);
    m_ValueEdit->setText(valueString);
}

代码中创建了两个按钮,当点击“Add 1”按钮后,调用槽函数onClickedAddButton(),当点击“Sub 1”按钮后, 调用槽函数onClickedSubButton(),而onClickedSubButton()槽函数中有发送信号subSignals,subSignals信号 又连接一个lambda匿名函数,lambda匿名函数中调用函数onRecvSubSignals()

这里使用了3种信号和槽的连接方式

  • 第一种,源程序的第27行,函数原型如下
    static QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *member, Qt::ConnectionType = Qt::AutoConnection)
    sender: 为发送信号对象的指针。
    signal: 为信号的名字,使用SIGNAL宏修饰信号。
    receiver: 为接收信号的对象指针。
    member: 为槽函数的名字,使用SLOT宏修饰槽函数。
    Qt::ConnectionType: 最后一个为信号和槽的连接方式。
  • 第二种,源程序的第27行,函数原型如下 static QMetaObject::Connection connect(const QObject *sender, const QMetaMethod &signal, const QObject *receiver, const QMetaMethod &method, Qt::ConnectionType type = Qt::AutoConnection);
    const QMetaMethod: 元对象的成员方法类。
  • 第三种,源程序的第27行,函数原型如下 QMetaObject::Connection QObject::connect(const QObject *asender, const char *asignal, const char *amember, Qt::ConnectionType atype) const{ return connect(asender, asignal, this, amember, atype); }
    可以使用lambda匿名函数的方式连接信号和槽。

2. 信号和槽的连接方式

上面介绍信号和槽的连接时都有一个默认的参数Qt::ConnectionType = Qt::AutoConnection,这个参数就是信号和槽的连接方式,具体每一种连接详细描述如下:

  • AutoConnection: 自动连接。connect连接类型的默认值,如果信号和槽在同一个线程,则槽函数被直接调用,效果同DirectConnection连接方式;如果信号和槽函数分属在两个不同的线程,则和QueuedConnection连接方式相同,通过向槽所在的线程发送QMetaCallEvent来异步调用槽函数。
  • DirectConnection: 直接连接。信号发送时,槽函数直接被调用。请不要再不同的线程中使用直接连接方式,因为这不是线程安全的。
  • QueuedConnection: 队列连接。信号发送时,槽不会立刻被调用,而是向拥有槽的接受者所在的线程发送QMetaCallEvent事件,等到线程的事件循环分拣出该事件时才执行。槽在接收者的线程环境中执行。
  • BlockingQueuedConnection: 阻塞队列式连接。当信号发送时,阻塞信号所在的线程,一直等到接收接收槽函数所在的线程执行槽函数后才解除阻塞。请不要在同一线程中使用该连接方式。

3. 信号和槽的一些注意事项

  • 一个信号可以连接一个或多个槽函数,一个槽函数也可以接收多个信号。
  • 槽的参数必须和信号的类型匹配,槽的参数可以少于但不能多于信号的参数的个数。
  • 连接信号和槽时,不需要考虑考虑访问控制,因为是基于字符串连接。
  • 信号和槽的连接是动态的,可是用disconnect()函数断开连接。
  • 信号可以连接信号,但是槽不能连接槽。
  • 跨线程的信号和槽连接,实际上是使用QApplication::postEvent()完成槽的调用,因此槽函数一般发生在槽所在对象的线程中。
  • QObject::sender()可以获取发送信号的对象。

4. 跨线程的信号和槽

当我们需要做一些耗时的事情时,可以使用线程处理这类的事情,然后使用信号的方式通知主进程当前运行的进度等信息。 下面是一个关于跨线程的信号和槽的例子:

头文件,ThreadSigAndSlotsWidget.h

#ifndef THREAD_SIGANDSLOT_H
#define THREAD_SIGANDSLOT_H

#include <QProgressBar>
#include <QWidget>
#include <QPushButton>
#include <QThread>
#include "UIBase/UIBaseWindow.h"

class SignalThread;
class ThreadSigAndSlotsWidget : public UIBaseWindow
{
    Q_OBJECT

public:
    ThreadSigAndSlotsWidget(QWidget *parent = nullptr);
    ~ThreadSigAndSlotsWidget();

private:
    QProgressBar *m_ProgressBar = nullptr;
    QPushButton *m_StartButton = nullptr;

    SignalThread *m_Thread = nullptr;

private slots:
    void recvThreadValue(int);
    void onClickedStarButton(void);
};

// --------------------- Signal Thread -------------------------
class SignalThread : public QThread
{
    Q_OBJECT

public:
    SignalThread(QObject *parnet = nullptr);
    ~SignalThread();

protected:
    virtual void run(void) override;

signals:
    void valueChanged(int);
};

#endif

源程序,ThreadSigAndSlotsWidget.cpp

#include "ThreadSigAndSlots.h"
#include "UIBase/UIGlobalTool.h"
#include <QVBoxLayout>
#include <QDebug>

ThreadSigAndSlotsWidget::ThreadSigAndSlotsWidget(QWidget *parent)
    :UIBaseWindow(parent)
{
    m_ProgressBar = new QProgressBar;
    m_StartButton = new QPushButton("Start");
    // 添加阴影
    g_GlobalTool->addShadowEffect(m_StartButton);
    m_ProgressBar->setValue(0);
    m_ProgressBar->setRange(0, 100);

    QVBoxLayout* mainLayout = new QVBoxLayout(this);

    mainLayout->addSpacerItem(new QSpacerItem(10, 30));
    mainLayout->addWidget(m_ProgressBar, 0, Qt::AlignTop | Qt::AlignVCenter);
    mainLayout->addWidget(m_StartButton, 0, Qt::AlignTop | Qt::AlignVCenter);

    m_Thread = new SignalThread(this);

    // 链接信号和槽
    QObject::connect(m_Thread, &SignalThread::valueChanged, \
                     this, &ThreadSigAndSlotsWidget::recvThreadValue);
    QObject::connect(m_StartButton, &QPushButton::clicked, \
                     this, &ThreadSigAndSlotsWidget::onClickedStarButton);

    m_Thread->start();
}

ThreadSigAndSlotsWidget::~ThreadSigAndSlotsWidget()
{

}

void ThreadSigAndSlotsWidget::recvThreadValue(int value)
{
    m_ProgressBar->setValue(value);
    qDebug() << "GUI Thread ID: " << QThread::currentThreadId();
}

void ThreadSigAndSlotsWidget::onClickedStarButton(void)
{
    if (!m_Thread->isRunning())
        m_Thread->start();
}

// --------------------- Signal Thread -------------------------
SignalThread::SignalThread(QObject *parnet)
    :QThread(parnet)
{

}

SignalThread::~SignalThread()
{

}

void SignalThread::run(void)
{
    while (1)
    {
        qDebug() << "Thread ID: " << QThread::currentThreadId();
        for (int i=0; i<=100; ++i)
        {
            emit valueChanged(i);
            QThread::msleep(100);
        }

        return;
    }
}

程序的运行效果如下图所示:

点击程序启动时启动线程,Start按钮后也可以开启线程,线程中执行一些耗时操作后发送信息个主进程并显示当前的完成进度。

控制台的打印信息如下:
Thread ID: 0x1f44
GUI Thread ID: 0x38d8
可见,发送信号所在线程和槽函数所在线程是不同的。

不会飞的纸飞机
扫一扫二维码,了解我的更多动态。

下一篇文章:Qt中的事件(1)