QML使用moveToThread线程【QML工程使用C++】
阅读原文时间:2023年07月10日阅读:1

一、需求来源

对于使用Qt线程,有两种方式,见本人其他文章:https://www.cnblogs.com/judes/p/6884964.html

个人认为QObject::moveToThread方式是最好的,无需死循环判断某个条件是否成立,如此非常消耗CPU【用C++11条件变量可解决】

所以翻遍整个网络也想要找到QML+moveToThread操作线程的方式。

我理想中的工作模式是:

所有工作类【如网络、串口等】继承于QObject,然后moveTothread到某个QThread对象,QML里通过信号与槽的方式控制工作类的开关及数据收发。

无奈,只有此博主提及了一下:https://blog.csdn.net/LHRui_1/article/details/83861142,我最开始的做法与此博主开始一致:

在main.cpp里moveTothread,然后注册这个对象到上下文,再在qml直接访问对象内部。

这样是行不通的,会提示错误:不能访问非当前线程对象..【大致意思是这样】

因为:main.cpp里,工作对象是子线程的【已经moveTothread了】,而engine是主线程的,使用主线程engine注册子线程工作对象到上下文,然后在QML里调用,肯定会出错,具体原因也不是很明白,实在没有相关资料了。

二、解决

还好,放弃baidu搜索,换了bing国际版搜索,看到一篇文章:https://forum.qt.io/topic/62073/qthread-qml

博士非常谦虚,没有讽刺QThread子类方法【因为官方也认可这种方法,自然有人使用。而本人非常相信这是不够“效率”的做法,与使用此方法的朋友争论许久,最后放弃。或许代码界没有答案】,

只声明这是ta自己的opinions。在看了文章之后发现,这就是我想要的效果,并且我相信是最高效正确的做法。

1、工作类work.h

#ifndef WORKER_H
#define WORKER_H

#include
#include
#include

class Worker: public QObject {
Q_OBJECT
public:
Worker(QString &data) : mData(data) {}

public slots:
void process() {
qDebug() << "Process's Thread : " << QThread::currentThreadId();
mData += "process\n";

    emit processFinished();  
}

signals:
void processFinished();

private:
QString &mData;
};

class WorkerInterface : public QObject {
Q_OBJECT
Q_PROPERTY(QString data READ getData NOTIFY dataChanged)
public:
WorkerInterface() : mWorker(mData) {
mWorker.moveToThread(&mThread);
connect(this, &WorkerInterface::process, &mWorker, &Worker::process);
connect(&mWorker, &Worker::processFinished, [this]{
qDebug() << "ProcessFinished in : " << QThread::currentThreadId();
emit dataChanged();
});

    mThread.start();  
}

QString getData() const {  
    return mData;  
}

~WorkerInterface() {  
    mThread.exit();  
    mThread.wait();  
}

signals:
void dataChanged();
void process();

private:
QThread mThread;
QString mData;
Worker mWorker;
};

#endif // WORKER_H

2、main.cpp

#include
#include
#include
#include
#include "worker.h"

int main(int argc, char *argv[])
{
QApplication app(argc, argv);
qmlRegisterType("Workers", , , "Worker");

QQmlApplicationEngine engine;

engine.load(QUrl(QStringLiteral("qrc:/main.qml")));

qDebug() << "Main thread : " << QThread::currentThreadId();

return app.exec();  

}

3、main.qml

import QtQuick 2.5
import QtQuick.Controls 1.4
import Workers 1.0

ApplicationWindow {
visible: true
width:
height:
title: qsTr("Hello World")

SystemPalette {  
    id: pal  
    colorGroup: SystemPalette.Active  
}

Worker {  
    id: worker;  
}

Button {  
    id: button  
    text: qsTr("Process")  
    anchors.centerIn: parent

    onClicked: worker.process();  
}

Text {  
    text: worker.data  
    anchors.horizontalCenter: parent.horizontalCenter  
    anchors.top: button.bottom  
    anchors.topMargin:  
}  

}

三、剖析

1、定义工作类Worker继承于QObject

2、定义连接QML和C++的中间接口类WorkerInterface继承于QObject

