使用QML开发界面
加载QML的3种方式
支持文件路径、资源(资源文件)路径,网络路径
- 使用QQmlApplicationEngine
这个类结合了QQmlEngine和QQmlComponent
QGuiApplication app(argc,argv);//1、使用QQmlApplicationEngine加载qml代码,他结合了QQmlEngine和QQmlComponent
//构造函数指定qml文件的路径
QQmlApplicationEngine appeng("qrc:/Main.qml");//获取根节点并设置参数
QList < QObject * > objs = appeng.rootObjects();//假设qml中的根节点是Window,那么其实例化对象的类型是QQuickWindow
//因此将其转为实际的类型QQuickWindow*
auto win = (QQuickWindow * ) objs[0];//修改其坐标和标题
win -> setX(30);
win -> setY(30);
win -> setTitle("你好世界");return app.exec();
- 使用QQuickView
要求:qml文件的根节点不能是Window及其派生元素,因为QQuickView 会自己创建根窗口,
此外,还需要调用其show()方法,窗口才会显示出来
QGuiApplication app(argc,argv);//使用QQuickView
//这种方式的话qml代码的根类型不能是Window,他会自动创建根窗口
QQuickView view;
//设置qml文件路径
view.setSource(QUrl("qrc:/MyItem.qml"));
//需要手动调用show才会显示
view.show();return app.exec();
- 使用QQmlComponent和QQmlEngine
- QQmlComponent加载QQmlEngine引擎
- QQmlComponent设置qml文件路径
- 调用QQmlComponent的create()方法(返回根节点的实例化对象的指针),创建实例后才会显示窗口
- 以通过QQmlComponent的isError()方法判断qml文件是否出错
- 以及errorString()方法获取具体的错误信息
QGuiApplication app(argc,argv);//使用QQmlComponent和QQmlEngine
QQmlEngine eng;
//1、组件加载引擎
QQmlComponent com( & eng);
//2、加载qml文件
com.loadUrl(QUrl("qrc:/Main.qml"));//获取错误信息,并打印
if (com.isError()) {qDebug() << com.errorString();
}//4、然后创建这个组件才会实例化并显示
//创建后会返回根节点的指针(假设根节点是Window,实例化之后就是QQuickWindow)
//使用智能指针管理这个实例化的组件
std::shared_ptr < QQuickWindow > p((QQuickWindow * ) com.create());//设置根节点的一些属性
p -> setTitle("Main.qml");
p -> setColor(Qt::red);return app.exec();
查找子节点并读取和修改节点的属性
- 先获取到根节点的实例的指针
- qml中的子节点需要设置objectName属性
- 调用findChild<>()方法根据objectName来查找对应的子节点,获取到子节点实例的指针
- 找到了则调用property()和setProperty()方法来读取和设置对应的属性
//加载引擎和qml文件
QQmlEngine eng;
QQmlComponent com( & eng);
com.loadUrl(QUrl("qrc:/Main.qml"));//假设qml中根节点是Window,实例化之后就是QQuickWindow
std::shared_ptr < QQuickWindow > p((QQuickWindow * ) com.create());//通过findChild方法(模板方法)访问qml里面的节点并修改属性
//需要先给qml中的节点设置objectName属性,然后根据这个objectName进行查找
//假设查找objectName为"mytxt"的子节点
auto mytxt = p -> findChild < QObject * > ("mytxt");//非空则找到了
if (mytxt != nullptr) {qDebug() << "找到了";//读取属性,返回的是QVariantauto width = mytxt -> property("width");qDebug() << "mytxt的宽度是:" << width.toInt();//修改属性mytxt -> setProperty("text", "你好世界");
}
递归遍历所有节点
可以调用自带的void QObject::dumpObjectTree() const,这个函数可以打印出所有的子节点
手写:
//参数1:某个节点的实例的指针
void printAllNode(QObject* obj,int level=0){if(obj==nullptr) return;QString head="";for(int i=0;i<level;++i){head+="-";}QString str=head;str+="className:";str+=obj->metaObject()->className();str+=" ";str+="objectName:";str+=obj->objectName();str+=" ";str+=obj->property("width").toString();str+=":";str+=obj->property("height").toString();qDebug()<<str;//获取子节点auto subs=obj->children();//递归遍历子节点for(auto itor:subs){printAllNode(itor,level+1);}
};
cpp和qml中的类型对应
- 基础类型对应关系
如果qml中函数的参数的类型是这些基础类型,那么cpp中可以传下面的类型
QVariant var
- 数组类型对应关系
如果qml中函数的参数的实际类型是数组,那么cpp中可以传下面的类型
- 对象类型对应关系
如果qml中函数的参数的实际类型是js对象,那么cpp中可以传下面的类型
QVariantMap
cpp端直接调用qml端的函数
使用静态方法QMetaObject::invokeMethod来调用qml中函数
- 参数1:节点指针
一定要获取到qml函数所在节点的实例化对象的指针,
只获取到父节点或者祖宗节点都不行,会找不到这个函数
- 参数2:qml函数名
qml文件:
Window{id:rootwidth: 400height: 300visible: truetitle: "main.qml"//qml自定义信号signal sig1(msg:string)//无参数 无返回值function qmlFunc1(){print("call qmlFunc1")}//参数为int 或string或var 无返回值function qmlFunc2(index:int,str:string,param:var){print("call qmlFunc2;",index," ",str," ",param)}//有返回值,没有显示指定返回值类型,则返回值类型为varfunction qmlFunc3(){print("call qmlFunc3")return "a string"}//有返回值,显示指定类型为stringfunction qmlFunc4():string{print("call qmlFunc4")return "a string"}//有参数,有返回值function qmlFunc5(cnt:var):var{print("call qmlFunc5:",cnt)return "a string"}//参数是数组function qmlFunc6(arr:var){print("qmlFunc6")//遍历这个数组for(var i=0;i<arr.length;++i){print(arr[i]," ")}}//参数是js对象function qmlFunc7(obj:var){print("qmlFunc7")//遍历这个对象for(var key in obj){print("key:",key," value:",obj[key]);}}Text {id: txtobjectName: "mytxt"text: qsTr("text")font.pixelSize: 25Rectangle{anchors.fill: txtcolor: "red"z:-1}}Button{id:btnobjectName: "btn"anchors.centerIn: parentwidth: 100height: 30text: "test qml"onClicked: {root.sig1("signal1 from qml")}}}
调用qml中无参数,无返回值的函数
QQmlApplicationEngine appeng("qrc:/Main.qml");
QList < QObject * > objs = appeng.rootObjects();
auto win = (QQuickWindow * ) objs[0];//参数1:qml函数所在节点的实例化对象的指针
//参数2:qml函数名
QMetaObject::invokeMethod(win, "qmlFunc1");
调用qml中带参数,无返回值的函数
两端的参数类型要对应好
QQmlApplicationEngine appeng("qrc:/Main.qml");
QList < QObject * > objs = appeng.rootObjects();
auto win = (QQuickWindow * ) objs[0];//qml中的var类型对应qt里面的QVariant
//qml中的string类型对应qt里面的QString
//后面传对应的参数
QMetaObject::invokeMethod(win, "qmlFunc2",100, QString("你好"), QVariant(123));
调用qml中带返回值的函数,且没有显示指定返回值的类型
用QVariant接收返回值
然后用qReturnArg包裹
作为QMetaObject::invokeMethod的第三个参数传进去
QQmlApplicationEngine appeng("qrc:/Main.qml");
QList < QObject * > objs = appeng.rootObjects();
auto win = (QQuickWindow * ) objs[0];//调用qml函数,有返回值的函数(但是没有显示指定返回值类型),使用QVariant接收返回值
//用qReturnArg包裹下返回值
QVariant ret;
QMetaObject::invokeMethod(win, "qmlFunc3", qReturnArg(ret));
//转为实际的类型
qDebug() << ret.toString();
调用qml中带返回值的函数,且显示指定返回值的类型
如果qml中的函数显示指定了类型,那么就用Cpp端对应的类型接收
比如这里qml中的函数显示指定了返回值的函数时string类型
QQmlApplicationEngine appeng("qrc:/Main.qml");
QList < QObject * > objs = appeng.rootObjects();
auto win = (QQuickWindow * ) objs[0];//调用qml函数,有返回值的函数(显示指定了返回值类型),直接使用qt中对应的类型接收
QString ret2;
QMetaObject::invokeMethod(win, "qmlFunc4", qReturnArg(ret2));
qDebug() << ret2;
调用qml中带返回值,带参数的函数
显然就是综合前面的来调用
QQmlApplicationEngine appeng("qrc:/Main.qml");
QList < QObject * > objs = appeng.rootObjects();
auto win = (QQuickWindow * ) objs[0];//调用带参数,带返回值的函数
//参数3:用来接收返回值
//后面的参数就传qml函数所需的参数
QVariant ret3;
QMetaObject::invokeMethod(win, "qmlFunc5", qReturnArg(ret3), QVariant(4));
qDebug() << ret3.toString();
调用qml中参数实际类型是数组的函数
cpp端可以传QVariantList等等(见上面的类型对应)
但是传参的时候仍然要用QVariant包裹,
因为qml中的函数参数类型是var(只不过实际类型是数组)
QQmlApplicationEngine appeng("qrc:/Main.qml");
QList < QObject * > objs = appeng.rootObjects();
auto win = (QQuickWindow * ) objs[0];//qml中的函数参数实际类型是js数组
//cpp端则传QVariantList...等等
//调用时要用 QVariant包裹
QVariantList arr {1,2,3,4};
QMetaObject::invokeMethod(win, "qmlFunc6",QVariant(arr));//传vector<int>
std::vector < int > arr2 {1,2,3,4};
QMetaObject::invokeMethod(win, "qmlFunc6",QVariant::fromValue(arr2));
调用qml中参数实际类型是js对象的函数
cpp端只能传QVariantMap
QQmlApplicationEngine appeng("qrc:/Main.qml");
QList < QObject * > objs = appeng.rootObjects();
auto win = (QQuickWindow * ) objs[0];//qml中的函数参数实际类型是js对象
//cpp端则传QVariantMap
QVariantMap maps;
maps["name"] = "张三";
maps["age"] = 18;
QMetaObject::invokeMethod(win, "qmlFunc7",QVariant::fromValue(maps));
cpp端接收qml端的信号
- 仍然是通过connect函数连接,且只支持qt4的写法,也不支持lambda表达式
- 需要获取到信号所在节点的实例化对象的指针(即信号的发送者)
- qml中信号的参数类型在连接时要转成cpp的类型
先定义cpp中接受信号的对象和槽函数
class MyClass : public QObject
{Q_OBJECT
public:explicit MyClass(QObject *parent = nullptr) : QObject{parent}{}
public slots:void cppSlot(QString msg){qDebug()<<"CppSlot:"<<msg;}void cppSlot2(){qDebug()<<"按钮点击了";}signals:
};
连接槽函数
QQmlApplicationEngine appeng("qrc:/Main.qml");
QList < QObject * > objs = appeng.rootObjects();
auto win = (QQuickWindow * ) objs[0];//定义接收的对象
MyClass obj;//绑定接收对象的槽函数
//qml中信号所在节点的实例化对象的地址 具体的信号 接收对象的地址 接收对象的槽函数
//注意参数类型要对应,比如这里qml中的信号参数是string 这里变成QString
QObject::connect(win, SIGNAL(sig1(QString)), & obj, SLOT(cppSlot(QString)));//绑定qml自带的信号:Button元素的clicked信号
auto btn = win -> findChild < QObject * > ("btn");if (btn != nullptr) {QObject::connect(btn, SIGNAL(clicked()), & obj, SLOT(cppSlot2()));
}
qml端接收cpp端的信号
方式1:和上面一样,只不过发送者变成cpp里面的对象,接收者变成qml里面的节点
方式2:cpp端自己接收信号,绑定cpp端的槽函数,然后在槽函数中通过invokeMethod来调用qml中的函数
cpp扩展qml类型
自定义一个类,继承自QObject或者QQuickItem或者QQuickPaintedItem,这样才能被qml使用
类中添加宏Q_OBJECT,使他支持信号槽
类中添加宏QML_ELEMENT,使他能够在qml文件中使用
使用Q_PROPERTY定义各种属性,给属性提供读取函数和属性改变信号,这些属性可以在qml中使用
在成员方法前面加上Q_INVOKABLE,这样就可以将cpp的函数暴露给qml了,qml中可以调用
如果有定义的枚举,使用强类型枚举,且使用Q_ENUM注册
下面是一个cpp类扩展了qml的类型
#ifndef CPPTYPE_H
#define CPPTYPE_H#include <QObject>
#include <QQmlEngine>//1、继承QObject或他的派生类 只有这样才能给qml使用
class CppType : public QObject
{//2、添加Q_OBJEC支持信号槽Q_OBJECT//3、添加QML_ELEMENT使得他能够在qml中使用QML_ELEMENT//4、设置属性 提供读写函数 和 属性改变信号Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)//属性还可以绑定数据成员 此时不用提供读写函数 和 手动发射属性改变信号了(属性改变信号还是要提供) 他自己会处理Q_PROPERTY(int age MEMBER age_ NOTIFY ageChanged)
public://cpp定义的枚举enum class MyEnum{Value1,Value2};//注册到元对象中Q_ENUM(MyEnum)explicit CppType(QObject *parent = nullptr) : QObject{parent}{}~CppType()=default;//提供属性读取函数QString name(){return objectName();}//提供属性修改函数void setName(QString name){//前后属性一样就不用改了if(name==objectName()){return;}setObjectName(name);//发射属性改变信号emit nameChanged();}//5、将成员函数暴露给qml,qml中调用//函数前面加Q_INVOKABLE就可以暴漏出去Q_INVOKABLE void cppFunc1(){qDebug()<<"call cppFunc1";}//函数参数3种类型 基础 数组 js对象Q_INVOKABLE QString cppFunc2(int index,std::vector<int> arr,QVariantMap maps){qDebug()<<"call cppFunc2";qDebug()<<"index:"<<index;for(const auto& itor:arr){qDebug()<<itor;}for(const auto& key:maps.keys()){qDebug()<<"key:"<<key<<" value:"<<maps[key];}return "from cpp";}signals://提供属性改变信号void nameChanged();void ageChanged();//其他信号 他的信号处理器在qml中: onOtherSignal,遵循qml的规则on+信号名void otherSingal();
private:int age_=0;
};#endif // CPPTYPE_H
然后在加载qml文件之前,
需要将这个类注入到qml中,
注入完成之后,qml中就可以使用这个cpp扩展的类型了
使用qmlRegisterType这个模板函数进行注入
template <typename T>
int qmlRegisterType(const char *uri, int versionMajor, int versionMinor, const char *qmlName)
- 模板参数:自定义的cpp类
- 参数1:qml中使用import时的导入名
- 参数2:主版本号
- 参数3:子版本号
- 参数4:qml使用这个类时的元素名
//qml调用cpp 即使用cpp扩展qml类型
//一定要在加载qml文件之前将cpp扩展的类型注入到qml中
//模板参数:cpp扩展的类
//参数1:qml中使用import时的导入名
//参数2:主版本号
//参数3:子版本号
//参数4:qml使用这个类时的元素名
qmlRegisterType < CppType > ("CppType", 1, 0, "CppType");QQmlApplicationEngine appeng;
appeng.load("qrc:/Main2.qml");
然后就可以把他当成一个qml的类型在qml中使用了,遵循qml的各种语法和使用方式
import QtQuick 2.15
import QtQuick.Controls//需要先import,这里要和之前qmlRegisterType传入的参数对应
import CppType 1.0Window{id:rootwidth: 400height: 300visible: truetitle: "main2.qml"Button{width: 100height: 30text: "btn"onClicked: {cpp1.name="aaa"cpp1.age=20//调用cpp扩展类型的函数cpp1.cppFunc1()//可以直接这样传//cpp1.cppFunc2(3,[1,2,3],{name:"张三",age:18})//js数组var arr=[1,2,3]//js对象var obj={name:"张三",age:18}cpp1.cppFunc2(3,arr,obj)//访问cpp里面的枚举print(CppType.MyEnum.Value1)}}//使用cpp扩展的qml类型CppType{id:cpp1name:"cpptype"age:18onAgeChanged: {print("年龄:",age)}onNameChanged: {print("姓名:",name)}onOtherSingal: {}}
}
如果我们在qml中仅仅只是想使用cpp里面的一些函数和属性,
而不是直接使用cpp扩展的整个类型,
那么我们可以不用将这个类型注入到qml中,
而是在加载qml文件之前实例化这个cpp类型的对象
然后设置上下文属性:
//创建要使用的cpp类的实例
CppType cppType;QQmlApplicationEngine appeng;
//在加载qml文件之前获取上下文环境
//设置上下文属性
//参数1:在qml中使用实例名称
//参数2:对应的实例地址
appeng.rootContext() -> setContextProperty("cppType", & cppType);
appeng.load("qrc:/Main3.qml");
qml中直接通过对象名.函数名 或者 对象名.属性名直接调用
(这种方式无法访问cpp里面定义的枚举类型)
import QtQuick 2.15
import QtQuick.ControlsWindow{id:rootwidth: 400height: 300visible: truetitle: "window"Button{width: 100height: 30text: "btn"//还可以直接自定义属性来绑定cpp对象的属性property int age:cppType.ageonAgeChanged: {print("age变化:",age)}onClicked: {//直接使用cpp的函数,通过对象名.函数名调用(这里的函数名就是setContextProperty中设置的名称)cppType.cppFunc1()//直接访问里面的属性cppType.age=10}}
}
QWidget中局部使用qml
可以使用QQuickWidget
使用QQuickWidget来加载qml文件,就可以显示在QWidget中
qml中的根节点不能是Window,一般使用item或者其派生元素
QQuickWidget有两种resizeMode
- QQuickWidget::SizeViewToRootObject
将QQuickWidge的大小调整为和qml根元素的大小一致,默认就是这种模式,
在这种情况下,QQuickWidge的大小可以不用设置(只设置位置)
qml根元素的大小需要显式地设置,不设置就是0,QQuickWidget的宽高也会跟着缩小到0,导致显示不出来
- QQuickWidget::SizeRootObjectToView
将qml根元素的大小调整为和QQuickWidge的大小一致,
在这种情况下,QQuickWidget的大小要显式的设置(或者添加进布局之中)
qml根元素的大小可以不用设置(设置了也不生效),会调整为和QQuickWidge的大小一致
resizeMode的模式设置一定要在加载qml文件之前设置,否则会不生效!
这之后就可以在QQuickWidget显示qml的元素了,如何和里面的qml元素交互,就是前面说的那些方法了
#ifndef WIDGET_H
#define WIDGET_H#include <QWidget>
#include<QQuickWidget>class Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr) : QWidget(parent){resize(800,600);QQuickWidget* w=new QQuickWidget(this);//设置QuickWidget的几何属性w->setGeometry(10,10,300,300);//resize模式的设置一定要在加载qml文件(setSource)之前设置,否则会无效//resizeMode设置:qml根元素的大小调整为和QQuickWidget一致w->setResizeMode(QQuickWidget::SizeRootObjectToView);w->setSource(QUrl("qrc:/Main.qml"));}~Widget()=default;
};
#endif // WIDGET_H
qml文件
import QtQuick 2.15
import QtQuick.ControlsRectangle{color: "red"Slider{width: parentheight: 50anchors.centerIn: parent}
}
Q_PROPERTY的一些说明
定义:
Q_PROPERTY(type name
(READ getFunction[WRITE setFunction] |
MEMBER memberName[(READ getFunction | WRITE setFunction)])
[RESET resetFunction]
[NOTIFY notifySignal]
[REVISION int | REVISION(int[, int])]
[DESIGNABLE bool]
[SCRIPTABLE bool]
[STORED bool]
[USER bool]
[BINDABLE bindableProperty]
[CONSTANT]
[FINAL]
[REQUIRED])
一般需要注意的字段就几个点
- 属性是只读的,需要加CONSTANT
- 属性绑定了成员变量,可以不用提供属性读写函数,不用手动发射属性改变信号
- 需要在属性写入函数中发射属性改变信号(如果前后值不一样,即真正修改后才发射)
快速给数据成员设置属性
- 右键数据成员
- 点击重构
- 点击生成Q_PROPERTY