QML与C++对象多线程交互的解决方法

假设有一个C++类Controller继承QObject

此类有一个方法connectController(QString ip)标注了Q_INBOKABLE

此类在main.cpp中实例化,同时新建一个QThread,将controller对象移入子线程。

同时将此对象注册到QML中成为一个全局对象。

  Controller *controller = new Controller();
  QThread *control_thread = new QThread();
  qDebug() << "main" << app.thread()->currentThreadId();
  controller->moveToThread(control_thread);
  control_thread->start(); // 启动线程
  engine.rootContext()->setContextProperty("etcController", controller);

此时在QML中有一个按钮,点击后会调用controller的connectController方法:

    Button {
        anchors.centerIn: parent
        width: 100
        height: 50
        onClicked: {
            etcController.connectController(controllerSettings.etc_ip)
        }
    }

 在main.cpp中打印线程ID:

qDebug() << "main" << app.thread()->currentThreadId();

在connectController这个方法中也打印当前线程ID:

bool Controller::connectController(QString ip) {
  qDebug() << QThread::currentThreadId();
  return true;
}

理论上来说 启动程序后,点击按钮,将会输出两个不同的线程ID。因为我们已经使用moveToThread将controller对象移到子线程了。但是!! 问题来了,打印的线程ID是一样的。

该怎么解决呢,网上的资料包括qt的论坛基本都是说在controller和qml之间建立一个中间类,中间类持有controller实例,并将其移入子线程。同时复刻controller的信号与槽。在QML中触发中间类实例的信号来操作controller的方法。原文参考:QML使用moveToThread线程【QML工程使用C++】_mb5fdb128f2dba9的技术博客_51CTO博客

这样做未免太麻烦了,每一个类就得有一个辅助接口类。遍寻答案无果后经过高人指点,琢磨了一个快捷的办法。

细心的人可能已经看到文章开头的代码图内有一个信号connectConSign,这就是问题的关键。对于注入到QML的C++对象,目前来看,调用方法在哪个线程 方法就在哪个线程执行。所以才会出现打印的两个线程ID相同的情况。但是对象已被移入子线程了,直接让对象的信号 调用 自身的函数就好了。

  Controller *controller = new Controller();
  // 队列连接一个信号到槽以实现异步调用
  QObject::connect(controller, &Controller::connectConSign, controller,         
             &Controller::connectController, Qt::QueuedConnection);
  QThread *control_thread = new QThread();
  qDebug() << "main" << app.thread()->currentThreadId();
  controller->moveToThread(control_thread);
  control_thread->start(); // 启动线程
  engine.rootContext()->setContextProperty("etcController", controller);

这里把connectConSign信号与connectController方法连接到一起了,注意第五个参数,标注为队列连接。在QML中使用:

    Button {
        anchors.centerIn: parent
        width: 100
        height: 50
        onClicked: {
            etcController.connectConSign(controllerSettings.etc_ip)
        }
    }

运行后点击按钮,输出结果如下:

main 0x400c
0x236c

成功!

这里有一个特别说明,请不要把controller的信号和槽的连接语句放到controller类自身的构造函数之内,否则失效。

"对于注入到QML的C++对象,目前来看,调用方法在哪个线程 方法就在哪个线程执行。"这一句话做一个补充,我尝试过用

QMetaObject::invokeMethod(controller, "connectController", Qt::QueuedConnection, Q_ARG(QString,"192.168.2.222"));

这样反射执行的方法也会运行在子线程


2023.7.13 更新:

前面的部分只讲了QML怎么能调用另一个线程里的C++对象的方法,经过实际测试发现,调用之后无法正常的获取方法返回值。来重新回想一下之前的操作:

bool connectConSign(QString ip);

这个信号已在main.cpp中经过队列连接到一个槽函数

QObject::connect(controller, &Controller::connectConSign, controller,         
                                &Controller::connectController, Qt::QueuedConnection);

槽函数是这么实现的:

bool Controller::connectController(QString ip) {
  if (!m_zmotion.connectController(ip.toStdString())) {
    qDebug() << "[控制器] - 控制器连接失败. IP: " << ip;
    return false;
  }
  qDebug() << "[控制器] - 控制器连接成功. IP: " << ip;
  return true;
}

在main.cpp中注册到QML中成为一个全局对象:

Controller *controller = new Controller();
engine.rootContext()->setContextProperty("etcController", controller);

在QML中如果这么调用:

        onClicked: {
            if(mainWindow.isControllerConnected){
                etcController.disconnectCon()
                return
            }
            var result = etcController.connectConSign(controllerSettings.etc_ip)
            print(result)
            mainWindow.isControllerConnected = result
        }

打印出来的result就会是false,即使connectController内的逻辑正确无误。

无效的尝试:

首先我尝试给connectController()方法更改了返回类型为void,让其emit一个新增的信号:

void connectEnd(bool success);
void Controller::connectController(QString ip) {
  if (!m_zmotion.connectController(ip.toStdString())) {
    qDebug() << "[控制器] - 控制器连接失败. IP: " << ip;
    emit connectEnd(false);
    return;
  }
  qDebug() << "[控制器] - 控制器连接成功. IP: " << ip;
  emit connectEnd(true);
}

