Qt-多线程

时间:2024-10-21 09:17:46

1. Qt 多线程概述

  • Qt 默认只有一个主线程,该线程既要处理窗口移动,又要处理各种运算。

  • 当要处理的运算比较复杂时,窗口就无法移动,所以在处理程序时在很多情况下都需要使用多线程来完成。

示例:移动窗口和复杂循环  

#include "widget.h"
#include "ui_widget.h"
#include <QDebug>

Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget)
{
  ui->setupUi(this);
  
  connect(ui->startBtn, &QPushButton::clicked, [=](){
  	// 模拟复杂运算
    for (int i = 1; i <= 10000000; i++)
    {
      ui->lcdNumber->display(QString::number(i));
    }
  });
}

2. QThread

QThread 类用来创建子线程

2.1 用法-1

方式: 单独定义线程类,线程类完成全部的业务逻辑,主线程只负责启动子线程和退出子线程

使用步骤:

  1. 创建一个类(MyThread),并继承 QThread

  2. QThread 中有一个 protected 级别的 virtual void run(), 必须在该函数中实现线程业务逻辑

  3. 启动线程必须使用 start() 槽函数

  4. 在 MyThread 中可以定义线程执行完成信号(isDone), 在 run函数结束前发射isDone信号,来处理线程执行完毕的逻辑

示例:

1.创建一个类,并继承 QThread

class MyThread : public QThread
{
    Q_OBJECT
public:
    explicit MyThread(QObject *parent = nullptr);

protected:
    // 线程处理函数,不能直接调用,需要使用start来启动线程
    void run();

signals:
    // 自定义信号,当线程执行完毕时可以发射该信号,告诉系统线程已执行完毕
    void isDone();

public slots:
};

 2.在 run 函数中实现业务逻辑

void MyThread::run()
{
  // 模拟复杂业务逻辑
  for (int i = 0; i < 10000000; i++)
  {
    qDebug() << i;
  }
  
  // 线程结束时发送 isDone 信号
  emit this->isDone();
}

 3.在主窗口实例化 MyThread 类,并调用 start方法,来启动线程

4.链接myThread 对象 和 isFinished 信号

Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget)
{
  ui->setupUi(this);

  myThread = new MyThread(this);
  connect(ui->startBtn, &QPushButton::clicked, [=](){
    // 通过 start 来启动线程,不能直接调用 run
    myThread->start();
  });

  // 当子线程结束时发送 isDone 信号,连接信号和槽 可以处理子线程结束之后的事情
  connect(myThread, &MyThread::isDone, [=](){
    qDebug() << "线程结束";
    myThread->quit();
  });

}

2.2 用法-2

方式:主窗口与线程类配合完成业务逻辑。

  • 子类提供线程能力,

  • 主窗口利用子类提供的线程能力进行业务处理。 同时还需要开启和关闭子线程

示例: 计时器(线程版)

实现思路: 子类每秒向主窗口发送一个信号,主窗口接收到信号后进行累加操作,并将累加结果显示到窗口中

// mythread.h

#ifndef MYTHREAD_H
#define MYTHREAD_H

#include <QObject>

class MyThread : public QObject
{
    Q_OBJECT
public:
    explicit MyThread(QObject *parent = nullptr);

void sendMsg();

signals:
    void mySignal();
public slots:
};

#endif // MYTHREAD_H

// mythread.c

#include "mythread.h"
#include <QThread>


//核心功能: 没个一段时间主动向主窗口发送一个信号


MyThread::MyThread(QObject *parent) : QObject(parent)
{

}
void MyThread::sendMsg()
{
    while (true) {
        QThread::sleep(1);
        emit this->mySignal();
    }
}


//widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include "mythread.h"
namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = nullptr);
    ~Widget();

signals:
    void startSibThread();

private:
    Ui::Widget *ui;

    MyThread *myThread;

    int num;
};

#endif // WIDGET_H



//widget.c

