参考资料:
- Exposing Attributes of C++ Types to QML
- Overview - QML and C++ Integration
- Embedding C++ Objects into QML with Context Properties
参照 View-Model 模型,QML作为 View,C++中的对象作为 Model,实现业务逻辑和界面的分离。
暴露单个C++类的属性
通过这种方法,QML中可以直接访问注册到上下文中的C++类实例,并且是注册到QML的全局(具体是注册到一个 QQuickView 或者 engine)。以自定义一个 Name 类,类包括一个 data 属性为例。
类定义
需要暴露给QML访问的类需要有特殊的定义:
/*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 处理,编译时出现“无法识别的符号”错误
Q_PROPERTY(QString data READ data WRITE setData NOTIFY dataChanged)
这一行代码定义了暴露给QML访问的接口,这里我们提供的对象是一个 QString,READ 接口是一个名为 data 的函数, WRITE 接口是一个名为 setData 的函数,NOTIFY 接口用于通知的绑定,只有设置了 NOTIFY 接口,QML 才能自动与 C++ 中的属性同步。这里的命名方式最好与默认的统一。
类实现
/*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 中,必须判断数据是否更新,只有当数据真正改变时才发出信号,否则有无限递归的风险。
注册到上下文
然后是实例化一个 Name 类并注册到上下文。注册需要在读取.qml
文件之前完成。
/*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 中读取
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 定义时的命名。
使用 QList 作为 Model
更常见的需求是,暴露一组对象的属性,并通过 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 接口)。
注册类型到QML上下文中
当前的 Name 类已经完备的实现了注册到 QML 上下文中的接口,所以可以在 C++ 中调用
qmlRegisterType<Name>("com.myapp.name", 1, 0, "Name");
//包名,大版本号,小版本号,类型名
进行注册,在 QML 文件中加入引用后就可以直接使用 Name 类以及其属性。
import com.myapp.name 1.0
Name {
data: "Foo"
}
封装一组数据暴露给 QML
与直接放入 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")));
在 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修改属性
}
}
}
}
网友评论