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

文章来源声明: 原文作者:Quz; 来源站点:掘金; 原文链接:https://juejin.cn/post/7686688669903110194; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

从角色名、增删通知到选型标准一次讲清,还点出 Repeater 一次全量创建委托的性能坑。适合正在用 QML 列表、纠结 ListModel 还是 C++ 自定义模型的开发者对照参考。

如果一行数据有多个字段,光靠 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

4.gif

委托用 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++ 自定义模型

5.gif

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


已验证环境