这篇讲两种模型: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界面会立即跟着增删。
演示代码
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界面大致相同。
演示代码
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 }
}
}
模型类分头文件和实现两部分:
<span>#<span>ifndef</span> ITEMMODEL_H</span>
<span>#<span>define</span> ITEMMODEL_H</span>
<span>#<span>include</span> <span><QAbstractListModel></span></span>
<span>#<span>include</span> <span><QStringList></span></span>
<span>#<span>include</span> <span><QVector></span></span>
<span>struct</span> <span>Item</span> {
QString name;
QString color;
QString des;
};
<span>class</span> <span>ItemModel</span> : <span>public</span> QAbstractListModel
{
<span>Q_OBJECT
<span>Q_PROPERTY</span><span>(<span>int</span> count READ count NOTIFY countChanged)</span>
<span>public</span>:
enum FruitRoles {</span>
NameRole = Qt::UserRole + <span>1</span>,
ColorRole,
DesRole
};
<span><span>explicit</span> <span>ItemModel</span><span>(QObject *parent = <span>nullptr</span>)</span></span>;
<span><span>int</span> <span>rowCount</span><span>(<span>const</span> QModelIndex &parent = QModelIndex())</span> <span>const</span> <span>override</span></span>;
<span>QVariant <span>data</span><span>(<span>const</span> QModelIndex &index, <span>int</span> role = Qt::DisplayRole)</span> <span>const</span> <span>override</span></span>;
<span>QHash<<span>int</span>, QByteArray> <span>roleNames</span><span>()</span> <span>const</span> <span>override</span></span>;
<span><span>int</span> <span>count</span><span>()</span> <span>const</span></span>;
<span>// 供 QML 的增删按钮调用:必须包在 begin/end 之间,Repeater 才会同步增删委托</span>
<span>Q_INVOKABLE <span>void</span> <span>addItem</span><span>()</span></span>;
<span>Q_INVOKABLE <span>void</span> <span>removeLastItem</span><span>()</span></span>;
signals:
<span><span>void</span> <span>countChanged</span><span>()</span></span>;
<span>private</span>:
QVector<Item> m_items;
};
<span>#<span>endif</span></span>
<span>#<span>include</span> <span>"itemmodel.h"</span></span>
ItemModel::<span>ItemModel</span>(QObject *parent)
: <span>QAbstractListModel</span>(parent)
{
<span>// 初始化一些示例数据</span>
m_items.<span>append</span>({<span>"Item1"</span>, <span>"#3498db"</span>, <span>"from C++"</span>});
m_items.<span>append</span>({<span>"Item2"</span>, <span>"#e74c3c"</span>, <span>"from C++"</span>});
m_items.<span>append</span>({<span>"Item3"</span>, <span>"#2ecc71"</span>, <span>"from C++"</span>});
m_items.<span>append</span>({<span>"Item4"</span>, <span>"#f39c12"</span>, <span>"from C++"</span>});
}
<span><span>int</span> <span>ItemModel::rowCount</span><span>(<span>const</span> QModelIndex &parent)</span> <span>const</span>
</span>{
<span>return</span> parent.<span>isValid</span>() ? <span>0</span> : m_items.<span>size</span>();
}
<span>QVariant <span>ItemModel::data</span><span>(<span>const</span> QModelIndex &index, <span>int</span> role)</span> <span>const</span>
</span>{
<span>if</span> (!index.<span>isValid</span>() || index.<span>row</span>() >= m_items.<span>size</span>())
<span>return</span> <span>QVariant</span>();
<span>const</span> Item &item = m_items.<span>at</span>(index.<span>row</span>());
<span>switch</span> (role) {
<span>case</span> NameRole:
<span>return</span> item.name;
<span>case</span> ColorRole:
<span>return</span> item.color;
<span>case</span> DesRole:
<span>return</span> item.des;
<span>default</span>:
<span>return</span> <span>QVariant</span>();
}
}
<span>QHash<<span>int</span>, QByteArray> <span>ItemModel::roleNames</span><span>()</span> <span>const</span>
</span>{
QHash<<span>int</span>, QByteArray> roles;
roles[NameRole] = <span>"name"</span>;
roles[ColorRole] = <span>"color"</span>;
roles[DesRole] = <span>"des"</span>;
<span>return</span> roles;
}
<span><span>int</span> <span>ItemModel::count</span><span>()</span> <span>const</span>
</span>{
<span>return</span> m_items.<span>size</span>();
}
<span><span>void</span> <span>ItemModel::addItem</span><span>()</span>
</span>{
<span>static</span> <span>const</span> QStringList colors = {<span>"#3498db"</span>, <span>"#e74c3c"</span>, <span>"#2ecc71"</span>, <span>"#f39c12"</span>};
<span>const</span> <span>int</span> row = m_items.<span>size</span>();
<span>beginInsertRows</span>(<span>QModelIndex</span>(), row, row);
m_items.<span>append</span>({<span>QString</span>(<span>"Item%1"</span>).<span>arg</span>(row + <span>1</span>),
colors.<span>at</span>(row % colors.<span>size</span>()),
<span>QStringLiteral</span>(<span>"from C++"</span>)});
<span>endInsertRows</span>();
<span>emit <span>countChanged</span><span>()</span></span>;
}
<span><span>void</span> <span>ItemModel::removeLastItem</span><span>()</span>
</span>{
<span>if</span> (m_items.<span>isEmpty</span>())
<span>return</span>;
<span>const</span> <span>int</span> row = m_items.<span>size</span>() - <span>1</span>;
<span>beginRemoveRows</span>(<span>QModelIndex</span>(), row, row);
m_items.<span>removeAt</span>(row);
<span>endRemoveRows</span>();
<span>emit <span>countChanged</span><span>()</span></span>;
}
最后在 main.cpp 里把实例挂到上下文,QML 侧才有一个叫 itemModel 的对象可用:
<span>// 注册C++类型到QML</span>
<span>qmlRegisterType</span><ItemModel>(<span>"ItemModel"</span>, <span>1</span>, <span>0</span>, <span>"ItemModel"</span>);
<span>// 创建模型实例并设置到上下文</span>
ItemModel itemModel;
engine.<span>rootContext</span>()-><span>setContextProperty</span>(<span>"itemModel"</span>, &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
从角色名、增删通知到选型标准一次讲清,还点出 Repeater 一次全量创建委托的性能坑。适合正在用 QML 列表、纠结 ListModel 还是 C++ 自定义模型的开发者对照参考。