背景:
【qml-3】qml与c++交互第二次尝试(类型方式)
还是qml学习笔记。
这次搁置太久了。其实不太会,还是以教程为主,但讲课那哥们也是以尝试为主,只是人家经验多。这里吐槽一下,qml目前还是差太多。
官方手册都啥也不是,远不如widget完善好用。版本差异有些挺大,比如学了5再用6,有些真不一样。我干脆从最新的6开始搞,因为5早晚淘汰。
写代码时有些属性明明能用,但就是报错说非法,你得按住ctrl转到类型定义,还不能跳转到头文件,弹窗报错但能复制那个头文件的名,然后去文件系统qt安装目录里找到它再参考,发现里面有这个属性可以用。
先吐槽这些,也许是自己太笨。
回顾:
之前写过实例方式的qml和c++交互。
亦即,在c++里直接把类型实例化,把对象指针注册到qml上下文,然后qml直接调用它。
这种方式倒是直截了当,但是从qml里看有些晦涩,冷不丁出来个对象名,拿来就用。
个人感觉这种方式还可以,适合规模小的项目,简单高效。
类型方式:
本次记录的是类型方式,亦即把c++里写好的类,不实例化,而是把类型注册给qml,就像它自己的Button、Item一样,用的时候使用“{}”给它实例化。这么说起来好像更合适一些。那就上demo。
quick项目说明:
这里要插入一段,我觉得按照官方态度,应该是让咱们用Design Studio(DS)做ui,导出到c++的quick项目再混合编程。
这个该死的DS熟悉起来别具一格,等有时间再整理。这里主要说quick项目。
如上图,两种类型,图上选中的是分类类型,构建后qml文件会复制到build目录,发布时跟着exe一起,前后端分离。好处是可以随时改qml不用编译exe,不好就是所谓不安全。
compat那个是把qml放到qrc资源文件,它就一起编译进exe发布,所谓更安全。
隐约记着其实web也有这种概念,以前做c#全栈,我们都是上传aspx,改着方便。
这次demo我选的分离方式,无所谓,改一下很简单。
demo:
如上所说我选的不带compat的quick项目,一路下一步就行,它默认是cmake方式构建。
如上图,我已经添加了一个MyClass类。代码如下:
#ifndef MYCLASS_H
#define MYCLASS_H
#include <QObject>
#include <QQmlEngine>
#include <QDebug>
class MyClass : public QObject
{
Q_OBJECT
QML_ELEMENT
public:
explicit MyClass(QObject *parent = nullptr);
public slots:
QString onFunc()
{
qDebug() << "slot in cpp";
return "cpp value";
}
void onFromQml_GetValue()
{
qDebug() << "onFromQml_GetValue";
emit sigToQml_SendValue("cpp_value");
}
signals:
void sigToQml_SendValue(QString);
};
#endif // MYCLASS_H
#include "myclass.h"
MyClass::MyClass(QObject *parent)
: QObject{parent}
{
qDebug() << "Class is created in cpp.";
}
已经尽量简单了。主要为了验证一些事。
上面QString onFunc()这个槽函数加了返回值。各位应该记得,qt手册里讲过,connect的队列模式下信号是拿不到返回值的,因为是异步。只有direct和阻塞队列方式可以,也容易理解。这里不深究qt手册了,您可以自己看,要是我记错了咱再讨论。我这里就是为了验证qml和c++之间是个啥情况。
onFromQml_GetValue槽函数就再正常不过了,不多解释。就是让它接受qml发来的信号,再把“返回值”发给qml。我就是想实现qml调用c++,比如查询。
再看qml代码:
import QtQuick
import QtQuick.Controls
import cpp.MyClass
Window {
id: root
width: 640
height: 480
visible: true
title: qsTr("Hello World")
//qml查询信号
signal sigToCpp_GetValue
onSigToCpp_GetValue: {
myclass.onFromQml_GetValue();
}
//c++返回信号
Connections {
target: myclass
//以下两种方式都可以,非要共存那就下面的传统方式优先,我试出来的。
//Connections里面,function方式推荐,官方已经不推荐传统方式了。
function onSigToQml_SendValue(s) {
btn.text = s;
print("-----" + s);
}
// onSigToQml_SendValue: (s) => { root.title = s; }
}
//C++实例化
MyClass {
id: myclass
}
Button {
id: btn
width: 100
height: 40
text: "press me"
onClicked: {
// text = myclass.onFunc();//这是可以的
sigToCpp_GetValue();//发送qml信号
}
}
}
CMakeLists.txt
...
qt_add_executable(appuntitled
main.cpp
# myclass.h myclass.cpp #注意这里
)
qt_add_qml_module(appuntitled
URI cpp.MyClass #注意这里
VERSION 1.0
QML_FILES
Main.qml
SOURCES myclass.h myclass.cpp #注意这里
)
...
对于CMakeList.txt的内容,说明一下。在qml分离方式的项目中添加类,它会在CMakeLists.txt中把头文件和源文件加到qt_add_qml_module函数中,在qml资源方式的项目中,它会加到qt_add_executable函数中。亲测效果没影响,但是个人感觉加在qt_add_executable中更合适。这算一个讨论点。因为目前我没试出来区别。
注意,cmake里设置的URI是qml里import时用的模块,同时对应build目录中那个扩展名是qmltypes的文件路径。至于自己定义的c++类名,和这个没有直接关系。如果有多个c++类要为qml服务,可以都加到某个模块中。就像QtQuick.Controls一样,里面有好多类。
我之前以为,C++里只要写了那个QML_ELEMENT宏就可以自动实现了,但不幸还得改cmake文件,这个URI是个地址,对应build目录里的情况。
之前默认可不是这样的,qt会默认一个untitile,你把它改成自己想要的就行了。当然对应main函数也要改。
因为项目选择的是分离模式,qml是不参与编译,而是单独的文件,所以这里写的路径其实对应build目录里的文件路径,发布时一并复制给目标机即可。
效果:
没问题的,就是点击按钮,由qml向c++发送请求,c++返回数据。
解析:
其实最直接简单的方式就是直接调c++的槽函数就行了,可以带返回值,目前感觉最方便。
之前我以为普通函数也可以,实测不行,必须是槽函数。(后面有补充)
非要发信号走个流程就是上面那样,跟第一篇博客(实例方式)一样道理。就是实例化的时机不一样。
问题:
无论是哪种方式的项目,看教程以及我亲测都会有个问题。每次做完了运行不影响,但qml文件里总有警告,以下三步操作后警告会消失,不一定第几步有效。
1、先运行一下,让它生成build目录,寻址需要。
2、关闭项目,重新打开项目。
3、关闭creator,重新打开。
到这里忍不住还是要吐槽,这可不是我一个人的问题,教程视频里也是如此。还是不完善。您说呢?
补充:
上面说直接调用c++普通函数不行,其实是如果是普通函数,声明时需要在前面加一个宏Q_INVOKABLE。具体看手册吧,这个宏的意义就是让函数可以被元对象系统调用。熟悉qt信号槽的朋友到这里应该明白原因了。qt推崇的松散耦合就是信号槽,元对象系统实现的,qml这种前后端分离的东西显然也是基于元对象的信号槽。这就是为什么直接调用普通函数不可以,而是要加上这个宏。
先记录到这里,本文完。