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()的槽函数中(坑死我了)。有时间再更其他两种多线程方法。

猜你喜欢

转载自www.cnblogs.com/yuanwebpage/p/12638001.html