跳转到内容

第 9 课:QAbstractListModel 给 ListView 提供数据 ​

更新: 10/23/2026 字数: 0 字 时长: 0 分钟

上一课 ListModel 是 QML 里写死的数据。真实项目里,数据通常在 C++ 里,比如数据库、网络、业务对象。
这一课我们把 C++ 的列表模型接到 QML 的 ListView 上。

用 QAbstractListModel 驱动 ListView 的数据展示与增删 ​

第一步:写 C++ 模型类 ​

新建 FruitModel,继承 QAbstractListModel。

fruitmodel.h:

cpp
#ifndef FRUITMODEL_H
#define FRUITMODEL_H

#include <QAbstractListModel>
#include <QML_ELEMENT>
#include <QList>
#include <QString>

struct Fruit {
    QString name;
    int price;
};

class FruitModel : public QAbstractListModel
{
    Q_OBJECT
    QML_ELEMENT

public:
    enum Roles {
        NameRole = Qt::UserRole + 1,
        PriceRole
    };

    explicit FruitModel(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;

    Q_INVOKABLE void addFruit(const QString &name, int price);
    Q_INVOKABLE void removeFruit(int row);

private:
    QList<Fruit> m_fruits;
};

#endif

fruitmodel.cpp:

cpp
#include "fruitmodel.h"

FruitModel::FruitModel(QObject *parent)
    : QAbstractListModel(parent)
{
    m_fruits = {
        { "苹果", 5 },
        { "香蕉", 3 },
        { "橙子", 4 }
    };
}

int FruitModel::rowCount(const QModelIndex &parent) const
{
    if (parent.isValid())
        return 0;
    return m_fruits.size();
}

QVariant FruitModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid() || index.row() < 0 || index.row() >= m_fruits.size())
        return {};

    const Fruit &fruit = m_fruits.at(index.row());

    switch (role) {
    case NameRole:
        return fruit.name;
    case PriceRole:
        return fruit.price;
    default:
        return {};
    }
}

QHash<int, QByteArray> FruitModel::roleNames() const
{
    return {
        { NameRole, "name" },
        { PriceRole, "price" }
    };
}

void FruitModel::addFruit(const QString &name, int price)
{
    beginInsertRows(QModelIndex(), m_fruits.size(), m_fruits.size());
    m_fruits.append({ name, price });
    endInsertRows();
}

void FruitModel::removeFruit(int row)
{
    if (row < 0 || row >= m_fruits.size())
        return;

    beginRemoveRows(QModelIndex(), row, row);
    m_fruits.removeAt(row);
    endRemoveRows();
}

第二步:加到 CMake ​

在 CMakeLists.txt 的 qt_add_qml_module 里,把这两个文件加到 SOURCES:

cmake
qt_add_qml_module(appMyApp
    URI MyApp
    VERSION 1.0
    QML_FILES
        Main.qml
    SOURCES
        backend.h
        backend.cpp
        fruitmodel.h
        fruitmodel.cpp
)

第三步:在 QML 里使用 ​

Main.qml:

qml
import QtQuick
import QtQuick.Controls
import MyApp

ApplicationWindow {
    width: 420
    height: 480
    visible: true
    title: "第九课"

    FruitModel {
        id: fruitModel
    }

    Column {
        anchors.fill: parent
        anchors.margins: 12
        spacing: 8

        Row {
            spacing: 8

            TextField {
                id: nameInput
                width: 140
                placeholderText: "水果名"
            }

            TextField {
                id: priceInput
                width: 80
                placeholderText: "价格"
                inputMethodHints: Qt.ImhDigitsOnly
            }

            Button {
                text: "添加"
                onClicked: {
                    var price = parseInt(priceInput.text)
                    if (nameInput.text !== "" && !isNaN(price)) {
                        fruitModel.addFruit(nameInput.text, price)
                        nameInput.text = ""
                        priceInput.text = ""
                    }
                }
            }
        }

        ListView {
            width: parent.width
            height: 300
            model: fruitModel
            spacing: 4
            clip: true

            delegate: Rectangle {
                width: ListView.view.width
                height: 48
                color: index % 2 === 0 ? "#f5f5f5" : "white"
                border.color: "#ddd"

                Text {
                    anchors.left: parent.left
                    anchors.leftMargin: 12
                    anchors.verticalCenter: parent.verticalCenter
                    text: name
                    font.pixelSize: 18
                }

                Text {
                    anchors.right: parent.right
                    anchors.rightMargin: 60
                    anchors.verticalCenter: parent.verticalCenter
                    text: price + " 元"
                    color: "tomato"
                }

                Button {
                    anchors.right: parent.right
                    anchors.rightMargin: 8
                    anchors.verticalCenter: parent.verticalCenter
                    text: "删"
                    onClicked: fruitModel.removeFruit(index)
                }
            }
        }
    }
}