3、在中间类WorkerInterface中声明工作类的对象mWorker

4、在中间类WorkerInterface构造函数中连接信号与槽,mWorker::moveToThread,变量初始化等

5、注册中间类WorkerInterface

6、在QML中实例化中间类,并直接调用其信号控制工作类开启其槽函数

只有工作对象Worker才是被moveToThread的,它的槽函数全是在子线程中运行的。为了避免使用主线程的engin注册此对象【上面已分析出不可行】,所以定义了一个中间类:WorkerInterface,所有中间类是工作在main中的,手动调用其信号【如打开关闭】控制其工作对象对应的槽函数。

四、体会

1、Qt官方只是发布花里胡哨、功能强大的库,至于怎么去用非常灵活,官方有时候也只是建议,没有明确的答案。更多的需要自己去实践、源码剖析,得到适合自己的答案。

2、国内资源是真tm少,特别是国外新出的标准、技术,必要时,还是墙外的世界更精彩。

3、做自己认为对的事。

---------------------------------------------实践-----------------------------------------------------

一、Myudp.h

#ifndef MYUDP_H
#define MYUDP_H

#include
#include
#include
class Myudp : public QObject
{
Q_OBJECT
public:
explicit Myudp(QObject *parent = nullptr);
~Myudp();
signals:
void rcvdDataSignal(const QByteArray&);
void sendedSignal(const QString&);//发送成功
void getAString(QString str);
private slots:
void initSlot();
void requestSlot();
void sendSlot(const QByteArray&);
void closeSlot();
private:
QUdpSocket* udpClient = nullptr;
const QString localIp="127.0.0.1";
const quint16 localPort=8080;
const QString aimIp="127.0.0.1";
const quint16 aimPort=8888;
private:
};

class MyudpInterfase : public QObject
{
Q_OBJECT
Q_PROPERTY(QByteArray dataBa MEMBER dataBa)

public:
MyudpInterfase()
{
udp.moveToThread(&udpThread);
QObject::connect(&udp,SIGNAL(rcvdDataSignal(QByteArray)),this,SLOT(dataChangeSlot(QByteArray)));
QObject::connect(this,SIGNAL(initSignal()),&udp,SLOT(initSlot()));
QObject::connect(this,SIGNAL(sendSignal(QByteArray)),&udp,SLOT(sendSlot(QByteArray)));
QObject::connect(this,SIGNAL(closeSiganl()),&udp,SLOT(closeSlot()));
udpThread.start();
}
~MyudpInterfase()
{
udpThread.quit();
udpThread.wait();
}
private slots:
void dataChangeSlot(const QByteArray& ba)
{
dataBa = ba;
emit dataChangeSignal();
}
signals:
void initSignal();
void sendSignal(const QByteArray&);
void closeSiganl();
void dataChangeSignal();
private:
Myudp udp;
QThread udpThread;

QByteArray dataBa;  

};

#endif // MYUDP_H

1.1、Myudp类就是简单的C++中自定义类,应该最终被moveTothread

1.2、MyudpInterfase是用于QML访问的中间类,所有的信号连接可放在构造函数中,并且定义控制Myudp的信号;注意在构造函数中退出线程

1.3、Q_PROPERTY访问属性,如果没有访问器【READ、WRITE】,则需要加MEMBER来指定这个Q_PROPERTY对应的是控制哪个成员变量。

二、Myudp.cpp

#include "myudp.h"

Myudp::Myudp(QObject *parent) : QObject(parent)
{

}
Myudp::~Myudp()
{
if(udpClient != nullptr)
{
qDebug() << "内存回收"; delete udpClient; udpClient = nullptr; } } /***********************************************/ // z 函数名称:初始化 // h 函数作用:NULL // u 函数参数:NULL // x 函数返回值:NULL // y 备注:NULL /***********************************************/ void Myudp::initSlot() { if(udpClient == nullptr) { udpClient = new QUdpSocket(this); udpClient->bind(QHostAddress(localIp),localPort);
QObject::connect(udpClient,SIGNAL(readyRead()),this,SLOT(requestSlot()));
}
}