然后尝试在QML中通过Connections来监听connectEnd信号并处理:

    Connections {
        target: etcController.connectEnd
        function handle(result){
            print(result)
        }
    }

直接报错

QQmlEngine: Illegal attempt to connect to Controller(0x1bf585efbd0) that is in a different thread than the QML engine QQmlApplicationEngine(0x24ee3ff770.

嗯,不同线程之间不能连接。

好,那能不能尝试在main.cpp中获取main.qml的一个槽(function),然后像之前那样通过队列连接到Controller的connectEnd信号上呢?

打住, 太麻烦了,况且我也不清楚怎么在main.cpp中获取到那个Slot。

 解决方案:

  在main.qml中:

function handleConnectResult(result){
        print("ConnectResult:" + result)
    }

    Component.onCompleted: {
        etcController.connectEnd.connect(handleConnectResult)
    }

搞定!!!至此,QML中调用和监听都完成了,终于摆脱了线程间的中间对象了。

国内可能研究QML的还是太少, 翻阅了大量的资料都没有解决办法。无奈之下自己琢磨了这个奇淫技巧,如果有更好的方法麻烦评论告诉我一下


2023.8.3 更新

关于调用方法获取返回值的部分, 7月13号的方法太繁琐啦。之前忽略了一个信号槽连接的参数

Qt::BlockingQueuedConnection

在这之前都是使用的

Qt::QueuedConnect

看命名就可以明白差异了吧,BlockingQueued会阻塞直到槽函数执行完毕并返回。

举个例子:

当使用QueuedConnect时:

QObject::connect(controller, &MotionController::toPrintThreadId, controller, &MotionController::printfThreadId, Qt::QueuedConnecti
int MotionController::printfThreadId() {
    qDebug() << "printfThreadId: " << QThread::currentThreadId();
    return 999;
}
    Button {
        width: 100
        height: 50
        anchors.centerIn: parent
        onClicked: {
            print("result " + etcController.toPrintThreadId())
        }
    }

此时点击按钮的返回结果为, 可以看出槽函数内的qDebug()都没有执行 QML就已经收到返回值了

Main Thread:  0x3da0
Instance Thread:  0x3da0
qml: result 0
printfThreadId:  0x3f3c

换成阻塞的队列连接之后:

Main Thread:  0x39cc
Instance Thread:  0x39cc
printfThreadId:  0x3ba8
qml: result 999

 搞定, 这里有一个后续问题需要说明:

 如果使用的Blocking连接,槽函数内进行了延时操作,将会卡死主界面。

 要么就不要在BlockingQueued连接的槽函数内进行延时,要么就像上一种方案一样写一个emit返   回结果。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
QMLC++交互方法主要有以下几种: 1. 使用QObject::connect()函数连接QMLC++对象的信号和槽函数,实现数据的传递和交互。 2. 使用Q_INVOKABLE宏将C++函数声明为可在QML中调用的函数。这样,QML可以直接调用C++函数,实现数据的交互。 3. 使用Q_PROPERTY宏将C++对象的属性声明为可在QML中访问的属性。这样,QML可以直接访问C++对象的属性,实现数据的交互。 4. 使用QQmlContext类将C++对象注册到QML中,并在QML中访问这些对象。这样,QML可以直接访问C++对象,实现数据的交互。 下面是一个具体的例子,展示了如何在QML中访问C++对象的属性和函数。 C++代码: ``` // MyObject.h #include <QObject> class MyObject : public QObject { Q_OBJECT Q_PROPERTY(int value READ value WRITE setValue NOTIFY valueChanged) public: explicit MyObject(QObject *parent = nullptr); int value() const; void setValue(int value); signals: void valueChanged(); private: int m_value; }; // MyObject.cpp #include "MyObject.h" MyObject::MyObject(QObject *parent) : QObject(parent) { m_value = 0; } int MyObject::value() const { return m_value; } void MyObject::setValue(int value) { if (m_value != value) { m_value = value; emit valueChanged(); } } ``` QML代码: ``` import QtQuick 2.0 Item { width: 200 height: 200 Text { text: "Value: " + myObject.value } Slider { anchors.top: text.bottom from: 0 to: 100 value: myObject.value onValueChanged: myObject.value = value } Connections { target: myObject onValueChanged: text.text = "Value: " + value } Component.onCompleted: { console.log("Value:", myObject.value) } } ``` 在这个例子中,我们创建了一个名为MyObject的C++对象,并将其注册到QML中。MyObject类有一个名为value的属性和一个名为setValue的槽函数,用于设置属性值。在QML中,我们创建一个Slider和一个Text组件,用于显示和修改MyObject的value属性。我们还使用Connections组件将MyObject的valueChanged信号与Text的text属性绑定,以使其能够自动更新。最后,在Component.onCompleted信号处理程序中,我们打印MyObject的value属性的值,以验证其已成功从C++对象传递到QML界面。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值