QML下多线程实现方法
(注:非常感谢博文https://www.cnblogs.com/judes/p/11249300.html给我带来的启发,因为在QML下的多线程解决方案太少了,而且很多都只有方案,没有能实现的代码,这也是我写作这篇文章的原因。本文部分参考以上博文)
在编写QML应用时,时常会遇到这样的问题:后台需要不断读取数据(如网络数据或串口数据),一旦收到数据就显示到QML界面上。遇到这种问题最基本的思想就是多线程,然而QML应用下编写多线程存在很多问题。本文就实现QML下多线程谈谈自己的理解。
理论上讲,实现QML多线程有三种方案。一是自定义继承自QThread的类实现多线程,二是采用moveTothread,三是QML定义的类WorkerScript可以实现多线程。实际上前两种方案就是通用的QT下多线程实现方式,第三种是QML专属多线程实现方式。但是因为跟QML的交互问题,前两种实现方案存在很多实际问题。可能的话,我会把以上三种方案都实现。本文主要讲解第二种实现方式,以TCP通信为例,UI界面响应用户点击,同时开启线程接收TCP消息,并实时显示到UI界面。
1. 理论分析(建议直接看代码实现再回头看这一部分)
1.1 何时调用moveToThread
先来看看通常QT应用是怎么通过moveTothread实现多线程的。通常是自定义一个类如myTcp(该类必须继承自QObject),然后在mainwindow.cpp中声明线程QThread newThread;之后调用myTcp.moveToThread(&newThread),该对象就被转移到newThread中,所有关于该对象的事件处理都在newThread中执行(这一部分网上有很多参考,不详述)。
所以在QML中用moveTothread实现多线程,关键还是一个问题:怎么将自定义类移入一个线程中。很容易类比,QML中的main.cpp跟普通QT应用的mainwindow.cpp作用类似,可以在main.cpp中定义一个QThread newThread,并实例化一个myTcp对象移入到newThread,再将实例myTcp注册到QML中然后在QML中访问。这种常规思路存在的问题见开篇提到的博文。
所以最好的思路是在QML文件中实例化自定义类时将其移入到线程中(即在构造函数中完成这一部分)。这里至少需要自定义两个类,因为假设myTcp类是实现功能的类(即接收TCP数据),那么必然要定义一个线程QThread newThread,并实例化一个myTcp类,然后调用myTcp.moveToThread(&newThread),在一个类的定义中不可能同时实例化该类,这就需要另一个类myTcpMoveToThread定义一个线程newThread和实例化一个myTcp,并调用myTcp.moveToThread(&newThread)。
1.2 QTcpSocket实例化注意事项
QT所有的IO类都不能在不同的线程中调用,否则会报错Socket notifiers cannot be enabled or disabled from another thread。所以对应到本文的实现TCP通信的类,其声明、建立连接和读写数据的操作都必须在newThread中实现。
2. 实现代码
下图是程序的组织结构:
在tcpmodel.cpp和tcpmodel.h中实现自定义类
1 #ifndef TCPMODEL_H 2 #define TCPMODEL_H 3 #include <QTcpSocket> 4 #include <QThread> 5 6 class TcpModel:public QObject //实现TCP功能的类 7 { 8 Q_OBJECT 9 public: 10 TcpModel(QObject* parent=nullptr); 11 12 13 signals: 14 void dataRecved(QString data); //通知TcpMoveToThread类,数据接收 15 16 public slots: 17 void tcpWork(); 18 void tcpClose(); 19 20 private: 21 QTcpSocket* m_socket; 22 QString msg; 23 }; 24 25 //将TcpModel在QML初始化时移入到子线程 26 class TcpMoveToThread: public QObject 27 { 28 Q_OBJECT 29 Q_PROPERTY(QString m_data MEMBER m_data) 30 public: 31 TcpMoveToThread(QObject* parent=nullptr); 32 ~TcpMoveToThread(); 33 34 signals: 35 void dataChanged(); //用于通知QML应用,数据接收到 36 37 38 public slots: 39 void dataChangedSlot(QString msg); 40 41 private: 42 QThread m_thread; //定义的线程 43 TcpModel m_tcp; //定义的TCP类,这样就能在TcpMoveToThread构造函数中将其移入新的线程 44 QString m_data; //保存接收数据 45 }; 46 47 #endif // TCPMODEL_H
接下来是tcpmodel.cpp
1 #include "tcpmodel.h" 2 #include <QObject> 3 4 TcpModel::TcpModel(QObject* parent) 5 { 6 } 7 8 void TcpModel::tcpWork() 9 { 10 m_socket=new QTcpSocket(); 11 m_socket->connectToHost("127.0.0.1",8000); 12 if(m_socket->waitForConnected(-1)) 13 { 14 while(1) 15 { 16 if(m_socket->waitForReadyRead()) 17 { 18 QByteArray res=m_socket->readAll(); 19 msg=QString::fromLocal8Bit(res.data()); 20 emit dataRecved(msg); //接收完成信号 21 } 22 } 23 } 24 } 25 26 void TcpModel::tcpClose() 27 { 28 m_socket->close(); 29 delete m_socket; 30 } 31 32 33 34 TcpMoveToThread::TcpMoveToThread(QObject* parent) 35 { 36 m_tcp.moveToThread(&m_thread); //加入到子线程 37 38 connect(&m_thread,&QThread::started,&m_tcp,&TcpModel::tcpWork); //一旦线程开始,就调用接收Tcp的函数 39 connect(&m_tcp,&TcpModel::dataRecved,this,&TcpMoveToThread::dataChangedSlot); 40 connect(&m_thread,&QThread::finished,&m_tcp,&TcpModel::tcpClose); //线程结束时关闭socket,删除申请内存 41 m_thread.start(); //开启子线程 42 } 43 44 TcpMoveToThread::~TcpMoveToThread() 45 { 46 m_thread.exit(); 47 m_thread.wait(); 48 } 49 50 void TcpMoveToThread::dataChangedSlot(QString msg) 51 { 52 m_data=msg; 53 emit dataChanged(); 54 }
先简单说明一下逻辑,TcpModel是实现TCP功能的类,TcpMoveToThread类负责与QML交互,将TcpModel类加入到新的线程中。整个代码的逻辑如下图:
所有核心功能都在TcpMoveToThread的构造函数中,完成将对象移入到新线程,开启新线程,信号和槽的绑定。之所以收到数据要通知TcpMoveToThread类,是因为只注册了TcpMoveToThread到QML中,QML能直接访问TcpMoveToThread类中的m_data,获取收到的消息。同时1小节中1.2提到的问题,可以看到是在子线程开启时才完成套接字声明和连接,所有关于套接字的操作都在子线程中。
然后是main.cpp
1 #include <QGuiApplication> 2 #include <QQmlApplicationEngine> 3 #include "tcpmodel.h" 4 5 6 int main(int argc, char *argv[]) 7 { 8 QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); 9 10 QGuiApplication app(argc, argv); 11 12 qmlRegisterType<TcpMoveToThread>("TcpMoveToThread",1,0,"TcpMoveToThread"); //注册QML类 13 14 QQmlApplicationEngine engine; 15 const QUrl url(QStringLiteral("qrc:/main.qml")); 16 QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, 17 &app, [url](QObject *obj, const QUrl &objUrl) { 18 if (!obj && url == objUrl) 19 QCoreApplication::exit(-1); 20 }, Qt::QueuedConnection); 21 engine.load(url); 22 23 return app.exec(); 24 }
最后是main.qml
1 import QtQuick 2.12 2 import QtQuick.Layouts 1.12 3 import QtQuick.Controls 2.12 4 import TcpMoveToThread 1.0 5 6 7 ApplicationWindow { 8 visible: true 9 height: 300 10 width: 400 11 Button{ 12 id: redbutton 13 anchors.left: parent.left 14 anchors.top: parent.top 15 text: "加载红色" 16 onClicked: { 17 recloader.sourceComponent=redRec; 18 } 19 } 20 Button{ 21 id: bluebutton 22 anchors.right: parent.right 23 anchors.top: parent.top 24 text: "加载蓝色" 25 onClicked: { 26 recloader.sourceComponent=blueRec; 27 } 28 } 29 30 Text { 31 id: message 32 text: qsTr("text") 33 font.pixelSize: 25 34 anchors.horizontalCenter: parent.horizontalCenter 35 anchors.top: recloader.bottom 36 } 37 38 39 Connections{ 40 target: tcp 41 onDataChanged:{ 42 message.text=tcp.m_data; //此处连接了TcpMoveToThread类的信号,一旦数据改变,就改变message的内容 43 } 44 } 45 46 TcpMoveToThread{ 47 id: tcp 48 49 } 50 51 52 Loader{ 53 id: recloader 54 anchors.centerIn: parent 55 height: 100 56 width: 100 57 } 58 Component{ 59 id: redRec 60 Rectangle{ 61 color: "red" 62 } 63 } 64 Component{ 65 id:blueRec 66 Rectangle{ 67 color: "blue" 68 } 69 } 70 71 72 }
在main.qml中,定义两个按钮,用来显示在接收TCP消息时,UI界面仍然可以响应,同时定义的Text能实时显示TCP收到的数据。TCP服务端就不再赘述。运行该程序,效果如下:
通过TCP调试助手发送什么数据就显示什么数据,同时点击两个按钮中间的正方形能改变颜色,说明多线程成功。
3.后记
在写本篇文章时遇到的最大的坑是最初在编写TCP接收函数tcpWork()时,写成如下形式:
void TcpModel::tcpWork() { m_socket=new QTcpSocket(); m_socket->connectToHost("127.0.0.1",8000); while(m_socket!=nullptr) { QByteArray res=m_socket->readAll(); msg=QString::fromLocal8Bit(res.data()); emit dataRecved(msg); //接收完成信号 } }
这样得到的效果就是一旦关闭服务器,UI界面就卡死了。原因是QT的TCP函数connectToHost()和readAll()都是非阻塞的,所以导致dataRecved()信号一直触发,那么QML就会一直卡在onDataChanged()的槽函数中(坑死我了)。有时间再更其他两种多线程方法。