如果一行数据有多个字段,光靠 JS 数组也可以做,但每次增删都得自己造一个新数组再赋回去。
这篇讲两种模型:QML 自带的ListModel,以及从 C++ 继承 QAbstractListModel 写出来的自定义模型。两者都能直接给 Repeater 组件用,也都自带增删通知,界面会跟着数据自动更新。
结构化模型要解决两件事
字段怎么给QML读
ListModel 的角色名来自 ListElement 的属性名;C++ 模型则由 roleNames() 决定。两边在委托里的写法一致:model.角色名,比如 model.name、model.color。
增删怎么通知界面
ListModel 的追加、删除操作在内部已经实现了,调用即可。C++ 模型没有这层便利,改数据前后必须自己把操作包在 beginInsertRows、endInsertRows 这类成对调用之间,Repeater 才知道该加一份委托了。
解释一下角色
委托里写的 model.name,name是模型给每行数据起的角色名,相当于列名。同一行里可以有任意多个角色,model.角色名 就是"取这一行的某一列"。ListModel 的角色名来自 ListElement 的属性名,C++ 模型来自 roleNames(),两边都只是给同一件事起名字。
Demo 1 ListModel

委托用 model.name 这一组写法取字段,支持增加/减少操作,QML界面会立即跟着增删。
演示代码
qml
import QtQuick
import QtQuick.Layouts
FadeInAnimation {
readonly property var colors: ["#3498db", "#e74c3c", "#2ecc71", "#f39c12"]
readonly property int maxCount: 8
// 提到外面,方便操作条直接调用 append / remove
ListModel {
id: listModel
ListElement { name: "Item1"; color: "#3498db"; des: "model.des" }
ListElement { name: "Item2"; color: "#e74c3c"; des: "model.des" }
ListElement { name: "Item3"; color: "#2ecc71"; des: "model.des" }
ListElement { name: "Item4"; color: "#f39c12"; des: "model.des" }
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 12
// ... 省略标题组件 TitleSeparator ...
CountBar {
info: "count: " + listModel.count
canIncrease: listModel.count < maxCount
canDecrease: listModel.count > 0
onIncreaseRequested: listModel.append({
name: "Item" + (listModel.count + 1),
color: colors[listModel.count % colors.length],
des: "model.des"
})
onDecreaseRequested: listModel.remove(listModel.count - 1)
}
Repeater {
model: listModel
delegate: ItemRect {
itemColor: model.color
itemText: model.name
itemDes: model.des
}
}
Item { Layout.fillHeight: true }
}
}
关键逻辑解析
ListModel 的角色名就是 ListElement 的属性名,声明了 name / color / des 三个字段,委托里就能写 model.name 这一组;增删走现成的 listModel.append({...}) 和 remove(index),变更通知由框架内部发出,Repeater 自动补上或撤掉那份委托,这正是它比手写数组模型省事的地方,增删一行不用把整个列表推倒重建。
两个写法要点:模型要提到外层并给 id,操作条才调得到它的方法;ListElement 的值只能是常量,需要动态生成的值(比如按下标取色)得通过 append 从 JS 传进去。
Demo 2 C++ 自定义模型