#include "widget.h"
#include "ui_widget.h"
#include <QDebug>
#include <QThread>
Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);

    this->myThread = new MyThread;
     QThread *thread  =  new QThread(this);
     this->myThread->moveToThread(thread);


     connect(this->myThread,&MyThread::mySignal, [this](){
        this->num++;
         ui->lcdNumber->display(this->num);
    });

     connect(this, &Widget::startSibThread,this->myThread, &MyThread::sendMsg);

    connect(ui->pushButton,&QPushButton::clicked,[=](){

        thread->start();
        emit this->startSibThread();
    });

}

Widget::~Widget()
{
    delete ui;
}

实现步骤:

1.创建子类(MyThread),继承于 QObject。

==在该类中设置一个能够每秒钟发送一次信号的函数, 功能就是提供线程能力==

2.在主窗口中引入MyThread,并将其对象放在线程(QTread)中进行调用 ( 注意:此步并没有启动线程)

3.主窗口中接收信号,在匹配的槽函数中进行业务处理

4.点击按钮时启动线程 ( 使用该方法启动线程时,必须使用信号和槽的方式 )

5.关闭定时器: ① 结束线程 ② 结束 while 循环

        1.创建子类(MyThread),继承于 QObject (在该类中设置一个能够每秒钟发送一次信号的函数)

class MyThread : public QObject
{
    Q_OBJECT
public:
    explicit MyThread(QObject *parent = nullptr);

    // 线程处理函数
    void dealThread();

signals:
    // 提供给主窗口的信号
    void mySignal();

public slots:
};
void MyThread::dealThread()
{
    while (1)
    {
        QThread::sleep(1);
        emit this->mySignal();
        qDebug() << "子线程号:" << QThread::currentThread();
    }
}

2.在主窗口中引入MyThread,并将其对象放在线程中进行调用

3.主窗口中接收信号,在匹配的槽函数中进行业务处理

Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget)
{
  ui->setupUi(this);

  // 实例化自定义的线程,但是不能指定父对象
  myThread = new MyThread;

  // 创建子线程
  qThread = new QThread(this);

  // 将自定义的线程加入到子线程中, 就是将自定义线程移动到系统提供的线程对象中
  // 如果实例化 MyThread 时指定了父对象就不能移动了
  myThread->moveToThread(qThread);

  // 处理线程发射的信号
  connect(myThread, &MyThread::mySignal, [=](){
    static int i = 0;
    i++;
    ui->lcdNumber->display(QString::number(i));
  });
}

4.点击按钮时启动程序

该方式启动线程时需要调用 start() 方法

同时发送自定义信号 startSubThread

void Widget::on_startBtn_clicked()
{
    // 启动线程
    qThread->start();

    emit this->startSubThread();
}
Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget)
{
    ui->setupUi(this);

    ......
    myThread->moveToThread(qThread);

    // 处理线程发射的信号
    connect(myThread, &MyThread::mySignal, [=](){
        static int i = 0;
        i++;
        ui->lcdNumber->display(QString::number(i));
    });

    qDebug() << "主线程号:" << QThread::currentThread();

    // 在 startSubThread 信号中调用子类提供的信号
    connect(this, &Widget::startSubThread, myThread, &MyThread::dealThread);

}

5.关闭定时器:① 结束线程 ② 结束 while 循环

在 MyThread 中设置 isStop 属性,用来控制线程和循环的开启或者关闭

class MyThread : public QObject
{
    Q_OBJECT
public:
    explicit MyThread(QObject *parent = nullptr);

    // 线程处理函数
    void dealThread();

    // 修改isStop状态
    void setIsStop(bool flag);

signals:
    // 提供给主窗口的信号
    void mySignal();

public slots:

private:
  	// 是否关闭线程的标志   true 为关闭, false 为不关闭
    bool isStop = false;
};
void MyThread::setIsStop(bool flag)
{
    this->isStop = flag;
}
void Widget::on_stopBtn_clicked()
{
    myThread->setIsStop(true);
    qThread->quit();
    qThread->wait();
}