运行后:

  • 列表显示初始三条水果
  • 输入名字和价格,点“添加”,列表末尾新增一行
  • 每行点“删”,删除该行

关键点讲解 ​

1. rowCount ​

告诉视图有多少行。

cpp
int FruitModel::rowCount(const QModelIndex &parent) const

parent 有效时返回 0,因为这是扁平列表,不是树。

2. data ​

根据 index.row() 和 role 返回当前行的数据。

cpp
case NameRole: return fruit.name;
case PriceRole: return fruit.price;

视图会调用这个函数取每一行的每个角色。

3. roleNames ​

这是最关键的一步。它把 C++ 的角色 ID 映射成 QML 委托里能直接用的名字。

cpp
{ NameRole, "name" },
{ PriceRole, "price" }

有了它,QML 委托里才能写:

qml
text: name
text: price + " 元"

如果没有 roleNames,QML 委托里就访问不到这些字段。

4. beginInsertRows / endInsertRows ​

增删行必须包在这对函数之间。

cpp
beginInsertRows(QModelIndex(), m_fruits.size(), m_fruits.size());
m_fruits.append(...);
endInsertRows();

beginInsertRows 和 endInsertRows 之间的区别:

  • 先调 beginInsertRows 告诉视图“我要插入了”
  • 再真正改 m_fruits
  • 最后调 endInsertRows 让视图更新

顺序错了会导致视图崩溃或显示错乱。

删除用 beginRemoveRows / endRemoveRows。

5. 修改某一行 ​

修改已有行要用 dataChanged 信号:

cpp
emit dataChanged(index(row), index(row), { NameRole, PriceRole });

不发出这个信号,视图不会刷新那一行。

6. Q_INVOKABLE 暴露增删方法 ​

cpp
Q_INVOKABLE void addFruit(...);
Q_INVOKABLE void removeFruit(int row);

QML 里就能直接 fruitModel.addFruit(...)、fruitModel.removeFruit(index)。

和 QML ListModel 的对比 ​

方面QML ListModelC++ QAbstractListModel
适合场景简单、静态或少量数据大量数据、业务逻辑、数据库
性能一般好,支持懒加载
增删改直接 append / remove必须 begin/end 成对使用
角色名ListElement 字段名roleNames() 映射
扩展性弱强,可接任意 C++ 数据源

你的动手练习 ​

练习 1 ​

给 FruitModel 加一个 clear() 方法,清空所有数据。

提示:

cpp
beginResetModel();
m_fruits.clear();
endResetModel();

QML 里加一个“清空”按钮调用它。

练习 2 ​

给 FruitModel 加一个 updatePrice(int row, int price) 方法,修改某一行的价格。

要求:

  • 改完后发出 dataChanged
  • QML 里每行加一个“改价”按钮,点击把价格加 1

练习 3 ​

在 QML 里显示总数量:

qml
Label {
    text: "共 " + fruitModel.rowCount() + " 种水果"
}

思考:这样写,添加或删除后会自动更新吗?如果不会,怎么改?

提示:rowCount() 是普通方法,不是 Q_PROPERTY。可以加一个 count 属性,在增删时发信号。

练习 4(思考题) ​

如果 addFruit 里忘了调 beginInsertRows 和 endInsertRows,直接 m_fruits.append(...),会发生什么?为什么?