QML下多线程实现方法

您所在的位置:网站首页 promose是多线程吗 QML下多线程实现方法

QML下多线程实现方法

2023-06-02 12:38| 来源: 网络整理| 查看: 265

在编写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中实现自定义类

#ifndef TCPMODEL_H #define TCPMODEL_H #include #include class TcpModel:public QObject //实现TCP功能的类 { Q_OBJECT public: TcpModel(QObject* parent=nullptr); signals: void dataRecved(QString data); //通知TcpMoveToThread类,数据接收 public slots: void tcpWork(); void tcpClose(); private: QTcpSocket* m_socket; QString msg; }; //将TcpModel在QML初始化时移入到子线程 class TcpMoveToThread: public QObject { Q_OBJECT Q_PROPERTY(QString m_data MEMBER m_data) public: TcpMoveToThread(QObject* parent=nullptr); ~TcpMoveToThread(); signals: void dataChanged(); //用于通知QML应用,数据接收到 public slots: void dataChangedSlot(QString msg); private: QThread m_thread; //定义的线程 TcpModel m_tcp; //定义的TCP类,这样就能在TcpMoveToThread构造函数中将其移入新的线程 QString m_data; //保存接收数据 }; #endif // TCPMODEL_H

接下来是tcpmodel.cpp

#include "tcpmodel.h" #include TcpModel::TcpModel(QObject* parent) { } void TcpModel::tcpWork() { m_socket=new QTcpSocket(); m_socket->connectToHost("127.0.0.1",8000); if(m_socket->waitForConnected(-1)) { while(1) { if(m_socket->waitForReadyRead()) { QByteArray res=m_socket->readAll(); msg=QString::fromLocal8Bit(res.data()); emit dataRecved(msg); //接收完成信号 } } } } void TcpModel::tcpClose() { m_socket->close(); delete m_socket; } TcpMoveToThread::TcpMoveToThread(QObject* parent) { m_tcp.moveToThread(&m_thread); //加入到子线程 connect(&m_thread,&QThread::started,&m_tcp,&TcpModel::tcpWork); //一旦线程开始,就调用接收Tcp的函数 connect(&m_tcp,&TcpModel::dataRecved,this,&TcpMoveToThread::dataChangedSlot); connect(&m_thread,&QThread::finished,&m_tcp,&TcpModel::tcpClose); //线程结束时关闭socket,删除申请内存 m_thread.start(); //开启子线程 } TcpMoveToThread::~TcpMoveToThread() { m_thread.exit(); m_thread.wait(); } void TcpMoveToThread::dataChangedSlot(QString msg) { m_data=msg; emit dataChanged(); }

先简单说明一下逻辑,TcpModel是实现TCP功能的类,TcpMoveToThread类负责与QML交互,将TcpModel类加入到新的线程中。整个代码的逻辑如下图:

  所有核心功能都在TcpMoveToThread的构造函数中,完成将对象移入到新线程,开启新线程,信号和槽的绑定。之所以收到数据要通知TcpMoveToThread类,是因为只注册了TcpMoveToThread到QML中,QML能直接访问TcpMoveToThread类中的m_data,获取收到的消息。同时1小节中1.2提到的问题,可以看到是在子线程开启时才完成套接字声明和连接,所有关于套接字的操作都在子线程中。

  然后是main.cpp

#include #include #include "tcpmodel.h" int main(int argc, char *argv[]) { QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QGuiApplication app(argc, argv); qmlRegisterType("TcpMoveToThread",1,0,"TcpMoveToThread"); //注册QML类 QQmlApplicationEngine engine; const QUrl url(QStringLiteral("qrc:/main.qml")); QObject::connect(&engine, &QQmlApplicationEngine::objectCreated, &app, [url](QObject *obj, const QUrl &objUrl) { if (!obj && url == objUrl) QCoreApplication::exit(-1); }, Qt::QueuedConnection); engine.load(url); return app.exec(); }

  最后是main.qml

import QtQuick 2.12 import QtQuick.Layouts 1.12 import QtQuick.Controls 2.12 import TcpMoveToThread 1.0 ApplicationWindow { visible: true height: 300 width: 400 Button{ id: redbutton anchors.left: parent.left anchors.top: parent.top text: "加载红色" onClicked: { recloader.sourceComponent=redRec; } } Button{ id: bluebutton anchors.right: parent.right anchors.top: parent.top text: "加载蓝色" onClicked: { recloader.sourceComponent=blueRec; } } Text { id: message text: qsTr("text") font.pixelSize: 25 anchors.horizontalCenter: parent.horizontalCenter anchors.top: recloader.bottom } Connections{ target: tcp onDataChanged:{ message.text=tcp.m_data; //此处连接了TcpMoveToThread类的信号,一旦数据改变,就改变message的内容 } } TcpMoveToThread{ id: tcp } Loader{ id: recloader anchors.centerIn: parent height: 100 width: 100 } Component{ id: redRec Rectangle{ color: "red" } } Component{ id:blueRec Rectangle{ color: "blue" } } }

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



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3