第 9 课:QAbstractListModel 给 ListView 提供数据
更新: 10/23/2026 字数: 0 字 时长: 0 分钟
上一课 ListModel 是 QML 里写死的数据。真实项目里,数据通常在 C++ 里,比如数据库、网络、业务对象。
这一课我们把 C++ 的列表模型接到 QML 的 ListView 上。
用 QAbstractListModel 驱动 ListView 的数据展示与增删
第一步:写 C++ 模型类
新建 FruitModel,继承 QAbstractListModel。
fruitmodel.h:
#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;
};
#endiffruitmodel.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:
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:
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
告诉视图有多少行。
int FruitModel::rowCount(const QModelIndex &parent) constparent 有效时返回 0,因为这是扁平列表,不是树。
2. data
根据 index.row() 和 role 返回当前行的数据。
case NameRole: return fruit.name;
case PriceRole: return fruit.price;视图会调用这个函数取每一行的每个角色。
3. roleNames
这是最关键的一步。它把 C++ 的角色 ID 映射成 QML 委托里能直接用的名字。
{ NameRole, "name" },
{ PriceRole, "price" }有了它,QML 委托里才能写:
text: name
text: price + " 元"如果没有 roleNames,QML 委托里就访问不到这些字段。
4. beginInsertRows / endInsertRows
增删行必须包在这对函数之间。
beginInsertRows(QModelIndex(), m_fruits.size(), m_fruits.size());
m_fruits.append(...);
endInsertRows();beginInsertRows 和 endInsertRows 之间的区别:
- 先调
beginInsertRows告诉视图“我要插入了” - 再真正改
m_fruits - 最后调
endInsertRows让视图更新
顺序错了会导致视图崩溃或显示错乱。
删除用 beginRemoveRows / endRemoveRows。
5. 修改某一行
修改已有行要用 dataChanged 信号:
emit dataChanged(index(row), index(row), { NameRole, PriceRole });不发出这个信号,视图不会刷新那一行。
6. Q_INVOKABLE 暴露增删方法
Q_INVOKABLE void addFruit(...);
Q_INVOKABLE void removeFruit(int row);QML 里就能直接 fruitModel.addFruit(...)、fruitModel.removeFruit(index)。
和 QML ListModel 的对比
| 方面 | QML ListModel | C++ QAbstractListModel |
|---|---|---|
| 适合场景 | 简单、静态或少量数据 | 大量数据、业务逻辑、数据库 |
| 性能 | 一般 | 好,支持懒加载 |
| 增删改 | 直接 append / remove | 必须 begin/end 成对使用 |
| 角色名 | ListElement 字段名 | roleNames() 映射 |
| 扩展性 | 弱 | 强,可接任意 C++ 数据源 |
你的动手练习
练习 1
给 FruitModel 加一个 clear() 方法,清空所有数据。
提示:
beginResetModel();
m_fruits.clear();
endResetModel();QML 里加一个“清空”按钮调用它。
练习 2
给 FruitModel 加一个 updatePrice(int row, int price) 方法,修改某一行的价格。
要求:
- 改完后发出
dataChanged - QML 里每行加一个“改价”按钮,点击把价格加 1
练习 3
在 QML 里显示总数量:
Label {
text: "共 " + fruitModel.rowCount() + " 种水果"
}思考:这样写,添加或删除后会自动更新吗?如果不会,怎么改?
提示:rowCount() 是普通方法,不是 Q_PROPERTY。可以加一个 count 属性,在增删时发信号。
练习 4(思考题)
如果 addFruit 里忘了调 beginInsertRows 和 endInsertRows,直接 m_fruits.append(...),会发生什么?为什么?