(注:非常感謝博文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()的槽函數中(坑死我了)。有時間再更其他兩種多線程方法。