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
View Code

接下來是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 }
View Code

先簡單說明一下邏輯,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 }
View Code

  最后是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 }
View Code

  在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()的槽函數中(坑死我了)。有時間再更其他兩種多線程方法。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM