假设有一个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返 回结果。