QT5创建线程的两种方法(一)----继承Qthread

QT5创建线程

前言

QT5创建线程有两种方法,一种是qt4.6之前的方法,即创建一个自己的线程类继承QThread类。另一种是qt5后官方推荐的方法,即创建一个Object继承Qobject类,将自己要在线程里实现的方法和对象,在该类中定义。然后在主线程里实例化一个QThread对象,利用Qobject类的moveToThread方法,将自己创建的Object类都移到该线程里,这样Object类里的槽函数都是在新的线程里运行的了。
首先讲第一种方法,这边通过新建一个串口接收的例子来详细讲解。
示例程序是采用Qt Creator 4.11.1编辑和编译的。

UI设计

QT窗口应用程序中的主线程都是UI线程,也就是main函数这个函数入口的执行线程,所有的UI对象和方法也只能在主线程里调用。

新建Qt Widgets Application项目

在这里插入图片描述
Qt Widgets Application的模板已经为我们写好了初始的ui界面和mainwidows类。我们在这个基础上编辑UI界面和编写程序。

编辑UI界面

在这里插入图片描述
串口通讯需要设置:端口号波特率数据位奇偶校验位停止位流控制等参数,利用QTUI设计界面的Combo BOX组件(复选框)来选择串口的设置,利用push button组件来控制线程的开启和关闭,利用Text Browser组件来显示接收的串口数据。
为了编程方便需要在Ui设计界面对所有的组件进行重新命名。
在这里插入图片描述

线程类编写

源码

不贴源码了,传送门

代码重点详解

继承QThread新建线程类

在h文件中,我们定义一个线程类继承QThread,准备重载QThread类的虚函数run()

#include <QThread>
#include <QSerialPort>
class SerialsThread:public QThread
{
    Q_OBJECT
public:
    SerialsThread();
    ~SerialsThread();
protected:
    void run() override;
private:
    QSerialPort *myQSerialPort=nullptr;
signals:
    void portStatus(bool);
    void datareceived();
private slots:
    void receivedata();

};

在cpp文件中,我们重载run()run()函数里的任务就是工作在新的线程里的。可打印现在的线程号,与UI线程号对比,我们会发现它们工作在不同的线程。run()函数,当在主线程里调用start()函数,便会调用,在没有事件循环或者while循环的情况下,当代码执行完,线程便会结束。不建议用terminate()函数强行结束线程。

void SerialsThread::run()
{
    qDebug()<<"SerialsThread is running"<<QThread::currentThread();
    //do the job
}
pro文件

在写串口线程类前,我们要在pro文件里用上serialport模块,在pro文件里加上一行代码。这样我们就可以使用< QSerialPort >这个串口类。

QT       += serialport
Q_OBJECT宏定义

我们知道,QT最有特色的机制就是,信号和槽的机制,这个宏定义就设置了相关的东西。有了这个宏定义,我们才能使用信号和槽。

对串口的设置的方法
    myQSerialPort->setParity(QSerialPort::NoParity);//设置奇偶校验位为0
    myQSerialPort->setDataBits(QSerialPort::Data8);//设置数据位为8bit
    myQSerialPort->setFlowControl(QSerialPort::NoFlowControl);//设置流控制为OFF
    myQSerialPort->setStopBits(QSerialPort::OneStop);//设置停止位为1
    myQSerialPort->setBaudRate(QSerialPort::Baud9600);//设置波特率9600
     myQSerialPort->setPortName(myuserview.PortName);//设置端口号myuserview.PortName,根据复选框的选择
打开串口
if(!myQSerialPort->open(QIODevice::ReadWrite))//用ReadWrite 的模式尝试打开串口
    {
        qDebug()<<myuserview.PortName<<"打开失败";
        return;
    }
接收串口数据

首先在重写的run函数中,链接readyRead信号和接收数据的槽函数,readyRead信号是当有新的的数据可以被读取时,就会发射这个信号,QT的帮助文档中是这样描述的。
tips:F1 进入光标上函数或对象的帮助文档。

This signal is emitted once every time new data is available for reading from the device’s current read channel

connect(myQSerialPort,&QSerialPort::readyRead,this,&SerialsThread::receivedata);

链接好信号和槽函数后,需要开启事件循环。这个很重要,否则会什么东西都接收不到。

exec();//开启事件循环

在槽函数中,调用readAll方法。并发射 void datareceived() 信号,这个信号链接主线程中的槽函数,可用来跟新text browser的数据。

void SerialsThread::receivedata()
{
    QByteArray array=myQSerialPort->readAll();
    if(!array.isEmpty())
    {
        serialsreceivebuffer=serialsreceivebuffer.append(array);
        emit datareceived();
    }
}
主线程和线程间的通讯
根据串口打开状态,设置复选框的状态

