参考资料:html
参照 View-Model 模型,QML做为 View,C++中的对象做为 Model,实现业务逻辑和界面的分离。数组
经过这种方法,QML中能够直接访问注册到上下文中的C++类实例,而且是注册到QML的全局(具体是注册到一个 QQuickView 或者 engine)。以自定义一个 Name 类,类包括一个 data 属性为例。并发
须要暴露给QML访问的类须要有特殊的定义:app
/*name.h*/ #include <QObject> class Name : public QObject //继承自QObject { Q_OBJECT//QObject宏 Q_PROPERTY(QString data READ data WRITE setData NOTIFY dataChanged) public: Name(QObject *parent = nullptr);//默认构造函数 Name(QString _name);//构造函数 QString data() const;//READ 接口 void setData(const QString& _data);//WRITE 接口 signals: QString dataChanged();//NOTIFY 信号(不需实现) private: QString m_data;//私有属性 };
能够经过右键项目->新建文件->C++ Class 来添加新类,继承自 QObject 而且自动添加文件到项目中。函数
Warning:不要在 cpp 文件中直接定义类,由于 Q_OBJECT 宏须要通过 moc 处理,非
.h
文件不会被 moc 处理,编译时出现“没法识别的符号”错误ui
Q_PROPERTY(QString data READ data WRITE setData NOTIFY dataChanged)
这一行代码定义了暴露给QML访问的接口,这里咱们提供的对象是一个 QString,READ 接口是一个名为 data 的函数, WRITE 接口是一个名为 setData 的函数,NOTIFY 接口用于通知的绑定,只有设置了 NOTIFY 接口,QML 才能自动与 C++ 中的属性同步。这里的命名方式最好与默认的统一。spa
/*name.cpp*/ #include "name.h" Name::Name(QObject *parent) : QObject(parent) {//默认构造函数 } Name::Name(QString _data) : m_data(_data) {//自定义构造函数,初始化私有对象m_data } QString Name::data() const { return m_data;//READ 接口实现,返回私有对象 } void Name::setData(const QString& _data) { if(_data != m_data){//WRITE 接口实现,更新m_data并发出信号 m_data = _data; emit dataChanged(); } }
在 setData 中,必须判断数据是否更新,只有当数据真正改变时才发出信号,不然有无限递归的风险。code
而后是实例化一个 Name 类并注册到上下文。注册须要在读取.qml
文件以前完成。htm
/*main.cpp*/ Name a_name("test"); QQmlApplicationEngine engine; QQmlContext* rootContex = engine.rootContext();//拿到engine的根上下文 rootContex->setContextProperty("name", QVariant::fromValue(a_name));//注册 engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
QML 中能够读取到接口返回的属性,直接给属性赋值,而且监听到属性变化。对象
/*main.qml*/ Rectangle { Text { text:name.data //至关于调用name.data()这个接口 Connections {//创建一个到NOTIFY接口的链接 target: name onDataChanged:{//对应NOTIFY接口 console.log("data has beed changed!"); } } } Button { text:"changeData" onClicked: { name.data = "hasChanged!"; } } }
当点击按钮时,Text 的文字会发生变化,同时控制台输出 “data has beed changed!”,闭环达成。
Connections 用来监听事件,监听的 target 是定义了 NOTIFY 的对象(注意 onDataChanged 这个 event handler 的驼峰命名法和原 NOTIFY 定义时的命名。
更常见的需求是,暴露一组对象的属性,并经过 QML 中的 View 来自动渲染。例如咱们把 Name 类放进一个 QList 列表,做为 Model 传给 QML 中的 ListView。
/*main.cpp*/ QList<QObject*> NameList; NameList.append(new Name("name1")); NameList.append(new Name("name2"));//增长两个Name对象在数组中 QQmlApplicationEngine engine; QQmlContext* rootContex = engine.rootContext();//拿到engine的根上下文 rootContex->setContextProperty("nameList", QVariant::fromValue(NameList));//注册 engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
/*main.qml*/ ListView{ width: parent.width height: 300 model: nameList delegate: Rectangle { height: 30 width: parent.width color: "#000000" Text { text: model.data //调用每一个对象的data()方法 color: "#FFFFFF" Connections{ target: model //监听这个对象 onDataChanged:{ console.log("changed!"); } } } } }
这样就获得了一个列表渲染。此时渲染出来的列表在注册到上下文的时候就已经肯定,列表项Name
动态的修改会体现到界面上,可是动态增删数组元素不会发生变化(由于 QList 自己并无 NOTIFY 接口)。
当前的 Name 类已经完备的实现了注册到 QML 上下文中的接口,因此能够在 C++ 中调用
qmlRegisterType<Name>("com.myapp.name", 1, 0, "Name"); //包名,大版本号,小版本号,类型名
进行注册,在 QML 文件中加入引用后就能够直接使用 Name 类以及其属性。
import com.myapp.name 1.0 Name { data: "Foo" }
与直接放入 QList 不一样,经过更高级的封装能够实现动态绑定的列表渲染。避免使用QQmlListProperty
这个方法,文档有问题且相关资料少
参考资料
朴素的 C++ 线性表类型(数组或者 Vector 模板类等等)经过封装就能够成为被 QML 直接访问的 Model。封装成的类能够是继承自 QAbstractListModel 或者更复杂的 QAbstractTableModel。关键在于继承后实现几个做为 QML 调用接口的虚函数(完整的虚函数表参照文档):
/*必须实现的虚函数*/ int rowCount(const QModelIndex &parent) const;//返回数据行数 QVariant data(const QModelIndex &index, int role) const;//返回根据index和role请求的数据 QHash<int, QByteArray> roleNames() const;//返回数据别名
经过实现更多的虚函数能够完成更复杂的面向 QML 功能。
假定咱们有一个 QList 内部的每一个元素都是 Name 类型,注意 Name 已经完成了对 QML 访问所必须的封装。因而一个可在QML中渲染的 Name 类型的线性表封装成的类应该长这样:
/*namelist.h*/ #include <QAbstractListModel> #include <QVariant> #include <QDebug> #include "name.h" class NameList : public QAbstractListModel { Q_OBJECT public: enum datatype { type1 = 0 }; NameList(QObject *parent); NameList(){ addName("test1"); addName("test2"); } /*必须实现的虚函数 供QML引擎调用*/ int rowCount(const QModelIndex &parent) const;//返回数据行数 QVariant data(const QModelIndex &index, int role) const;//返回所求的数据 QHash<int, QByteArray> roleNames() const;//返回数据别名 /*其余接口*/ Q_INVOKABLE bool pushData(QString a_name); private: QList<Name*> _NameList;//被封装的数组 };
首先类内声名了一个枚举类型,每一个类型对应数据项中被访问的一个属性。Name 类只有一个 data 属性,因此只定义了一种类型。
而后是rowCount(const QModelIndex &parent)
,QML引擎查询列表时经过这个函数取得列表项的数量。
QVariant data(const QModelIndex &index, int role)
是QML引擎用来访问每一个列表项的接口,访问的时候会经过index
代表索引,role
代表查找的属性(对应枚举类型datatype
)。
QHash<int, QByteArray> roleNames()
返回role的别名(暂时不是特别重要)。
/*namelist.cpp*/ #include <QQmlListProperty> #include <QList> #include "namelist.h" NameList::NameList(QObject *parent) { } int NameList::rowCount(const QModelIndex &parent) const { return _NameList.count();//返回私有列表数据量 } QVariant NameList::data(const QModelIndex &index, int role) const { qDebug() << role; int row = index.row();//index包含.row()和.count()等属性 return QVariant::fromValue(_NameList.at(row));//数据项包装成QVariant返回 } QHash<int, QByteArray> NameList::roleNames() const { QHash<int, QByteArray> d; d[datatype::type1] = "Foo";//给tpye1设置别名 return d; } bool NameList::pushData(QString a_name) { Name* cache = new Name(a_name); beginInsertRows(QModelIndex(), _NameList.count(), _NameList.count()); _NameList.append(cache); endInsertRows(); return true; }
列表被封装之后,在进行增删的时候须要先调用beginInsertRows(QModelIndex, int , int)
,第一个参数对应 Model 数据,经过QModelIndex()
获得这个Model的虚拟rootItem
;后两个参数表明所改动的行数范围:例如在第二行加入3个数据,则两个参数分别是\( (2, 4) \). 修改完成后还要调用endInsertRows()
声名修改完毕。
/*main.cpp*/ NameList theList;//实例化一个类 QQmlApplicationEngine engine; QQmlContext* rootContex = engine.rootContext(); rootContex->setContextProperty("namelist", &theList);//注册到上下文 engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
/*main.qml*/ ListView{ width: parent.width height: 300 model: namelist //把抽象类做为model delegate: Rectangle { height: 30 width: parent.width color: "#999999" Text { text: model.modelData.name //name是每一个item的属性 color: "#FFFFFF" } Button { anchors.right: parent.right width: 50 height: 30 font.family: "FontAwesome" font.pixelSize: 24 text: "\uf019" onClicked: { model.modelData.name = textField.text;//能够直接给item修改属性 } } } }