用法小结:

  1. 创建 MyThread 类(继承QObject即可)

① 提供一个信号(void mySignal())

② 定义了一个方法(void dealThread())

核心功能:使用死循环,每隔一段时间之后就发射一个信号 (角色:监工)

  1. 在窗口类中(widget)

① 实例化 MyThread 得到 myThread对象

② 监听 mySignal 信号,一旦监听到信号之后就去实现业务的主体功能(i++,lcd->display(i))

③ 实例化 QThread 得到 qThread对象,再将 myThread转移到qThread中,这个时候就有了线程的功能

④ 在开始按钮上绑定信号和槽,在槽函数中调用 qThread->start() 来启动线程

⑤ 调用 MyThread::dealThread 方法,必须要在 Widget 类中使用自定义信号(startSubThread)的方式,并在其处理信号的槽函数位置调用 MyThread::dealThread 方法

2.3 轮播图

绘图基础回顾:

  1. 绘制图片要在 void paintEvent(QPaintEvent *event) 事件中

  2. 使用到的类 QPainter 、 QPixmap

  3. 窗口中的图片需要进行重绘时,需要使用 QWidget 的 update 方法

第一步: 通过点击信号切换图片

核心思路: 将图片资源地址保存在一个 QVector 中,通过索引号来访问; 切换到下一张图片就是 索引号自增1 ,再调用 update() 方法重绘图片

① 使用数组保存图片资源地址

② 使用索引来控制显示哪张图片

③ 在 paintEvent 事件中绘制图片

④ 点击下一张按钮时对索引号进行自增1,再重绘图片

class Widget : public QWidget
{
  Q_OBJECT

public:
  explicit Widget(QWidget *parent = nullptr);
  ~Widget();
  // 绘图事件
  void paintEvent(QPaintEvent *);

private:
  Ui::Widget *ui;
	// imgPaths 用来保存图片资源路径
  QVector<QString> imgPaths;
  // index 用来控制显示 imPaths 中的那张图片
  int index = 0;
};
Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget)
{
  ui->setupUi(this);

  this->resize(600, 600);

  // 将数组保存在一个 QVector 中
  imgPaths = {
    ":/images/1.jpg", ":/images/2.jpg", ":/images/3.jpg", ":/images/4.jpg"
  };

  // 点击下一张按钮时对索引号自增1, 注意进行边界检测
  connect(ui->nextBtn, &QPushButton::clicked, [=](){
    this->index = ++this->index == 4 ? 0 : this->index;
    // 重绘图片
    this->update();
  });
}

void Widget::paintEvent(QPaintEvent *)
{
  // 根据索引号绘制图片
  QPainter painter(this);
  QPixmap pix;
  pix.load(this->imgPaths[this->index]);
  painter.drawPixmap(QRect(0, 0, 200, 200), pix);
}

第二步: 使用线程替换 next 按钮

核心思路:创建 MyThread 类,每三秒向主窗口发送一个信号,主窗口每次接收到该信号时自动对索引号自增1,接着在重绘图片

  1. 创建 MyThread 类,每3秒向窗口类

  2. 主窗口监听到MyThread发送的信号后,对索引号进行自增1,接着在重绘图片

  3. 程序启动时,开启子线程

1.创建MyThread 类,每3秒向窗口类

class MyThread : public QObject
{
    Q_OBJECT
public:
  explicit MyThread(QObject *parent = nullptr);

  // 定义线程函数
  void dealThread();

signals:
  // 自定义信号,每三秒向主窗口发送一次信号
  void mySignal();

};
void MyThread::dealThread()
{
  // 每 3 秒向窗口发送一次信号
  while (1)
  {
    QThread::sleep(3);
    emit this->mySignal();
  }
}

2.主窗口监听到MyThread发送的信号后,对索引号进行自增1,接着在重绘图片