/***********************************************/
// z 函数名称:接收数据
// h 函数作用:NULL
// u 函数参数:NULL
// x 函数返回值:NULL
// y 备注:NULL
/***********************************************/
void Myudp::requestSlot()
{
if(udpClient->pendingDatagramSize() == )
{
return;
}
QByteArray ba;
ba.resize(udpClient->pendingDatagramSize());
QHostAddress tempHost("");
quint16 port = ;
udpClient->readDatagram(ba.data(),udpClient->pendingDatagramSize(),&tempHost,&port);

emit rcvdDataSignal(ba);  

}

/**
*函数名:发送槽函数
*函数参数:NULL
*函数作用:NULL
*函数返回值:NULL
*备注:NULL
*/
void Myudp::sendSlot(const QByteArray &info)
{
if(info.size()==udpClient->writeDatagram(info,QHostAddress(aimIp),aimPort))
{
QString str = info.toHex().toUpper();
emit sendedSignal(str);
}
}

/*****************************************************************/
//作者:朱小勇
//函数名称:关闭
//函数参数:NULL
//函数返回值:NULL
//函数作用:NULL
//备注:NULL
/*****************************************************************/
void Myudp::closeSlot()
{
udpClient->close();
}

三、main.cpp

#include
#include
#include
#include "myudp.h"
#include
#include
#include
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);

QGuiApplication app(argc, argv);

qmlRegisterType<MyudpInterfase>("Myudp.module",,,"Udp");

#if 1
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/qml/my.qml")));
if (engine.rootObjects().isEmpty())
return -;
#endif

return app.exec();  

}

四、my.qml

import QtQuick 2.9
import QtQuick.Window 2.2
import QtQuick.Controls 2.2
import QtQuick.Controls 1.4
import QtGraphicalEffects 1.0
import QtQuick.Layouts 1.3
import Myudp.module 1.0
ApplicationWindow{
id: root
visible: true
width: Screen.width
height: Screen.height
title: qsTr("test")
Component.onCompleted: {
root.visibility = Window.Maximized
}

Udp {  
    id: udp  
}

TabView {  
    anchors.fill: parent  
    Tab {  
        title: "UDP"  
        Rectangle {  
            GroupBox {  
                id: group1  
                title: "数据接收"  
                width: parent.width/  
                height: parent.height  
                flat: false  
                TextEdit{  
                    id: rcvTextEdit  
                    anchors.fill: parent  
                    anchors.margins: 5  
                }  
                Connections {  
                    target: udp  
                    onDataChangeSignal: {  
                        rcvTextEdit.append(udp.dataBa.toString().toUpperCase())  
                    }  
                }  
            }  
            GroupBox{  
                id: group2  
                title: "数据发送"  
                width: parent.width/  
                height: parent.height/  
                anchors.left: group1.right  
                flat: false  
                TextEdit{  
                    id: sendTextEdit  
                    anchors.fill: parent  
                    anchors.margins:  
                }  
            }  
            Rectangle {  
                width: parent.width/  
                height: parent.height/  
                anchors.top: group2.bottom  
                anchors.left: group1.right  
                Row {  
                    width: parent.width  
                    height:  
                    spacing:  
                    Button {  
                        text: qsTr("初始化")  
                        onClicked: {  
                            udp.initSignal()  
                        }  
                    }  
                    Button {  
                        text: qsTr("发送")  
                        onClicked: {  
                            udp.sendSignal(sendTextEdit.text)  
                        }  
                    }  
                    Button {  
                        text: qsTr("退出")  
                        onClicked: {  
                            udp.closeSiganl()  
                            Qt.quit()  
                        }  
                    }  
                }  
            }  
        }  
    }  
    Tab {  
        title: "Blue"  
        Rectangle { color: "blue" }  
    }  
    Tab {  
        title: "Green"  
        Rectangle { color: "green" }  
    }  
}  

}

4.1、注意上面的Connections

由于udp是全局的,而rcvTextEdit是多层控件树下的,所以Connections的位置不能随意写,刚开始我把Connections放在了和udp一个层级,注意这样就访问不到rcvTextEdit了。