QT5创建线程有两种方法,一种是qt4.6之前的方法,即创建一个自己的线程类继承QThread类。另一种是qt5后官方推荐的方法,即创建一个Object继承Qobject类,将自己要在线程里实现的方法和对象,在该类中定义。然后在主线程里实例化一个QThread对象,利用Qobject类的moveToThread方法,将自己创建的Object类都移到该线程里,这样Object类里的槽函数都是在新的线程里运行的了。
首先讲第一种方法,这边通过新建一个串口接收的例子来详细讲解。
示例程序是采用Qt Creator 4.11.1编辑和编译的。
QT窗口应用程序中的主线程都是UI线程,也就是main函数这个函数入口的执行线程,所有的UI对象和方法也只能在主线程里调用。
Qt Widgets Application的模板已经为我们写好了初始的ui界面和mainwidows类。我们在这个基础上编辑UI界面和编写程序。
串口通讯需要设置:端口号、波特率、数据位、奇偶校验位、停止位、流控制等参数,利用QTUI设计界面的Combo BOX组件(复选框)来选择串口的设置,利用push button组件来控制线程的开启和关闭,利用Text Browser组件来显示接收的串口数据。
为了编程方便需要在Ui设计界面对所有的组件进行重新命名。
不贴源码了,传送门。
在h文件中,我们定义一个线程类继承QThread,准备重载QThread类的虚函数run()
。
#include
#include
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文件里用上serialport
模块,在pro文件里加上一行代码。这样我们就可以使用< QSerialPort >
这个串口类。
QT += serialport
我们知道,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);
}
首先链接myserialsthread
的datareceived
信号和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.