class Widget : public QWidget
{
    Q_OBJECT

public:
    ...

signals:
    // 启动子线程的信号
    void startSubThread();

private:
    ...

    MyThread *myThread;
    QThread *qThread;
};
// 实例化自定义类 和 线程类,并将自定义类添加到线程中
myThread = new MyThread;
qThread = new QThread(this);
myThread->moveToThread(qThread);
// 通过线程调整索引号
connect(myThread, &MyThread::mySignal, [=](){
  this->index = ++this->index == 4 ? 0 : this->index;
  this->update();
});

3.程序启动时,开启子线程

// 启动子线程就直接调用 dealThread 槽函数
connect(this, &Widget::startSubThread, myThread, &MyThread::dealThread);

// 启动子线程
qThread->start();
emit this->startSubThread();

2.4 轮播图实现代码(全)

//mythread.h

#ifndef MYTHREAD_H
#define MYTHREAD_H

#include <QObject>

class MyThread : public QObject
{
    Q_OBJECT
public:
    explicit MyThread(QObject *parent = nullptr);
    void sendMsg();

    void setIsRun(bool flag);
signals:
    void mySignal();
public slots:

private:
    //true 循环发射信号,false 停止循环发送信号
    bool isRun = true;
};

#endif // MYTHREAD_H

//mythread.c

#include "mythread.h"
#include <QThread>

MyThread::MyThread(QObject *parent) : QObject(parent)
{

}
 void MyThread:: sendMsg()
 {
     while(this->isRun)
     {
         QThread::sleep(3);
         emit this->mySignal();
     }
 }
 void MyThread::setIsRun(bool flag)
{
  this->isRun = flag;
 }


//widget.h

#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>
#include <QVector>
#include "mythread.h"
#include <QThread>

namespace Ui {
class Widget;
}

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = nullptr);
    ~Widget();

    void enterEvent(QEvent *);
    void leaveEvent(QEvent *);

private slots:
    void on_pushButton_2_clicked();

    void on_pushButton_clicked();
signals:
    void startSubThread();
private:
    Ui::Widget *ui;

    MyThread *myThread;
    QThread *qThread;


    QVector<QString>imgList;
    int index = 0;
};

#endif // WIDGET_H


//widget.c

#include "widget.h"
#include "ui_widget.h"

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);

    this->imgList = {":/images/ashe.png",":/images/jax.png",":/images/vn.png"};

    ui->label->setPixmap(QPixmap(this->imgList[this->index]));
    ui->label->setScaledContents(true);

    //1.准备线程
    this->myThread = new MyThread;
    this->qThread = new QThread;
    this->myThread->moveToThread(this->qThread);

    connect(this,&Widget::startSubThread,this->myThread,&MyThread::sendMsg);
    //3.处理监工提供信号
    connect(this->myThread,&MyThread::mySignal,[this](){
        this->index++;
        if (this->index == this->imgList.size())
        {
            this->index = 0;
        }
        ui->label->setPixmap(QPixmap(this->imgList[this->index]));
        });

    //2. 启动子线程
    this->qThread->start();
    emit this->startSubThread();


}

Widget::~Widget()
{
    delete ui;
}

void Widget::on_pushButton_2_clicked()
{
    this->index++;
    if (this->index == this->imgList.size())
    {
        this->index = 0;
    }
    ui->label->setPixmap(QPixmap(this->imgList[this->index]));
}

void Widget::on_pushButton_clicked()
{
    this->index--;
    if (this->index == -1)
    {
        this->index = this->imgList.size() - 1;
    }
    ui->label->setPixmap(QPixmap(this->imgList[this->index]));
}

//鼠标进入停止线程
void Widget::enterEvent(QEvent *)
{
    this->qThread->quit();
    this->myThread->setIsRun(false);

}

//鼠标离开启动线程
void Widget::leaveEvent(QEvent *)
{
    this->myThread->setIsRun(true);
    this->qThread->start();
    emit this->startSubThread();
}