使用QML开发界面
加载QML的3种方式
支持文件路径 、资源(资源文件)路径 ,网络路径
- 使用QQmlApplicationEngine
这个类结合了QQmlEngine和QQmlComponent
cpp
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()方法,窗口才会显示出来
cpp
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()方法获取具体的错误信息
cpp
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()方法来读取和设置对应的属性
cpp
//加载引擎和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() << "找到了";
//读取属性,返回的是QVariant
auto width = mytxt -> property("width");
qDebug() << "mytxt的宽度是:" << width.toInt();
//修改属性
mytxt -> setProperty("text", "你好世界");
}
递归遍历所有节点
可以调用自带的void QObject::dumpObjectTree() const,这个函数可以打印出所有的子节点
手写:
cpp
//参数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文件:
cpp
Window{
id:root
width: 400
height: 300
visible: true
title: "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)
}
//有返回值,没有显示指定返回值类型,则返回值类型为var
function qmlFunc3()
{
print("call qmlFunc3")
return "a string"
}
//有返回值,显示指定类型为string
function 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: txt
objectName: "mytxt"
text: qsTr("text")
font.pixelSize: 25
Rectangle{
anchors.fill: txt
color: "red"
z:-1
}
}
Button{
id:btn
objectName: "btn"
anchors.centerIn: parent
width: 100
height: 30
text: "test qml"
onClicked: {
root.sig1("signal1 from qml")
}
}
}
调用qml中无参数,无返回值的函数
cpp
QQmlApplicationEngine appeng("qrc:/Main.qml");
QList < QObject * > objs = appeng.rootObjects();
auto win = (QQuickWindow * ) objs[0];
//参数1:qml函数所在节点的实例化对象的指针
//参数2:qml函数名
QMetaObject::invokeMethod(win, "qmlFunc1");
调用qml中带参数,无返回值的函数
两端的参数类型要对应好
cpp
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的第三个参数传进去
cpp
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类型
cpp
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中带返回值,带参数的函数
显然就是综合前面的来调用
cpp
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(只不过实际类型是数组)
cpp
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
cpp
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中接受信号的对象和槽函数
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:
};
连接槽函数
cpp
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的类型
cpp
#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这个模板函数进行注入
cpp
template <typename T>
int qmlRegisterType(const char *uri,
int versionMajor,
int versionMinor,
const char *qmlName)
- 模板参数:自定义的cpp类
- 参数1:qml中使用import时的导入名
- 参数2:主版本号
- 参数3:子版本号
- 参数4:qml使用这个类时的元素名
cpp
//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的各种语法和使用方式
cpp
import QtQuick 2.15
import QtQuick.Controls
//需要先import,这里要和之前qmlRegisterType传入的参数对应
import CppType 1.0
Window{
id:root
width: 400
height: 300
visible: true
title: "main2.qml"
Button{
width: 100
height: 30
text: "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:cpp1
name:"cpptype"
age:18
onAgeChanged: {
print("年龄:",age)
}
onNameChanged: {
print("姓名:",name)
}
onOtherSingal: {
}
}
}
如果我们在qml中仅仅只是想使用cpp里面的一些函数和属性,
而不是直接使用cpp扩展的整个类型,
那么我们可以不用将这个类型注入到qml中,
而是在加载qml文件之前实例化这个cpp类型的对象
然后设置上下文属性:
cpp
//创建要使用的cpp类的实例
CppType cppType;
QQmlApplicationEngine appeng;
//在加载qml文件之前获取上下文环境
//设置上下文属性
//参数1:在qml中使用实例名称
//参数2:对应的实例地址
appeng.rootContext() -> setContextProperty("cppType", & cppType);
appeng.load("qrc:/Main3.qml");
qml中直接通过对象名.函数名 或者对象名.属性名直接调用
(这种方式无法访问cpp里面定义的枚举类型)
cpp
import QtQuick 2.15
import QtQuick.Controls
Window{
id:root
width: 400
height: 300
visible: true
title: "window"
Button{
width: 100
height: 30
text: "btn"
//还可以直接自定义属性来绑定cpp对象的属性
property int age:cppType.age
onAgeChanged: {
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元素交互,就是前面说的那些方法了
cpp
#ifndef WIDGET_H
#define WIDGET_H
#include <QWidget>
#include<QQuickWidget>
class Widget : public QWidget
{
Q_OBJECT
public:
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文件
cpp
import QtQuick 2.15
import QtQuick.Controls
Rectangle{
color: "red"
Slider{
width: parent
height: 50
anchors.centerIn: parent
}
}
Q_PROPERTY的一些说明
定义:
cpp
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