# C++模型
集成C++和QML的最常见方法之一是通过 模型(models) 。 模型(model) 向 视图(view) 提供数据,例如ListViews
、GridView
、PathViews
和其他采用模型并为模型中的每个条目创建代理实例的视图。该视图足够智能,可以仅在这些实例可见或者在缓存范围中时创建它们。这使得拥有数以万计条目的大型模型成为可能,但它仍然能有一个非常流畅的用户界面。代理就类似于一个模板,用于呈现模型条目的数据。总而言之:视图使用代理作为模板呈现模型中的条目,模型是视图中数据的提供者。
当不想使用C++时,也可以在纯QML中定义模型。有几种方法可以为视图提供模型。对于来自C++的数据或大量数据的处理,C++模型比这种纯QML的方法更适合。但通常只需要几个条目,那么这些QML模型则非常适合。
ListView {
// 使用一个整形作为模型
model: 5
delegate: Text { text: 'index: ' + index }
}
ListView {
// 使用一个JS数组作为模型
model: ['A', 'B', 'C', 'D', 'E']
delegate: Text { 'Char['+ index +']: ' + modelData }
}
ListView {
// 使用一个动态QML列表模型作为模型
model: ListModel {
ListElement { char: 'A' }
ListElement { char: 'B' }
ListElement { char: 'C' }
ListElement { char: 'D' }
ListElement { char: 'E' }
}
delegate: Text { 'Char['+ index +']: ' + model.char }
}
QML视图知道如何处理这些不同类型的模型。 对于来自C++世界的模型,视图期望遵循特定的协议。 该协议在QAbstractItemModel
中定义的API中定义,描述了动态的行为。 该API是为在桌面小部件世界中驱动视图而开发的,并且足够灵活,可以作为树、多列表格、列表的基础。 在QML中,通常使用API的列表的变体QAbstractTableModel
,或者对于TableView
元素使用API的表的变体QAbstractItemModel
。 API包含一些必须实现的函数和一些扩展模型功能的可选函数。 可选部分主要处理用于更改、添加或删除数据这样的动态用例。
# 简单模型
一个典型的QML C++模型派生自QAbstractListModel
并至少实现了data
和rowCount
函数。 在下面的示例中,将使用QColor
类提供的一系列SVG颜色名称,并使用模型显示它们。 数据存储在数据容器QList<QString>
中。
DataEntryModel
派生自QAbstractListModel
,并实现了强制要求必须实现的函数。 这里可以忽略rowCount
的父节点,因为父节点仅在树模型中使用。 QModelIndex
类提供单元格的行和列信息,用于视图检索数据。 该视图从模型中以行/列和基于角色的方式提取信息。 QAbstractListModel
在QtCore
中定义,但QColor
在QtGui
中定义。 这就是为什么要有额外的QtGui
依赖。 对于QML应用程序,可以依赖QtGui
,但通常不应依赖QtWidgets
。
#ifndef DATAENTRYMODEL_H
#define DATAENTRYMODEL_H
#include <QtCore>
#include <QtGui>
class DataEntryModel : public QAbstractListModel
{
Q_OBJECT
public:
explicit DataEntryModel(QObject *parent = 0);
~DataEntryModel();
public: // QAbstractItemModel interface
virtual int rowCount(const QModelIndex &parent) const;
virtual QVariant data(const QModelIndex &index, int role) const;
private:
QList<QString> m_data;
};
#endif // DATAENTRYMODEL_H
在实现方面,最复杂的部分是data函数。 首先需要进行范围检查以确保获得了有效的索引。 然后检查是否支持显示角色(display role)。 模型中的每个项目都可以具有多个显示角色,定义所包含数据的各个方面。 Qt::DisplayRole
是视图要求的默认文本角色。 在Qt中,定义了一小部分可以使用的默认角色,但为了清晰起见,模型通常会定义自己的角色。 在该示例中,所有不包含显示角色的调用都将被忽略,并返回默认值QVariant()
。
#include "dataentrymodel.h"
DataEntryModel::DataEntryModel(QObject *parent)
: QAbstractListModel(parent)
{
// 使用颜色名称列表初始化数据(QList<QString>)
m_data = QColor::colorNames();
}
DataEntryModel::~DataEntryModel()
{
}
int DataEntryModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
// 返回数据的数量
return m_data.count();
}
QVariant DataEntryModel::data(const QModelIndex &index, int role) const
{
// 索引返回请求的行和列信息
// 忽略列信息,只使用行信息
int row = index.row();
// 行的边界检测
if(row < 0 || row >= m_data.count()) {
return QVariant();
}
// 一个模型可以为不同角色返回数据
// 默认的角色是展示角色(display role).
// 可以在QML中通过"model.display"访问
switch(role) {
case Qt::DisplayRole:
// 返回指定行的颜色名
// Qt自动将其转换成QVariant类型
return m_data.value(row);
}
// 视图要求其他数据,只需返回一个空的QVariant
return QVariant();
}
下一步是使用qmlRegisterType
向QML注册模型。 这是在main.cpp
文件中,在加载QML文件之前完成注册。
#include <QtGui>
#include <QtQml>
#include "dataentrymodel.h"
int main(int argc, char *argv[])
{
QGuiApplication app(argc, argv);
// 注册DataEntryModel类型
// 在URL"org.example"、版本1.0下
// 在名称"DataEntryModel"下
qmlRegisterType<DataEntryModel>("org.example", 1, 0, "DataEntryModel");
QQmlApplicationEngine engine;
engine.load(QUrl(QStringLiteral("qrc:/main.qml")));
return app.exec();
}
现在可以使用QML导入语句import org.example 1.0
访问DataEntryModel
,并像使用其他QML项一样使用它DataEntryModel {}
。
在这个例子中使用它来显示一个简单的颜色条目列表。
import org.example 1.0
ListView {
id: view
anchors.fill: parent
model: DataEntryModel {}
delegate: ListDelegate {
// use the defined model role "display"
text: model.display
}
highlight: ListHighlight { }
}
ListDelegate
是一种自定义类型,用于显示一些文本。 ListHighlight
只是一个矩形。 将这部分代码抽取出来以保持示例紧凑。
视图现在可以使用C++模型和模型的显示属性展示字符串列表。 它仍然非常简单,但已经可以在QML中使用。 通常,数据是从模型外部提供的,模型将充当视图的接口。
提示
要展示数据是表格而不是列表,请使用QAbstractTableModel
。 与实现QAbstractListModel
相比,唯一区别是还必须提供columnCount
方法。
# 更复杂点的数据
实际上,模型数据通常要比上面展示的复杂得多。 因此需要自定义角色,以便视图可以通过属性查询其他数据。 例如,该模型不仅可以提供十六进制字符串形式的颜色,还可以提供来自HSV颜色模型的色调、饱和度和亮度,如QML中的model.hue
、model.saturation
和model.brightness
.
#ifndef ROLEENTRYMODEL_H
#define ROLEENTRYMODEL_H
#include <QtCore>
#include <QtGui>
class RoleEntryModel : public QAbstractListModel
{
Q_OBJECT
public:
// 定义要使用的角色名称
enum RoleNames {
NameRole = Qt::UserRole,
HueRole = Qt::UserRole+2,
SaturationRole = Qt::UserRole+3,
BrightnessRole = Qt::UserRole+4
};
explicit RoleEntryModel(QObject *parent = 0);
~RoleEntryModel();
// QAbstractItemModel接口
public:
virtual int rowCount(const QModelIndex &parent) const override;
virtual QVariant data(const QModelIndex &index, int role) const override;
protected:
// 返回QML使用的角色映射
virtual QHash<int, QByteArray> roleNames() const override;
private:
QList<QColor> m_data;
QHash<int, QByteArray> m_roleNames;
};
#endif // ROLEENTRYMODEL_H
在头文件中,添加了用于QML的角色映射。 现在,当QML尝试访问模型中的属性(例如"model.name")时,列表视图将查找"name"的映射并使用NameRole
在模型中询问数据。 用户定义的角色应该以Qt::UserRole
开头,并且需要对每个模型都是唯一的。
#include "roleentrymodel.h"
RoleEntryModel::RoleEntryModel(QObject *parent)
: QAbstractListModel(parent)
{
// 设置名称到角色名的哈希容器 (QHash<int, QByteArray>)
// model.name, model.hue, model.saturation, model.brightness
m_roleNames[NameRole] = "name";
m_roleNames[HueRole] = "hue";
m_roleNames[SaturationRole] = "saturation";
m_roleNames[BrightnessRole] = "brightness";
// 将颜色名称作为QColor附加到数据列表 (QList<QColor>)
for(const QString& name : QColor::colorNames()) {
m_data.append(QColor(name));
}
}
RoleEntryModel::~RoleEntryModel()
{
}
int RoleEntryModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_data.count();
}
QVariant RoleEntryModel::data(const QModelIndex &index, int role) const
{
int row = index.row();
if(row < 0 || row >= m_data.count()) {
return QVariant();
}
const QColor& color = m_data.at(row);
qDebug() << row << role << color;
switch(role) {
case NameRole:
// 以十六进制字符串形式返回颜色名称 (model.name)
return color.name();
case HueRole:
// 返回颜色的HUE (model.hue)
return color.hueF();
case SaturationRole:
// 返回颜色的饱和度 (model.saturation)
return color.saturationF();
case BrightnessRole:
// 返回颜色的亮度(model.brightness)
return color.lightnessF();
}
return QVariant();
}
QHash<int, QByteArray> RoleEntryModel::roleNames() const
{
return m_roleNames;
}
现在,在类的实现中,仅有两个地方发生变化。 首先,在初始化时。使用QColor数据类型初始化数据列表。 此外,定义角色名称映射,使其可供QML访问。 该映射可以在稍后的::roleNames
函数中返回。
第二个变化在::data
函数中。 扩展switch语句以使能覆盖其他角色(例如色调、饱和度、亮度)。 无法从颜色返回SVG名称,这是因为颜色可以是任何颜色但是SVG名称却是有限的,所以跳过这个角色。 存储名称需要创建一个结构体struct { QColor, QString }
,这才能识别命名的颜色。
注册类型后,可以在用户界面中使用模型及其条目。
ListView {
id: view
anchors.fill: parent
model: RoleEntryModel {}
focus: true
delegate: ListDelegate {
text: 'hsv(' +
Number(model.hue).toFixed(2) + ',' +
Number(model.saturation).toFixed() + ',' +
Number(model.brightness).toFixed() + ')'
color: model.name
}
highlight: ListHighlight { }
}
将返回的类型转换为JS数字类型,以便能够使用定点表示法格式化数字。 此代码也可以在没有调用Number(例如普通的 model.saturation.toFixed(2)
)的情况下工作。 选择哪种格式取决于对传入数据的信任程度。
# 动态数据
动态数据涵盖了从模型中插入、删除和清除数据的各个方面。 QAbstractListModel
要求了在删除或插入条目时出现某种行为。 这些行为需要在调用操作前后通过信号来表示。 例如,要将一行插入到模型中,首先需要发出beginInsertRows
信号,然后操作数据,最后发出endInsertRows
信号。
将以下函数添加到头文件中。 这些函数使用Q_INVOKABLE
声明,以便能够在QML中调用它们。 另一种方法是将它们声明为public的槽。
// 在index位置插入一个颜色 (以0为开始, count-1为结尾)
Q_INVOKABLE void insert(int index, const QString& colorValue);
// 使用追加在最后插入一个颜色
Q_INVOKABLE void append(const QString& colorValue);
// 通过index移除一个颜色
Q_INVOKABLE void remove(int index);
// 清理整改模型 (e.g. reset)
Q_INVOKABLE void clear();
此外,定义了一个count
属性来获取模型的尺寸和一个get
方法来获取给定索引处的颜色。 当想从QML中迭代模型内容时,这很有用。
// 给定模型的尺寸
Q_PROPERTY(int count READ count NOTIFY countChanged)
// 给定index的颜色
Q_INVOKABLE QColor get(int index);
insert的实现首先检查边界以及给定值是否有效,只有这样才开始插入数据。
void DynamicEntryModel::insert(int index, const QString &colorValue)
{
if(index < 0 || index > m_data.count()) {
return;
}
QColor color(colorValue);
if(!color.isValid()) {
return;
}
// 视图协议(begin => 操作 => end)
emit beginInsertRows(QModelIndex(), index, index);
m_data.insert(index, color);
emit endInsertRows();
// 更新数量属性
emit countChanged(m_data.count());
}
追加颜色非常简单。可以根据模型的尺寸来重用insert函数。
void DynamicEntryModel::append(const QString &colorValue)
{
insert(count(), colorValue);
}
remove与insert类似,但它根据remove的操作协议调用。
void DynamicEntryModel::remove(int index)
{
if(index < 0 || index >= m_data.count()) {
return;
}
emit beginRemoveRows(QModelIndex(), index, index);
m_data.removeAt(index);
emit endRemoveRows();
// do not forget to update our count property
emit countChanged(m_data.count());
}
辅助函数count
极其简单,它只返回数据计数。 get
函数也非常简单。
QColor DynamicEntryModel::get(int index)
{
if(index < 0 || index >= m_data.count()) {
return QColor();
}
return m_data.at(index);
}
需要注意:只返回QML可以理解的值。 如果它不是QML基本类型或者已知类型之一,则需要先使用qmlRegisterType
或qmlRegisterUncreatableType
注册该类型。 如果用户不能在QML中实例化自己的对象,则使用qmlRegisterUncreatableType
注册。
现在可以在QML中使用模型并在模型中插入、附加、删除条目。 这是一个小示例,它允许用户输入颜色名称或颜色十六进制值,然后将颜色附加到模型并显示在列表视图中。 代理上的红色圆圈表示允许用户从模型中删除此条目。 在条目被删除后,模型会通知列表视图并更新其内容。
上面使用的是QML代码,可以在本章的资源中找到完整的源代码。 该示例使用了QtQuick.Controls
和QtQuick.Layout
模块使代码更加紧凑。 这些控件模块在Qt Quick中提供了一组与桌面相关的UI元素,并且布局模块提供了一些非常有用的布局管理器。
import QtQuick
import QtQuick.Window
import QtQuick.Controls
import QtQuick.Layouts
// our module
import org.example 1.0
Window {
visible: true
width: 480
height: 480
Background { // a dark background
id: background
}
// our dyanmic model
DynamicEntryModel {
id: dynamic
onCountChanged: {
// we print out count and the last entry when count is changing
print('new count: ' + dynamic.count)
print('last entry: ' + dynamic.get(dynamic.count - 1))
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 8
ScrollView {
Layout.fillHeight: true
Layout.fillWidth: true
ListView {
id: view
// set our dynamic model to the views model property
model: dynamic
delegate: ListDelegate {
required property var model
width: ListView.view.width
// construct a string based on the models proeprties
text: 'hsv(' +
Number(model.hue).toFixed(2) + ',' +
Number(model.saturation).toFixed() + ',' +
Number(model.brightness).toFixed() + ')'
// sets the font color of our custom delegates
color: model.name
onClicked: {
// make this delegate the current item
view.currentIndex = model.index
view.focus = true
}
onRemove: {
// remove the current entry from the model
dynamic.remove(model.index)
}
}
highlight: ListHighlight { }
// some fun with transitions :-)
add: Transition {
// applied when entry is added
NumberAnimation {
properties: "x"; from: -view.width;
duration: 250; easing.type: Easing.InCirc
}
NumberAnimation { properties: "y"; from: view.height;
duration: 250; easing.type: Easing.InCirc
}
}
remove: Transition {
// applied when entry is removed
NumberAnimation {
properties: "x"; to: view.width;
duration: 250; easing.type: Easing.InBounce
}
}
displaced: Transition {
// applied when entry is moved
// (e.g because another element was removed)
SequentialAnimation {
// wait until remove has finished
PauseAnimation { duration: 250 }
NumberAnimation { properties: "y"; duration: 75
}
}
}
}
}
TextEntry {
id: textEntry
onAppend: function (color) {
// called when the user presses return on the text field
// or clicks the add button
dynamic.append(color)
}
onUp: {
// called when the user presses up while the text field is focused
view.decrementCurrentIndex()
}
onDown: {
// same for down
view.incrementCurrentIndex()
}
}
}
}
模型视图编程是Qt中较为复杂的开发任务之一。 作为普通应用程序开发人员,它是必须实现接口的极少数类之一,除了这些极少数的类,其他类都可以正常使用。 模型的构图应该总是从QML端开始,应该设想用户将如何在QML中使用模型。 为此,首先使用ListModel
创建原型,并了解它在QML中的最佳工作方式,这通常是一个好主意。 在定义QML API时也应该如此设想用户操作。 使数据从C++到QML不仅是跨越了一个技术边界,它也是从命令式编程到声明式编程的编程范式的变化。 所以要同时为受到挫折和收获惊喜时刻做好准备 😃 。
← 常见的Qt类 使用C++扩展QML →