QML Repeater 数据模型:ListModel 与 C++ QAbstractListModel

如果一行数据有多个字段,光靠 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++ 侧的数据,一个继承 QAbstractListModelItemModel,"增加/减少"操作调用的是模型接口中带 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 的位置更适合"固定几行、一起显示"的场景。


已验证环境

相关推荐
Quz1 小时前
QML Repeater 基础用法:整数、JS 数组、字符串数组三种模型
qt
江湖人称菠萝包8 小时前
【Qt】《Qt 5.9 C++开发指南》笔记-Chapter3-Qt类库概述
笔记·qt·qt5
qq_344920569 小时前
Qt事件过滤器
开发语言·c++·qt
sunriver20009 小时前
【Qt】由于找不到 Qt6Core5Compatd.dll
qt·qt6core5compatd
qq_344920569 小时前
Qt 鼠标双击QLabel全屏显示图像
c++·qt
江湖人称菠萝包12 小时前
【Qt】《Qt 5.9 C++开发指南》笔记-Chapter4-常用界面设计组件
笔记·qt·qt5
实心儿儿12 小时前
Qt — QtCreator 创建项目
qt
Quz1 天前
QML 常见的四种导航布局
qt
江湖人称菠萝包1 天前
【Qt】《Qt 5.9 C++开发指南》笔记-Chapter2-GUI应用程序设计基础
笔记·qt·qt5