如果想实现,串口接收线程中,如果打开串口成功,则在主线程中的复选框变为不可选的状态这个功能。就要利用信号和槽的机制,在串口线程中发射信号,并且在主线程的槽函数中响应,并设置复选框的状态。

主线程中,我们链接串口线程对象中的 void portStatus(bool) 信号和Mainwindow对象中的槽函数。

 connect(myserialsthread,&SerialsThread::portStatus,this,&MainWindow::serialsportStatusUpdate);

并根据信号传递的bool量参数,设置复选框的状态。

void MainWindow::serialsportStatusUpdate(bool status)
{
    ui->COMCB->setEnabled(!status);
    ui->BaudCB->setEnabled(!status);
}

串口线程,我们打开串口的代码的基础上,加上发射信号void portStatus(bool)

if(!myQSerialPort->open(QIODevice::ReadWrite))//用ReadWrite 的模式尝试打开串口
    {
        qDebug()<<myuserview.PortName<<"打开失败";
        emit portStatus(false);
        return;
    }
    else
    {
        emit portStatus(true);
    }
根据串口接收的数据,在text browser上显示

首先链接myserialsthreaddatareceived信号和Mainwindow的更新text browser的槽函数。

connect(myserialsthread,&SerialsThread::datareceived,this,&MainWindow::seriasreceiveTBupdate);

更新text browser的槽函数显示serialsreceivebuffer接收buffer,当buffer超过一定行数时,要清空text browser,当数据很多的时候,如果不清空的话,程序会异常。

void MainWindow::seriasreceiveTBupdate()
{
    //ui->ReceiveTB->insertPlainText(QString(serialsreceivedata));
    ui->ReceiveTB->setText(QString(serialsreceivebuffer));
    if(ui->ReceiveTB->document()->lineCount()>20)//当textbrowser行数超过20行后,清空textbrowser
    {
        ui->ReceiveTB->clear();
        serialsreceivebuffer.clear();
    }
}

常见的问题

跨线程对象创建对象

利用继承QThread类方法来新建线程很容易出现跨线程对象创建对象的问题。例如,如果想实现在开启事件循环前,对打开的串口发送个''connect success''的字符串,我们这样写。

SerialsThread::SerialsThread()
{
    myQSerialPort=new QSerialPort();
}
void SerialsThread::run()
{
//这边做设置串口,和打开串口的操作,省略不写
	connect(myQSerialPort,&QSerialPort::readyRead,this,&SerialsThread::receivedata);
    myQSerialPort->write("connected");
    exec();
}

这时就会报如下警告,意思是,不能在子线程中为主线程的父对象创建子对象。

QObject: Cannot create children for a parent that is in a different thread.
(Parent is QSerialPort(0x2dd4c40), parent’s thread is QThread(0x1c9650), current thread is SerialsThread(0x2dd4c00)

出现这个问题的原因是,在构造函数中实例化了myQSerialPort对象,因为SerialsThread这个类我们是在主线程的MainWindow构造函数里实例化的。QThread的特性是,除了run函数里运行的代码是属于子线程的,其他的都属于创建它的线程,也就是主线程。所以myQSerialPort是属于主线程的,我们调用write时,会为myQSerialPort这个对象创建子对象,这时就会报出这个警告。
如果不调用write,调用上文讲到的串口设置和打开方法,或者readAll方法,是不会出现这个问题的,因为这些方法没有创建子对象。

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    //user
    myserialsthread = new SerialsThread();
}

其实要更改也很简单,一种是不要在子线程里调用write,第二种是将实例化的操作放进run里。

SerialsThread::SerialsThread()
{
    //myQSerialPort=new QSerialPort();
}
void SerialsThread::run()
{
	myQSerialPort=new QSerialPort();
//这边做设置串口,和打开串口的操作,省略不写
	connect(myQSerialPort,&QSerialPort::readyRead,this,&SerialsThread::receivedata);
    myQSerialPort->write("connected");
    exec();
}

这里的exec()事件循环很重要,如果不使用的话,会出现,myQSerialPort->write("connected")返回7,表示已经写入7个字节,但接收的串口却没有任何东西收到。根据官方文档,这是因为,QIODevice的通讯是异步的,调用write时,只是将字节成功写入了缓存区,但没有实际写入设备,只有到事件循环返回时,才会写入设备。这个问题,这个博客讲的很好,传送们

Certain subclasses of QIODevice, such as QTcpSocket and QProcess, are asynchronous. This means that I/O functions such as write() or read() always return immediately, while communication with the device itself may happen when control goes back to the event loop.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值