这里换成 C++ 侧的数据,一个继承 QAbstractListModel 的 ItemModel,"增加/减少"操作调用的是模型接口中带 Q_INVOKABLE 声明的方法,QML界面大致相同。
演示代码
qml
import QtQuick
import QtQuick.Layouts
FadeInAnimation {
readonly property int maxCount: 8
// 先把根上下文的 C++ 模型捕获成文档根属性:context property 只能被本文档的
// 内联对象解析,跨文件组件(CountBar)实例上的绑定读不到它,会得到 null
readonly property var dataModel: itemModel
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 12
// ... 省略标题组件 TitleSeparator ...
CountBar {
info: "rowCount: " + dataModel.count
canIncrease: dataModel.count < maxCount
canDecrease: dataModel.count > 0
onIncreaseRequested: dataModel.addItem()
onDecreaseRequested: dataModel.removeLastItem()
}
Repeater {
model: dataModel
delegate: ItemRect {
itemColor: model.color
itemText: model.name
itemDes: model.des
}
}
Item { Layout.fillHeight: true }
}
}
模型类分头文件和实现两部分:
cpp
#ifndef ITEMMODEL_H
#define ITEMMODEL_H
#include <QAbstractListModel>
#include <QStringList>
#include <QVector>
struct Item {
QString name;
QString color;
QString des;
};
class ItemModel : public QAbstractListModel
{
Q_OBJECT
Q_PROPERTY(int count READ count NOTIFY countChanged)
public:
enum FruitRoles {
NameRole = Qt::UserRole + 1,
ColorRole,
DesRole
};
explicit ItemModel(QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override;
int count() const;
// 供 QML 的增删按钮调用:必须包在 begin/end 之间,Repeater 才会同步增删委托
Q_INVOKABLE void addItem();
Q_INVOKABLE void removeLastItem();
signals:
void countChanged();
private:
QVector<Item> m_items;
};
#endif
cpp
#include "itemmodel.h"
ItemModel::ItemModel(QObject *parent)
: QAbstractListModel(parent)
{
// 初始化一些示例数据
m_items.append({"Item1", "#3498db", "from C++"});
m_items.append({"Item2", "#e74c3c", "from C++"});
m_items.append({"Item3", "#2ecc71", "from C++"});
m_items.append({"Item4", "#f39c12", "from C++"});
}
int ItemModel::rowCount(const QModelIndex &parent) const
{
return parent.isValid() ? 0 : m_items.size();
}
QVariant ItemModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() >= m_items.size())
return QVariant();
const Item &item = m_items.at(index.row());
switch (role) {
case NameRole:
return item.name;
case ColorRole:
return item.color;
case DesRole:
return item.des;
default:
return QVariant();
}
}
QHash<int, QByteArray> ItemModel::roleNames() const
{
QHash<int, QByteArray> roles;
roles[NameRole] = "name";
roles[ColorRole] = "color";
roles[DesRole] = "des";
return roles;
}
int ItemModel::count() const
{
return m_items.size();
}
void ItemModel::addItem()
{
static const QStringList colors = {"#3498db", "#e74c3c", "#2ecc71", "#f39c12"};
const int row = m_items.size();
beginInsertRows(QModelIndex(), row, row);
m_items.append({QString("Item%1").arg(row + 1),
colors.at(row % colors.size()),
QStringLiteral("from C++")});
endInsertRows();
emit countChanged();
}
void ItemModel::removeLastItem()
{
if (m_items.isEmpty())
return;
const int row = m_items.size() - 1;
beginRemoveRows(QModelIndex(), row, row);
m_items.removeAt(row);
endRemoveRows();
emit countChanged();
}
最后在 main.cpp 里把实例挂到上下文,QML 侧才有一个叫 itemModel 的对象可用:
cpp
// 注册C++类型到QML
qmlRegisterType<ItemModel>("ItemModel", 1, 0, "ItemModel");
// 创建模型实例并设置到上下文
ItemModel itemModel;
engine.rootContext()->setContextProperty("itemModel", &itemModel);
关键逻辑解析
C++ 模型要交代三件事:
roleNames() 把角色枚举映射成 QML 里的名字(委托才能写 model.name),枚举值从 Qt::UserRole + 1 起、避开内置角色;
rowCount() 先判父节点是否有效,列表模型没有子行,父索引有效就返回 0;data() 按传入的角色返回对应字段,越界索引优先处理。
增删方法必须把操作包在 beginInsertRows、endInsertRows(或 beginRemoveRows、endRemoveRows)之间,这对成调用就是发给视图的变更通知,漏掉的话数据确实变了,界面却一动不动。
QML 侧还有两处细节:
readonly property var dataModel: itemModel 这一句声明是必要的,不能省,跨文件组件的绑定读不到注入的上下文属性,会获取到null;count 属性用 Q_PROPERTY 暴露、每次增删后 emit countChanged(),界面上的绑定才会刷新。
两种模型怎么选
| 维度 | ListModel |
C++ 自定义模型 |
|---|---|---|
| 定义在哪 | QML 里直接声明 | C++ 类,继承 QAbstractListModel |
| 角色名来源 | ListElement 的属性名 |
roleNames() 的映射表 |
| 委托取数据 | model.角色名 |
model.角色名 |
| 增删接口 | 现成的 append / remove |
自己写的 Q_INVOKABLE 方法 |
| 变更通知 | 框架已处理 | 必须自己用 begin / end 包裹 |
| 适合什么 | 数据量小、纯 QML 项目 | 数据来自 C++ 侧、行数大、要接后端 |
判断依据主要是数据的来源和体量。
数据只停留在页面中,且数据量不大时,ListModel 是最佳选择。数据本来就在 C++ 侧(文件、数据库、网络请求),或者行数上千、需要自己做分页和缓存,那就写 C++ 模型,把 roleNames()、rowCount()、data() 这些接口重新实现,剩下在 QML 里和在用 ListModel 时没什么区别。
最后一条和模型选择无关、但同样重要:Repeater 会把所有委托一次全建出来 ,模型多少行就建多少份实例。几十上百行无所谓,上千行就该换成 ListView 这类视图组件,它只为可见的那几行创建委托,滚出去就回收。Repeater 的位置更适合"固定几行、一起显示"的场景。
已验证环境:
- Qt 版本:Qt 6.8.2 / Qt 6.11.1
- 操作系统:Windows 11
- GitHub:QML-Minimal-Demos/qml_repeater