Qt Model/View架构详解
重要程度 : ⭐⭐⭐⭐⭐
实战价值 : 处理复杂数据展示(表格、树形结构、列表)
学习目标 : 掌握Qt的Model/View设计模式,能够自定义Model和Delegate处理复杂数据展示需求
本篇要点: 掌握Qt的Model/View设计模式的理论基础,为后续内容打下基础。
📚 目录
第一部分:基础理论 (第1-3章)
第1章 Model/View架构概述
- 1.1 什么是Model/View架构
- 1.2 Model/View的核心组件
- 1.3 Model/View的优势
第2章 Model基础
- 2.1 QAbstractItemModel详解
- 2.2 模型索引(QModelIndex)
- 2.3 数据角色(Roles)
- 2.4 Model的信号和槽
第3章 View基础
- 3.1 QAbstractItemView详解
- 3.2 选择模式和选择行为
- 3.3 编辑触发器
- 3.4 视图的信号与槽
第1章 Model/View架构概述
1.1 什么是Model/View架构
1.1.1 MVC vs MVP vs Model/View
在软件架构设计中,有多种将数据与界面分离的模式:
MVC(Model-View-Controller)模式
-
Model(模型):管理数据和业务逻辑
-
View(视图):显示数据给用户
-
Controller(控制器):处理用户输入,协调Model和View
用户输入 → Controller → 更新 Model → 通知 View → 显示更新
MVP(Model-View-Presenter)模式
- Model(模型):数据和业务逻辑
- View(视图):被动视图,由Presenter控制
- Presenter(展示器):View和Model之间的中介
Qt的Model/View模式
Qt采用了简化的MVC模式,将Controller的功能融入到View和Delegate中:
- Model(模型):负责数据的存储、访问和修改
- View(视图):负责数据的显示和用户交互
- Delegate(委托):负责数据的编辑和自定义渲染
| 设计模式 | 组件数量 | 特点 | 适用场景 |
|---|---|---|---|
| MVC | 3个(Model, View, Controller) | 经典模式,职责明确 | Web应用 |
| MVP | 3个(Model, View, Presenter) | View完全被动 | Android开发 |
| Model/View | 2-3个(Model, View, [Delegate]) | 简化的MVC,更灵活 | Qt桌面应用 |
1.1.2 Model/View架构的核心思想
核心原则:数据与显示分离
┌─────────────────────────────────────────┐
│ Application Data │
│ (存储在Model中) │
└──────────────┬──────────────────────────┘
│
│ data() / setData()
│
┌──────────▼──────────┐
│ │
┌───▼────┐ ┌───▼────┐
│ View 1 │ │ View 2 │
│ 表格 │ │ 列表 │
└────────┘ └────────┘
关键特性:
- 单一数据源:数据只存储在Model中
- 多视图同步:多个View可以显示同一个Model的数据
- 自动更新:Model数据变化时,所有View自动刷新
- 职责清晰:Model管理数据,View管理显示
1.1.3 Model/View vs 传统Widget
Qt提供了两种数据展示方式:
传统Widget方式(如QListWidget, QTableWidget, QTreeWidget)
cpp
// 使用QListWidget(传统方式)
QListWidget *listWidget = new QListWidget;
listWidget->addItem("Apple");
listWidget->addItem("Banana");
listWidget->addItem("Cherry");
// 数据直接存储在Widget中
// 如果需要另一个视图显示相同数据,必须复制数据
Model/View方式(如QListView + QStringListModel)
cpp
// 使用Model/View方式
QStringListModel *model = new QStringListModel;
model->setStringList({"Apple", "Banana", "Cherry"});
// 同一个Model可以被多个View共享
QListView *listView1 = new QListView;
listView1->setModel(model);
QListView *listView2 = new QListView;
listView2->setModel(model); // 共享同一数据源
// 修改Model,两个View都会自动更新
model->insertRow(0);
model->setData(model->index(0), "Mango");
对比总结:
| 特性 | 传统Widget | Model/View |
|---|---|---|
| 数据存储 | 数据直接存储在Widget中 | 数据存储在独立的Model中 |
| 多视图支持 | ❌ 需要复制数据 | ✅ 多个View共享一个Model |
| 数据同步 | ❌ 需要手动同步 | ✅ 自动同步 |
| 自定义能力 | ⚠️ 有限的自定义 | ✅ 高度可定制 |
| 性能 | ⚠️ 数据量大时较差 | ✅ 更好的内存管理 |
| 使用难度 | ✅ 简单易用 | ⚠️ 学习曲线较陡 |
| 适用场景 | 简单的静态数据展示 | 复杂数据、动态更新、多视图 |
1.1.4 为什么需要Model/View架构
使用场景示例:
场景1:需要多个视图显示同一数据
cpp
// 员工管理系统:同时显示表格和树形结构
QStandardItemModel *employeeModel = new QStandardItemModel;
// 表格视图
QTableView *tableView = new QTableView;
tableView->setModel(employeeModel);
// 树形视图(按部门分组)
QTreeView *treeView = new QTreeView;
treeView->setModel(employeeModel);
// 修改Model,两个视图都自动更新
场景2:大数据量展示
cpp
// 传统方式:将100万条数据加载到QListWidget会占用大量内存
// Model/View方式:只在需要时才加载可见数据(懒加载)
场景3:复杂的数据编辑逻辑
cpp
// 使用Delegate可以为不同列提供不同的编辑器
// 例如:姓名用文本框,性别用下拉框,日期用日期选择器
Model/View架构的必要性:
- ✅ 解耦数据与界面:修改数据逻辑不影响界面,修改界面不影响数据
- ✅ 提高复用性:同一Model可以用于不同的View
- ✅ 便于测试:可以单独测试Model的数据逻辑
- ✅ 更好的性能:懒加载、按需渲染
- ✅ 灵活的定制:通过Delegate实现复杂的渲染和编辑
1.2 Model/View架构的组成部分
1.2.1 Model(模型):数据的抽象表示
职责:
- 存储和管理数据
- 提供数据访问接口
- 通知View数据变化
核心接口:
cpp
class QAbstractItemModel : public QObject {
public:
// 获取数据
virtual QVariant data(const QModelIndex &index, int role) const = 0;
// 设置数据
virtual bool setData(const QModelIndex &index, const QVariant &value, int role);
// 获取行数
virtual int rowCount(const QModelIndex &parent = QModelIndex()) const = 0;
// 获取列数
virtual int columnCount(const QModelIndex &parent = QModelIndex()) const = 0;
signals:
// 数据变化信号
void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight);
};
简单示例:
cpp
class SimpleListModel : public QAbstractListModel {
QStringList m_data;
public:
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
return m_data.count();
}
QVariant data(const QModelIndex &index, int role) const override {
if (role == Qt::DisplayRole) {
return m_data.at(index.row());
}
return QVariant();
}
};
1.2.2 View(视图):数据的可视化呈现
职责:
- 显示Model中的数据
- 处理用户交互(选择、滚动)
- 触发编辑操作
常用视图类:
cpp
// 1. 列表视图
QListView *listView = new QListView;
listView->setModel(model);
listView->setViewMode(QListView::IconMode); // 图标模式
// 2. 表格视图
QTableView *tableView = new QTableView;
tableView->setModel(model);
tableView->horizontalHeader()->setStretchLastSection(true);
// 3. 树形视图
QTreeView *treeView = new QTreeView;
treeView->setModel(model);
treeView->expandAll();
View的核心方法:
cpp
// 设置模型
void setModel(QAbstractItemModel *model);
// 获取选择模型
QItemSelectionModel *selectionModel() const;
// 设置委托
void setItemDelegate(QAbstractItemDelegate *delegate);
1.2.3 Delegate(委托):编辑和渲染的控制
职责:
- 自定义数据的渲染(如何显示)
- 提供编辑器(如何编辑)
- 控制编辑器的行为
核心方法:
cpp
class QStyledItemDelegate : public QAbstractItemDelegate {
public:
// 绘制单元格
virtual void paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index) const;
// 创建编辑器
virtual QWidget *createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const;
// 设置编辑器数据
virtual void setEditorData(QWidget *editor,
const QModelIndex &index) const;
// 将编辑器数据写回Model
virtual void setModelData(QWidget *editor,
QAbstractItemModel *model,
const QModelIndex &index) const;
};
自定义Delegate示例:
cpp
// 为不同列提供不同的编辑器
class CustomDelegate : public QStyledItemDelegate {
public:
QWidget *createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override {
if (index.column() == 0) {
// 第0列:文本编辑器
return new QLineEdit(parent);
} else if (index.column() == 1) {
// 第1列:下拉框
QComboBox *combo = new QComboBox(parent);
combo->addItems({"Male", "Female"});
return combo;
}
return QStyledItemDelegate::createEditor(parent, option, index);
}
};
1.2.4 三者之间的协作关系
交互流程图:
┌──────────────────────────────────────────────────────┐
│ Application │
└──────────┬───────────────────────────────────────────┘
│
│ 创建和配置
│
┌──────▼───────┐
│ Model │ ◄──────────┐
│ (数据管理) │ │
└──────┬───────┘ │
│ │
│ data() │ setData()
│ dataChanged信号 │
│ │
┌──────▼───────┐ ┌─────┴──────┐
│ View │ │ Delegate │
│ (数据显示) │ ◄────┤ (渲染/编辑) │
└──────┬───────┘ └─────┬──────┘
│ │
│ 用户交互 │ paint()
│ 触发编辑 │ createEditor()
└────────────────────┘
完整示例:三者协同工作:
cpp
#include <QApplication>
#include <QTableView>
#include <QStandardItemModel>
#include <QStyledItemDelegate>
#include <QSpinBox>
// 1. 自定义Delegate:为年龄列提供SpinBox编辑器
class AgeDelegate : public QStyledItemDelegate {
public:
QWidget *createEditor(QWidget *parent,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override {
QSpinBox *editor = new QSpinBox(parent);
editor->setMinimum(0);
editor->setMaximum(150);
return editor;
}
void setEditorData(QWidget *editor, const QModelIndex &index) const override {
int value = index.model()->data(index, Qt::EditRole).toInt();
QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
spinBox->setValue(value);
}
void setModelData(QWidget *editor,
QAbstractItemModel *model,
const QModelIndex &index) const override {
QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
spinBox->interpretText();
int value = spinBox->value();
model->setData(index, value, Qt::EditRole);
}
};
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
// 2. 创建Model并填充数据
QStandardItemModel *model = new QStandardItemModel(3, 2);
model->setHorizontalHeaderLabels({"Name", "Age"});
model->setItem(0, 0, new QStandardItem("Alice"));
model->setItem(0, 1, new QStandardItem("25"));
model->setItem(1, 0, new QStandardItem("Bob"));
model->setItem(1, 1, new QStandardItem("30"));
model->setItem(2, 0, new QStandardItem("Charlie"));
model->setItem(2, 1, new QStandardItem("28"));
// 3. 创建View并设置Model
QTableView *view = new QTableView;
view->setModel(model);
// 4. 为年龄列设置Delegate
AgeDelegate *ageDelegate = new AgeDelegate;
view->setItemDelegateForColumn(1, ageDelegate);
view->resize(400, 300);
view->show();
return app.exec();
}
运行效果:
- 显示一个3行2列的表格
- 姓名列:普通文本编辑
- 年龄列:使用SpinBox编辑器(双击单元格时出现)
- 修改数据会自动更新到Model
1.3 Qt提供的Model/View类体系
1.3.1 抽象基类
1. QAbstractItemModel
- 所有Model类的基类
- 支持列表、表格、树形结构
- 必须实现的方法:
rowCount(),columnCount(),data(),index(),parent()
2. QAbstractListModel
- 列表模型基类(一维数据)
- 简化了树形接口
- 必须实现:
rowCount(),data()
3. QAbstractTableModel
- 表格模型基类(二维数据)
- 无层级结构
- 必须实现:
rowCount(),columnCount(),data()
继承关系:
QObject
└─ QAbstractItemModel
├─ QAbstractListModel
│ ├─ QStringListModel
│ └─ (自定义列表模型)
│
├─ QAbstractTableModel
│ └─ (自定义表格模型)
│
├─ QStandardItemModel
├─ QFileSystemModel
└─ QSqlTableModel
1.3.2 具体模型类
1. QStringListModel(字符串列表模型)
cpp
QStringListModel *model = new QStringListModel;
model->setStringList({"Apple", "Banana", "Cherry"});
QListView *view = new QListView;
view->setModel(model);
2. QStandardItemModel(通用模型)
- 最常用的便捷模型
- 支持列表、表格、树形结构
- 使用QStandardItem存储数据
cpp
QStandardItemModel *model = new QStandardItemModel(4, 2);
model->setItem(0, 0, new QStandardItem("Item 1-1"));
model->setItem(0, 1, new QStandardItem("Item 1-2"));
3. QFileSystemModel(文件系统模型)
cpp
QFileSystemModel *model = new QFileSystemModel;
model->setRootPath(QDir::currentPath());
QTreeView *tree = new QTreeView;
tree->setModel(model);
tree->setRootIndex(model->index(QDir::currentPath()));
4. QSqlTableModel(数据库表模型)
cpp
QSqlTableModel *model = new QSqlTableModel;
model->setTable("employees");
model->select();
QTableView *view = new QTableView;
view->setModel(model);
1.3.3 视图类
| 视图类 | 用途 | 适用数据结构 |
|---|---|---|
| QListView | 列表/图标视图 | 一维数据 |
| QTableView | 表格视图 | 二维数据 |
| QTreeView | 树形视图 | 层级数据 |
| QColumnView | 列视图(类似macOS Finder) | 层级数据 |
| QHeaderView | 表头视图 | 作为其他视图的表头 |
视图类的共同特性:
cpp
// 所有视图都继承自QAbstractItemView
class QAbstractItemView : public QAbstractScrollArea {
public:
void setModel(QAbstractItemModel *model);
void setItemDelegate(QAbstractItemDelegate *delegate);
QItemSelectionModel *selectionModel() const;
void setSelectionMode(SelectionMode mode);
void setEditTriggers(EditTriggers triggers);
};
1.3.4 委托类
1. QItemDelegate(旧版委托)
- Qt 4.x时代的委托类
- 不推荐使用
2. QStyledItemDelegate(推荐使用)
- Qt 5+推荐的委托类
- 使用当前样式绘制
- 更好的主题适配
cpp
class CustomDelegate : public QStyledItemDelegate {
void paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override {
// 自定义绘制逻辑
}
};
1.3.5 Model/View类继承关系图
┌─────────────────────────────────────────────────────────┐
│ Model类层次结构 │
└─────────────────────────────────────────────────────────┘
QObject
│
└─ QAbstractItemModel ◄─────────────────┐
│ │
├─ QAbstractListModel │ 抽象基类
│ ├─ QStringListModel │
│ └─ (CustomListModel) │
│ │
├─ QAbstractTableModel │
│ ├─ QSqlTableModel │
│ └─ (CustomTableModel) │
│ │
├─ QStandardItemModel │ 具体实现类
├─ QFileSystemModel │
├─ QSqlQueryModel │
└─ QAbstractProxyModel │
├─ QSortFilterProxyModel │
└─ QIdentityProxyModel │
┌─────────────────────────────────────────────────────────┐
│ View类层次结构 │
└─────────────────────────────────────────────────────────┘
QAbstractScrollArea
│
└─ QAbstractItemView
├─ QListView
├─ QTableView
├─ QTreeView
├─ QColumnView
└─ QHeaderView
┌─────────────────────────────────────────────────────────┐
│ Delegate类层次结构 │
└─────────────────────────────────────────────────────────┘
QObject
│
└─ QAbstractItemDelegate
├─ QItemDelegate (已过时)
└─ QStyledItemDelegate (推荐)
1.4 Model/View架构的优势
1.4.1 数据与显示分离
优势详解:
问题场景:使用传统Widget
cpp
// 问题:数据和UI耦合在一起
QListWidget *listWidget = new QListWidget;
listWidget->addItem("Task 1");
listWidget->addItem("Task 2");
// 如果要将数据保存到文件,需要遍历Widget
for (int i = 0; i < listWidget->count(); ++i) {
QString text = listWidget->item(i)->text();
// 保存到文件...
}
解决方案:使用Model/View
cpp
// 数据独立存储在Model中
class TaskModel : public QAbstractListModel {
QList<Task> m_tasks; // 业务数据
public:
// 保存数据只需要操作m_tasks
void saveToFile(const QString &filename) {
QFile file(filename);
if (file.open(QIODevice::WriteOnly)) {
QDataStream out(&file);
out << m_tasks;
}
}
void loadFromFile(const QString &filename) {
QFile file(filename);
if (file.open(QIODevice::ReadOnly)) {
QDataStream in(&file);
in >> m_tasks;
// 通知View刷新
emit dataChanged(index(0), index(m_tasks.count()-1));
}
}
};
1.4.2 多视图共享同一数据
实战示例:日程管理应用
cpp
#include <QApplication>
#include <QStandardItemModel>
#include <QListView>
#include <QTableView>
#include <QTreeView>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
// 1. 创建共享Model
QStandardItemModel *model = new QStandardItemModel;
model->setHorizontalHeaderLabels({"Event", "Time", "Priority"});
// 添加数据
QList<QStandardItem*> row1;
row1 << new QStandardItem("Meeting")
<< new QStandardItem("10:00 AM")
<< new QStandardItem("High");
model->appendRow(row1);
QList<QStandardItem*> row2;
row2 << new QStandardItem("Lunch")
<< new QStandardItem("12:00 PM")
<< new QStandardItem("Medium");
model->appendRow(row2);
// 2. 创建三个不同的视图显示同一数据
QListView *listView = new QListView;
listView->setModel(model);
QTableView *tableView = new QTableView;
tableView->setModel(model);
QTreeView *treeView = new QTreeView;
treeView->setModel(model);
// 3. 布局
QWidget window;
QHBoxLayout *layout = new QHBoxLayout;
QVBoxLayout *leftLayout = new QVBoxLayout;
leftLayout->addWidget(new QLabel("List View:"));
leftLayout->addWidget(listView);
QVBoxLayout *middleLayout = new QVBoxLayout;
middleLayout->addWidget(new QLabel("Table View:"));
middleLayout->addWidget(tableView);
QVBoxLayout *rightLayout = new QVBoxLayout;
rightLayout->addWidget(new QLabel("Tree View:"));
rightLayout->addWidget(treeView);
layout->addLayout(leftLayout);
layout->addLayout(middleLayout);
layout->addLayout(rightLayout);
window.setLayout(layout);
window.resize(900, 400);
window.show();
return app.exec();
}
效果:
- 三个视图同时显示相同的数据
- 在任一视图中编辑数据,其他视图自动同步更新
- 只需要维护一份数据
1.4.3 更好的性能和内存管理
懒加载(Lazy Loading)
cpp
// 传统方式:一次性加载所有数据
QListWidget *widget = new QListWidget;
for (int i = 0; i < 1000000; ++i) {
widget->addItem(QString("Item %1").arg(i)); // 占用大量内存
}
// Model/View方式:按需加载
class LazyModel : public QAbstractListModel {
int m_totalCount = 1000000;
QMap<int, QString> m_cache; // 只缓存已访问的数据
public:
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
return m_totalCount;
}
QVariant data(const QModelIndex &index, int role) const override {
if (role == Qt::DisplayRole) {
int row = index.row();
if (!m_cache.contains(row)) {
// 仅在需要时生成数据
m_cache[row] = QString("Item %1").arg(row);
}
return m_cache[row];
}
return QVariant();
}
};
性能对比:
| 场景 | 传统Widget | Model/View |
|---|---|---|
| 100万条数据初始化 | 数秒,占用GB内存 | 毫秒级,按需加载 |
| 滚动浏览 | 已全部加载在内存 | 仅渲染可见项 |
| 数据更新 | 需要刷新整个Widget | 只更新变化的部分 |
1.4.4 高度可定制化
示例:自定义进度条委托
cpp
class ProgressBarDelegate : public QStyledItemDelegate {
public:
void paint(QPainter *painter,
const QStyleOptionViewItem &option,
const QModelIndex &index) const override {
int progress = index.data().toInt();
// 绘制背景
painter->fillRect(option.rect, Qt::lightGray);
// 绘制进度条
QRect progressRect = option.rect;
progressRect.setWidth(option.rect.width() * progress / 100);
painter->fillRect(progressRect, Qt::green);
// 绘制文本
QString text = QString("%1%").arg(progress);
painter->drawText(option.rect, Qt::AlignCenter, text);
}
};
// 使用
QTableView *view = new QTableView;
view->setItemDelegateForColumn(2, new ProgressBarDelegate);
1.4.5 适用场景分析
何时使用传统Widget:
- ✅ 简单的静态列表(如设置选项)
- ✅ 数据量小(< 100项)
- ✅ 不需要自定义渲染
- ✅ 快速原型开发
何时使用Model/View:
- ✅ 数据量大(> 1000项)
- ✅ 需要多个视图显示同一数据
- ✅ 数据频繁更新
- ✅ 需要自定义渲染或编辑器
- ✅ 数据来自数据库或网络
- ✅ 需要排序、过滤、搜索功能
决策树:
开始
│
├─ 数据量 < 100 且不需要定制?
│ └─ 是 → 使用传统Widget (QListWidget, QTableWidget)
│
└─ 否 → 是否需要以下功能?
│
├─ 多视图共享数据? ────┐
├─ 自定义渲染? ────────┤
├─ 大数据量? ──────────┤
├─ 数据来自外部源? ────┤
├─ 复杂的编辑器? ──────┤
└─ 排序/过滤? ─────────┤
│
有任一项 → 使用Model/View架构
实战建议:
-
学习路径:
- 先熟悉
QStandardItemModel(最简单的现成Model) - 再学习自定义
QAbstractTableModel - 最后掌握复杂的
QAbstractItemModel(树形结构)
- 先熟悉
-
常见组合:
- 简单列表:
QStringListModel+QListView - 通用表格:
QStandardItemModel+QTableView - 文件浏览:
QFileSystemModel+QTreeView - 数据库:
QSqlTableModel+QTableView
- 简单列表:
-
性能优化:
- 大数据量使用
canFetchMore()/fetchMore()实现懒加载 - 避免频繁调用
dataChanged(),批量更新 - 使用
QPersistentModelIndex保存需要长期持有的索引
- 大数据量使用
本章小结:
✅ Model/View是Qt中处理复杂数据展示的核心架构
✅ 核心思想是数据与显示分离 ,通过Model、View、Delegate三者协作
✅ Qt提供了丰富的基类和具体实现类,适用于不同场景
✅ 相比传统Widget,Model/View在性能、灵活性、可维护性上都有显著优势
✅ 选择合适的架构取决于数据量、复杂度和定制需求
第2章 核心概念:索引、角色与数据访问
2.1 模型索引(QModelIndex)
2.1.1 QModelIndex的作用和意义
什么是QModelIndex?
QModelIndex 是 Model/View 架构中用于定位和访问模型中数据项的核心类。它就像是数据项的"地址"或"坐标"。
类比理解:
- 就像书的页码+行号可以定位书中的某一行文字
- QModelIndex 通过行号+列号可以定位模型中的某个数据单元
cpp
// QModelIndex 的基本使用
QModelIndex index = model->index(2, 1); // 第2行,第1列
QVariant data = model->data(index, Qt::DisplayRole); // 获取该位置的数据
为什么需要QModelIndex?
在 Model/View 架构中,View 不直接访问数据,而是通过 QModelIndex 向 Model 请求数据:
View Model
│ │
│ 需要显示第3行第2列 │
│ ──index(3,2)─────────► │
│ │
│ ◄─返回QModelIndex───── │
│ │
│ 用这个索引获取数据 │
│ ──data(index)────────► │
│ │
│ ◄─返回QVariant数据──── │
QModelIndex的核心特性:
- 轻量级:QModelIndex 只是一个引用,不存储实际数据
- 临时性:索引可能随模型变化而失效
- 唯一性:每个数据项都有唯一的索引表示
2.1.2 索引的构成:row、column、internalPointer、model
QModelIndex 的内部结构:
cpp
class QModelIndex {
private:
int r; // 行号
int c; // 列号
void *i; // 内部指针(用于树形结构)
const QAbstractItemModel *m; // 所属的 Model
};
主要成员方法:
cpp
// 1. 获取行号(从0开始)
int row() const;
// 2. 获取列号(从0开始)
int column() const;
// 3. 获取内部指针(高级用法,用于树形模型)
void *internalPointer() const;
// 4. 获取所属的 Model
const QAbstractItemModel *model() const;
// 5. 获取父索引(树形结构中使用)
QModelIndex parent() const;
// 6. 获取兄弟索引
QModelIndex sibling(int row, int column) const;
// 7. 检查索引是否有效
bool isValid() const;
示例:索引的基本使用
cpp
#include <QStandardItemModel>
#include <QDebug>
void demonstrateQModelIndex() {
// 创建一个 3x2 的模型
QStandardItemModel model(3, 2);
// 设置数据
model.setItem(0, 0, new QStandardItem("A1"));
model.setItem(0, 1, new QStandardItem("A2"));
model.setItem(1, 0, new QStandardItem("B1"));
model.setItem(1, 1, new QStandardItem("B2"));
model.setItem(2, 0, new QStandardItem("C1"));
model.setItem(2, 1, new QStandardItem("C2"));
// 创建索引
QModelIndex index = model.index(1, 1); // 第1行第1列(B2)
// 访问索引的属性
qDebug() << "Row:" << index.row(); // 输出: Row: 1
qDebug() << "Column:" << index.column(); // 输出: Column: 1
qDebug() << "Data:" << model.data(index); // 输出: Data: QVariant(QString, "B2")
qDebug() << "Model:" << index.model(); // 输出: Model 的指针
qDebug() << "Is valid:" << index.isValid(); // 输出: Is valid: true
// 获取兄弟索引
QModelIndex sibling = index.sibling(2, 0); // 移动到第2行第0列(C1)
qDebug() << "Sibling data:" << model.data(sibling); // 输出: "C1"
}
internalPointer 的作用(树形模型专用):
在树形模型中,internalPointer 用于存储指向树节点的指针,以便快速访问父节点:
cpp
// 树形模型中的典型用法
QModelIndex TreeModel::index(int row, int col, const QModelIndex &parent) const {
TreeItem *parentItem = getItem(parent); // 获取父节点
TreeItem *childItem = parentItem->child(row); // 获取子节点
if (childItem)
// 将子节点指针存储在 internalPointer 中
return createIndex(row, col, childItem);
else
return QModelIndex();
}
2.1.3 有效索引 vs 无效索引
无效索引(Invalid Index):
无效索引表示"不指向任何数据项"的索引,类似于空指针。
创建无效索引的情况:
cpp
// 1. 默认构造的索引是无效的
QModelIndex index;
qDebug() << index.isValid(); // false
// 2. 使用 QModelIndex() 构造
QModelIndex invalidIndex = QModelIndex();
qDebug() << invalidIndex.isValid(); // false
// 3. 访问超出范围的索引
QStandardItemModel model(3, 2);
QModelIndex outOfRange = model.index(10, 10); // 超出范围
qDebug() << outOfRange.isValid(); // false
// 4. 对于列表/表格模型,parent() 返回无效索引
QModelIndex index = model.index(0, 0);
QModelIndex parentIndex = index.parent();
qDebug() << parentIndex.isValid(); // false(表格模型没有父节点)
无效索引的用途:
在 Model/View 中,无效索引有特殊含义:
| 场景 | 无效索引的含义 |
|---|---|
parent() 返回值 |
表示该项没有父项(顶层项) |
index() 的 parent 参数 |
表示获取顶层项的索引 |
| 错误处理 | 表示操作失败 |
示例:无效索引的判断
cpp
void checkIndexValidity(const QAbstractItemModel *model) {
// 获取一个索引
QModelIndex index = model->index(0, 0);
// 判断索引是否有效
if (index.isValid()) {
// 安全地访问数据
QString text = index.data().toString();
qDebug() << "Data:" << text;
} else {
qDebug() << "Invalid index!";
}
// 获取父索引
QModelIndex parentIndex = index.parent();
if (!parentIndex.isValid()) {
qDebug() << "This is a top-level item";
}
}
最佳实践:
cpp
// ✅ 好的做法:使用前检查索引有效性
void processIndex(const QModelIndex &index, QAbstractItemModel *model) {
if (!index.isValid()) {
qWarning() << "Invalid index!";
return;
}
// 安全地使用索引
QString data = model->data(index).toString();
// ...
}
// ❌ 坏的做法:不检查就使用
void badPractice(const QModelIndex &index, QAbstractItemModel *model) {
QString data = model->data(index).toString(); // 可能崩溃!
}
2.1.4 持久索引(QPersistentModelIndex)
问题场景:
普通的 QModelIndex 在模型结构改变时可能失效:
cpp
QStandardItemModel model(5, 2);
QModelIndex index = model.index(3, 0); // 第3行
// 删除第2行
model.removeRow(2);
// 此时 index 仍然指向行号3,但数据已经变了!
// 原来第3行的数据现在在第2行
qDebug() << index.row(); // 仍然是3,但指向的数据已经不对了
解决方案:QPersistentModelIndex
QPersistentModelIndex 会自动跟踪数据项,即使模型结构发生变化:
cpp
QStandardItemModel model(5, 2);
model.setItem(3, 0, new QStandardItem("Target"));
// 使用持久索引
QPersistentModelIndex persistentIndex = model.index(3, 0);
qDebug() << "Before:" << persistentIndex.row(); // 3
qDebug() << "Data:" << model.data(persistentIndex); // "Target"
// 删除第2行
model.removeRow(2);
// 持久索引自动更新
qDebug() << "After:" << persistentIndex.row(); // 2 (自动调整!)
qDebug() << "Data:" << model.data(persistentIndex); // "Target" (数据正确)
QPersistentModelIndex 的特性:
| 特性 | QModelIndex | QPersistentModelIndex |
|---|---|---|
| 生命周期 | 临时的,随时可能失效 | 持久的,自动跟踪 |
| 内存占用 | 极小(几个字节) | 较大(需要维护映射) |
| 适用场景 | 临时使用 | 需要长期保存 |
| 性能 | 极快 | 稍慢(需要更新映射) |
| 模型变化 | 不自动更新 | 自动更新 |
QPersistentModelIndex 使用示例:
cpp
#include <QStandardItemModel>
#include <QPersistentModelIndex>
#include <QDebug>
class BookmarkManager {
QList<QPersistentModelIndex> m_bookmarks; // 存储书签
public:
void addBookmark(const QModelIndex &index) {
// 转换为持久索引存储
m_bookmarks.append(QPersistentModelIndex(index));
qDebug() << "Bookmark added at row" << index.row();
}
void printBookmarks(QAbstractItemModel *model) {
qDebug() << "Current bookmarks:";
for (const QPersistentModelIndex &index : m_bookmarks) {
if (index.isValid()) {
QString data = model->data(index).toString();
qDebug() << " Row" << index.row() << ":" << data;
} else {
qDebug() << " (deleted)";
}
}
}
};
int main() {
QStandardItemModel model;
for (int i = 0; i < 5; ++i) {
model.setItem(i, 0, new QStandardItem(QString("Item %1").arg(i)));
}
BookmarkManager manager;
// 添加书签到第2行和第4行
manager.addBookmark(model.index(2, 0));
manager.addBookmark(model.index(4, 0));
qDebug() << "\nBefore deletion:";
manager.printBookmarks(&model);
// 删除第1行
model.removeRow(1);
qDebug() << "\nAfter deletion:";
manager.printBookmarks(&model); // 持久索引自动更新!
return 0;
}
输出:
Bookmark added at row 2
Bookmark added at row 4
Before deletion:
Current bookmarks:
Row 2 : Item 2
Row 4 : Item 4
After deletion:
Current bookmarks:
Row 1 : Item 2 // 行号自动从2变为1
Row 3 : Item 4 // 行号自动从4变为3
何时使用持久索引:
✅ 应该使用 QPersistentModelIndex:
- 需要长期保存索引(如书签、收藏夹)
- 在模型可能发生增删操作时保持引用
- 实现撤销/重做功能
- 跨信号槽传递索引
❌ 不需要使用 QPersistentModelIndex:
- 临时访问数据
- 在
data()或paint()等方法中 - 性能敏感的场景
2.1.5 索引的创建:createIndex()方法
在自定义 Model 中创建索引
Model 不应该直接构造 QModelIndex,而应使用 createIndex() 方法:
cpp
// QAbstractItemModel 提供的方法
QModelIndex createIndex(int row, int column, void *ptr = nullptr) const;
QModelIndex createIndex(int row, int column, quintptr id) const;
为什么要用 createIndex():
- 确保索引与模型正确关联
- 自动设置
model()指针 - 支持内部指针(树形结构)
列表/表格模型中的用法:
cpp
class SimpleTableModel : public QAbstractTableModel {
QVector<QVector<QString>> m_data; // 二维数据
public:
SimpleTableModel() {
// 初始化3x2的数据
m_data = {
{"A1", "A2"},
{"B1", "B2"},
{"C1", "C2"}
};
}
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
return parent.isValid() ? 0 : m_data.size();
}
int columnCount(const QModelIndex &parent = QModelIndex()) const override {
return parent.isValid() ? 0 : 2;
}
// 注意:QAbstractTableModel 已经实现了 index() 方法
// 但理解其原理很重要
QModelIndex index(int row, int col, const QModelIndex &parent) const override {
if (parent.isValid())
return QModelIndex(); // 表格模型没有子项
if (row < 0 || row >= m_data.size() || col < 0 || col >= 2)
return QModelIndex(); // 超出范围
// 对于简单模型,不需要 internalPointer
return createIndex(row, col);
}
QVariant data(const QModelIndex &index, int role) const override {
if (!index.isValid())
return QVariant();
if (role == Qt::DisplayRole) {
return m_data[index.row()][index.column()];
}
return QVariant();
}
};
树形模型中的用法(使用 internalPointer):
cpp
// 树节点类
struct TreeNode {
QString data;
TreeNode *parent = nullptr;
QList<TreeNode*> children;
~TreeNode() { qDeleteAll(children); }
};
class TreeModel : public QAbstractItemModel {
TreeNode *m_root;
public:
TreeModel() {
m_root = new TreeNode{"Root"};
// 构建简单树结构
TreeNode *child1 = new TreeNode{"Child 1", m_root};
TreeNode *child2 = new TreeNode{"Child 2", m_root};
m_root->children = {child1, child2};
TreeNode *grandchild = new TreeNode{"Grandchild", child1};
child1->children = {grandchild};
}
~TreeModel() { delete m_root; }
// 创建索引:关键是使用 internalPointer 存储节点指针
QModelIndex index(int row, int col, const QModelIndex &parent) const override {
if (!hasIndex(row, col, parent))
return QModelIndex();
TreeNode *parentNode = parent.isValid()
? static_cast<TreeNode*>(parent.internalPointer())
: m_root;
TreeNode *childNode = parentNode->children.at(row);
// 将节点指针存储在 internalPointer 中
return createIndex(row, col, childNode);
}
// 获取父索引:从 internalPointer 中恢复节点信息
QModelIndex parent(const QModelIndex &child) const override {
if (!child.isValid())
return QModelIndex();
TreeNode *childNode = static_cast<TreeNode*>(child.internalPointer());
TreeNode *parentNode = childNode->parent;
if (parentNode == m_root)
return QModelIndex(); // 根节点的子项没有父索引
// 需要找到 parentNode 在其父节点中的行号
TreeNode *grandparent = parentNode->parent;
int row = grandparent->children.indexOf(parentNode);
return createIndex(row, 0, parentNode);
}
int rowCount(const QModelIndex &parent) const override {
TreeNode *parentNode = parent.isValid()
? static_cast<TreeNode*>(parent.internalPointer())
: m_root;
return parentNode->children.size();
}
int columnCount(const QModelIndex &) const override {
return 1;
}
QVariant data(const QModelIndex &index, int role) const override {
if (!index.isValid() || role != Qt::DisplayRole)
return QVariant();
TreeNode *node = static_cast<TreeNode*>(index.internalPointer());
return node->data;
}
};
createIndex() 的两种重载:
cpp
// 1. 使用 void* 指针(树形模型常用)
QModelIndex createIndex(int row, int column, void *ptr) const;
// 2. 使用 quintptr 整数ID(可以存储整数标识)
QModelIndex createIndex(int row, int column, quintptr id) const;
// 示例
class IdBasedModel : public QAbstractListModel {
QHash<int, QString> m_dataById; // ID -> 数据
QList<int> m_idList; // 有序的ID列表
public:
QModelIndex index(int row, int col, const QModelIndex &parent) const override {
if (parent.isValid() || row < 0 || row >= m_idList.size())
return QModelIndex();
int itemId = m_idList.at(row);
// 将 ID 存储在索引中
return createIndex(row, col, static_cast<quintptr>(itemId));
}
QVariant data(const QModelIndex &index, int role) const override {
if (!index.isValid() || role != Qt::DisplayRole)
return QVariant();
// 从索引中恢复 ID
int itemId = static_cast<int>(index.internalId());
return m_dataById.value(itemId);
}
};
2.1.6 实战示例:索引的使用
完整示例:学生成绩管理系统
cpp
#include <QApplication>
#include <QTableView>
#include <QAbstractTableModel>
#include <QDebug>
#include <QPushButton>
#include <QVBoxLayout>
// 学生数据结构
struct Student {
QString name;
int math;
int english;
int science;
};
// 自定义表格模型
class StudentModel : public QAbstractTableModel {
QList<Student> m_students;
public:
StudentModel(QObject *parent = nullptr) : QAbstractTableModel(parent) {
// 初始化数据
m_students = {
{"Alice", 85, 90, 88},
{"Bob", 78, 82, 75},
{"Charlie", 92, 88, 95},
{"David", 68, 72, 70}
};
}
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
return parent.isValid() ? 0 : m_students.size();
}
int columnCount(const QModelIndex &parent = QModelIndex()) const override {
return parent.isValid() ? 0 : 4; // 姓名、数学、英语、科学
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
if (!index.isValid())
return QVariant();
const Student &student = m_students.at(index.row());
if (role == Qt::DisplayRole) {
switch (index.column()) {
case 0: return student.name;
case 1: return student.math;
case 2: return student.english;
case 3: return student.science;
}
} else if (role == Qt::BackgroundRole) {
// 成绩低于75分标红
if (index.column() > 0) {
int score = data(index, Qt::DisplayRole).toInt();
if (score < 75) {
return QColor(Qt::red).lighter(160);
}
}
}
return QVariant();
}
QVariant headerData(int section, Qt::Orientation orientation, int role) const override {
if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
QStringList headers = {"Name", "Math", "English", "Science"};
return headers.at(section);
}
return QAbstractTableModel::headerData(section, orientation, role);
}
// 计算某个学生的平均分
double getAverage(const QModelIndex &index) const {
if (!index.isValid() || index.row() >= m_students.size())
return 0.0;
const Student &s = m_students.at(index.row());
return (s.math + s.english + s.science) / 3.0;
}
// 获取某列的最高分索引
QModelIndex getTopScoreIndex(int column) const {
if (column <= 0 || column > 3)
return QModelIndex();
int maxScore = -1;
int maxRow = -1;
for (int row = 0; row < m_students.size(); ++row) {
QModelIndex idx = index(row, column);
int score = data(idx, Qt::DisplayRole).toInt();
if (score > maxScore) {
maxScore = score;
maxRow = row;
}
}
return index(maxRow, column);
}
};
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
// 创建模型
StudentModel *model = new StudentModel;
// 创建视图
QTableView *view = new QTableView;
view->setModel(model);
view->setSelectionBehavior(QAbstractItemView::SelectRows);
// 按钮:显示选中学生的平均分
QPushButton *avgButton = new QPushButton("Show Average");
QObject::connect(avgButton, &QPushButton::clicked, [=]() {
QModelIndex current = view->currentIndex();
if (current.isValid()) {
double avg = model->getAverage(current);
QString name = model->data(model->index(current.row(), 0)).toString();
qDebug() << name << "average:" << avg;
}
});
// 按钮:高亮数学最高分
QPushButton *topMathButton = new QPushButton("Highlight Top Math Score");
QObject::connect(topMathButton, &QPushButton::clicked, [=]() {
QModelIndex topIndex = model->getTopScoreIndex(1); // 数学列
if (topIndex.isValid()) {
view->setCurrentIndex(topIndex);
int score = model->data(topIndex).toInt();
QString name = model->data(model->index(topIndex.row(), 0)).toString();
qDebug() << "Top math score:" << name << "-" << score;
}
});
// 布局
QWidget window;
QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(view);
layout->addWidget(avgButton);
layout->addWidget(topMathButton);
window.setLayout(layout);
window.resize(500, 300);
window.show();
return app.exec();
}
示例要点:
-
索引的创建和使用:
index(row, column)创建索引data(index, role)通过索引获取数据
-
索引的有效性检查:
- 使用
isValid()确保索引有效
- 使用
-
索引的遍历:
- 通过循环创建不同的索引来遍历数据
-
索引的导航:
index(current.row(), 0)访问同一行的其他列
运行效果:
- 显示学生成绩表格
- 成绩低于75分的单元格背景标红
- 点击"Show Average"显示当前选中学生的平均分
- 点击"Highlight Top Math Score"自动选中数学最高分的单元格
本节小结:
✅ QModelIndex 是访问模型数据的核心工具
✅ 索引由 行号、列号、内部指针、所属模型 组成
✅ 使用 isValid() 判断索引有效性
✅ QPersistentModelIndex 用于长期保存索引引用
✅ 自定义模型中使用 createIndex() 创建索引
✅ 树形模型通过 internalPointer 存储节点信息
- QModelIndex的作用和意义
- 索引的构成:row、column、internalPointer、model
- 有效索引 vs 无效索引
- 持久索引(QPersistentModelIndex)
- 索引的创建:createIndex()方法
- 实战示例:索引的使用
2.2 数据角色(Qt::ItemDataRole)
2.2.1 什么是数据角色
数据角色的概念
在 Model/View 架构中,一个数据项(单元格)可以有多种不同的表现形式 。数据角色(ItemDataRole)定义了数据的用途和类型。
类比理解:
想象一个联系人条目:
- 显示角色:显示姓名 "张三"
- 编辑角色:编辑时显示完整信息
- 图标角色:显示头像图标
- 提示角色:鼠标悬停时显示详细信息 "张三 - 软件工程师"
- 背景角色:VIP用户显示金色背景
为什么需要数据角色?
一个数据项需要在不同场景下提供不同的数据:
cpp
QVariant MyModel::data(const QModelIndex &index, int role) const {
if (role == Qt::DisplayRole) {
return "显示的文本"; // View 显示时使用
} else if (role == Qt::EditRole) {
return actualValue; // 编辑时使用真实值
} else if (role == Qt::ToolTipRole) {
return "这是提示信息"; // 鼠标悬停时显示
}
return QVariant();
}
数据角色的工作流程:
View 需要显示数据
│
├─ 请求 DisplayRole → 获取 "100%" 显示给用户
├─ 请求 EditRole → 获取 100(数值)用于编辑
├─ 请求 BackgroundRole → 获取绿色背景
└─ 请求 ToolTipRole → 获取 "任务已完成"
2.2.2 常用角色详解
Qt 预定义了多个标准角色,下面逐一详解:
2.2.2.1 Qt::DisplayRole - 显示文本
作用:View 用来显示的主要文本内容
返回类型 :QString 或可转换为字符串的类型
示例:
cpp
QVariant data(const QModelIndex &index, int role) const override {
if (role == Qt::DisplayRole) {
// 返回要显示的文本
if (index.column() == 0) {
return QString("第%1行").arg(index.row() + 1);
} else if (index.column() == 1) {
// 数值格式化显示
double value = m_data[index.row()];
return QString::number(value, 'f', 2); // 保留2位小数
}
}
return QVariant();
}
实战技巧:
- 用于格式化显示(如日期、货币、百分比)
- 可以与 EditRole 不同(显示友好格式,编辑原始值)
2.2.2.2 Qt::EditRole - 编辑数据
作用:编辑器获取和设置数据时使用
返回类型:数据的原始类型(int, double, QString 等)
与 DisplayRole 的区别:
cpp
QVariant data(const QModelIndex &index, int role) const override {
if (index.column() == 0) { // 日期列
QDate date = m_dates[index.row()];
if (role == Qt::DisplayRole) {
// 显示:友好格式
return date.toString("yyyy年MM月dd日");
} else if (role == Qt::EditRole) {
// 编辑:标准格式,便于编辑器处理
return date;
}
}
return QVariant();
}
完整的编辑流程:
cpp
// 1. 读取数据:View/Delegate 调用 data(index, Qt::EditRole)
QVariant data(const QModelIndex &index, int role) const override {
if (role == Qt::EditRole) {
return m_values[index.row()]; // 返回原始值
}
// ...
}
// 2. 写入数据:Delegate 调用 setData(index, value, Qt::EditRole)
bool setData(const QModelIndex &index, const QVariant &value, int role) override {
if (role == Qt::EditRole) {
m_values[index.row()] = value.toInt();
emit dataChanged(index, index, {Qt::EditRole, Qt::DisplayRole});
return true;
}
return false;
}
2.2.2.3 Qt::DecorationRole - 图标
作用:在文本旁边显示图标或图片
返回类型 :QIcon, QPixmap, QImage, QColor
示例1:文件类型图标
cpp
QVariant data(const QModelIndex &index, int role) const override {
if (role == Qt::DecorationRole && index.column() == 0) {
QString filename = m_files[index.row()];
if (filename.endsWith(".txt"))
return QIcon(":/icons/text.png");
else if (filename.endsWith(".pdf"))
return QIcon(":/icons/pdf.png");
else if (filename.endsWith(".jpg") || filename.endsWith(".png"))
return QIcon(":/icons/image.png");
}
return QVariant();
}
示例2:状态指示器(使用颜色圆点)
cpp
QVariant data(const QModelIndex &index, int role) const override {
if (role == Qt::DecorationRole && index.column() == 1) {
// 状态列:用颜色表示状态
QString status = m_status[index.row()];
if (status == "online")
return QColor(Qt::green); // 绿色圆点
else if (status == "busy")
return QColor(Qt::red); // 红色圆点
else
return QColor(Qt::gray); // 灰色圆点
}
return QVariant();
}
2.2.2.4 Qt::ToolTipRole - 工具提示
作用:鼠标悬停时显示的提示信息
返回类型 :QString
示例:
cpp
QVariant data(const QModelIndex &index, int role) const override {
if (role == Qt::ToolTipRole) {
// 提供详细的提示信息
QString name = m_data[index.row()].name;
QString email = m_data[index.row()].email;
QString phone = m_data[index.row()].phone;
return QString("<b>%1</b><br>邮箱: %2<br>电话: %3")
.arg(name).arg(email).arg(phone);
}
return QVariant();
}
高级技巧:富文本提示
cpp
if (role == Qt::ToolTipRole) {
// 使用 HTML 格式化
return QString(
"<table>"
"<tr><td><b>姓名:</b></td><td>%1</td></tr>"
"<tr><td><b>部门:</b></td><td>%2</td></tr>"
"<tr><td><b>职位:</b></td><td>%3</td></tr>"
"</table>"
).arg(name, department, position);
}
2.2.2.5 Qt::BackgroundRole - 背景色
作用:设置单元格的背景颜色
返回类型 :QBrush 或 QColor
示例1:条件格式化(类似Excel)
cpp
QVariant data(const QModelIndex &index, int role) const override {
if (role == Qt::BackgroundRole) {
int score = m_scores[index.row()][index.column()];
// 成绩分级着色
if (score >= 90)
return QColor("#d4edda"); // 浅绿色(优秀)
else if (score >= 80)
return QColor("#d1ecf1"); // 浅蓝色(良好)
else if (score >= 60)
return QColor("#fff3cd"); // 浅黄色(及格)
else
return QColor("#f8d7da"); // 浅红色(不及格)
}
return QVariant();
}
示例2:斑马纹效果
cpp
QVariant data(const QModelIndex &index, int role) const override {
if (role == Qt::BackgroundRole) {
// 奇偶行不同颜色
if (index.row() % 2 == 0)
return QColor(Qt::white);
else
return QColor(240, 240, 240); // 浅灰色
}
return QVariant();
}
示例3:渐变背景
cpp
QVariant data(const QModelIndex &index, int role) const override {
if (role == Qt::BackgroundRole) {
// 创建渐变画刷
QLinearGradient gradient(0, 0, 100, 0);
gradient.setColorAt(0, QColor("#667eea"));
gradient.setColorAt(1, QColor("#764ba2"));
return QBrush(gradient);
}
return QVariant();
}
2.2.2.6 Qt::ForegroundRole - 前景色(文字颜色)
作用:设置文本颜色
返回类型 :QBrush 或 QColor
示例:
cpp
QVariant data(const QModelIndex &index, int role) const override {
if (role == Qt::ForegroundRole) {
double value = m_values[index.row()];
// 正数绿色,负数红色
if (value > 0)
return QColor(Qt::darkGreen);
else if (value < 0)
return QColor(Qt::red);
else
return QColor(Qt::black);
}
return QVariant();
}
Background + Foreground 组合使用:
cpp
QVariant data(const QModelIndex &index, int role) const override {
QString status = m_tasks[index.row()].status;
if (role == Qt::BackgroundRole) {
if (status == "completed")
return QColor("#28a745"); // 绿色背景
else if (status == "urgent")
return QColor("#dc3545"); // 红色背景
} else if (role == Qt::ForegroundRole) {
if (status == "completed" || status == "urgent")
return QColor(Qt::white); // 白色文字(与深色背景搭配)
}
return QVariant();
}
2.2.2.7 Qt::FontRole - 字体
作用:自定义字体样式、大小、粗细
返回类型 :QFont
示例1:标题行加粗
cpp
QVariant data(const QModelIndex &index, int role) const override {
if (role == Qt::FontRole) {
if (index.row() == 0) {
// 第一行加粗
QFont font;
font.setBold(true);
font.setPointSize(12);
return font;
}
}
return QVariant();
}
示例2:根据数据动态设置
cpp
QVariant data(const QModelIndex &index, int role) const override {
if (role == Qt::FontRole) {
bool isImportant = m_data[index.row()].important;
QFont font;
if (isImportant) {
font.setBold(true); // 加粗
font.setItalic(true); // 斜体
font.setPointSize(11); // 字号
}
return font;
}
return QVariant();
}
示例3:等宽字体(代码、日志)
cpp
QVariant data(const QModelIndex &index, int role) const override {
if (role == Qt::FontRole && index.column() == 2) {
// 代码列使用等宽字体
QFont font("Courier New", 10);
return font;
}
return QVariant();
}
2.2.2.8 Qt::TextAlignmentRole - 对齐方式
作用:设置文本对齐方式
返回类型 :Qt::Alignment(可以组合使用)
示例:
cpp
QVariant data(const QModelIndex &index, int role) const override {
if (role == Qt::TextAlignmentRole) {
if (index.column() == 0) {
// 第一列左对齐
return Qt::AlignLeft | Qt::AlignVCenter;
} else if (index.column() == 1) {
// 数值列右对齐
return Qt::AlignRight | Qt::AlignVCenter;
} else if (index.column() == 2) {
// 状态列居中
return Qt::AlignCenter;
}
}
return QVariant();
}
对齐选项:
| 水平对齐 | 垂直对齐 |
|---|---|
Qt::AlignLeft |
Qt::AlignTop |
Qt::AlignRight |
Qt::AlignBottom |
Qt::AlignHCenter |
Qt::AlignVCenter |
Qt::AlignJustify |
2.2.2.9 Qt::CheckStateRole - 复选框状态
作用:显示复选框并管理其状态
返回类型 :Qt::CheckState 枚举值
CheckState 枚举:
Qt::Unchecked(0) - 未选中Qt::PartiallyChecked(1) - 部分选中(仅在三态复选框中使用)Qt::Checked(2) - 选中
示例1:简单复选框
cpp
QVariant data(const QModelIndex &index, int role) const override {
if (role == Qt::CheckStateRole && index.column() == 0) {
// 第一列显示复选框
bool checked = m_checkedItems.contains(index.row());
return checked ? Qt::Checked : Qt::Unchecked;
}
return QVariant();
}
bool setData(const QModelIndex &index, const QVariant &value, int role) override {
if (role == Qt::CheckStateRole && index.column() == 0) {
Qt::CheckState state = static_cast<Qt::CheckState>(value.toInt());
if (state == Qt::Checked)
m_checkedItems.insert(index.row());
else
m_checkedItems.remove(index.row());
emit dataChanged(index, index, {Qt::CheckStateRole});
return true;
}
return false;
}
// 必须设置可选中标志
Qt::ItemFlags flags(const QModelIndex &index) const override {
Qt::ItemFlags flags = QAbstractTableModel::flags(index);
if (index.column() == 0)
flags |= Qt::ItemIsUserCheckable; // 添加可选中标志
return flags;
}
示例2:全选/全不选功能
cpp
class CheckableModel : public QAbstractListModel {
QStringList m_items;
QSet<int> m_checkedRows;
public:
// ... rowCount(), data() 实现 ...
// 全选
void checkAll() {
for (int i = 0; i < m_items.size(); ++i)
m_checkedRows.insert(i);
emit dataChanged(index(0), index(m_items.size() - 1), {Qt::CheckStateRole});
}
// 全不选
void uncheckAll() {
m_checkedRows.clear();
emit dataChanged(index(0), index(m_items.size() - 1), {Qt::CheckStateRole});
}
// 反选
void toggleAll() {
for (int i = 0; i < m_items.size(); ++i) {
if (m_checkedRows.contains(i))
m_checkedRows.remove(i);
else
m_checkedRows.insert(i);
}
emit dataChanged(index(0), index(m_items.size() - 1), {Qt::CheckStateRole});
}
// 获取选中的行
QList<int> getCheckedRows() const {
return m_checkedRows.values();
}
};
2.2.3 自定义数据角色
为什么需要自定义角色?
标准角色无法满足所有需求时,可以定义自己的角色:
cpp
// 定义自定义角色(从 Qt::UserRole 开始)
enum CustomRoles {
RawDataRole = Qt::UserRole + 1, // 原始数据
SortRole = Qt::UserRole + 2, // 排序用的值
FilterRole = Qt::UserRole + 3, // 过滤用的值
CategoryRole = Qt::UserRole + 4, // 分类信息
TimestampRole = Qt::UserRole + 5 // 时间戳
};
自定义角色的使用:
cpp
class ProductModel : public QAbstractTableModel {
struct Product {
QString name;
double price;
QDateTime created;
QString category;
};
QList<Product> m_products;
public:
enum CustomRoles {
SortRole = Qt::UserRole + 1,
CategoryRole = Qt::UserRole + 2
};
QVariant data(const QModelIndex &index, int role) const override {
if (!index.isValid())
return QVariant();
const Product &product = m_products[index.row()];
if (index.column() == 0) { // 名称列
if (role == Qt::DisplayRole)
return product.name;
else if (role == CategoryRole)
return product.category; // 自定义角色:分类
} else if (index.column() == 1) { // 价格列
if (role == Qt::DisplayRole)
return QString("¥%1").arg(product.price, 0, 'f', 2);
else if (role == SortRole)
return product.price; // 自定义角色:排序用原始数值
else if (role == Qt::EditRole)
return product.price;
}
return QVariant();
}
};
// 使用自定义角色进行过滤
class CategoryFilterProxy : public QSortFilterProxyModel {
QString m_category;
public:
void setCategory(const QString &category) {
m_category = category;
invalidateFilter();
}
protected:
bool filterAcceptsRow(int row, const QModelIndex &parent) const override {
if (m_category.isEmpty())
return true;
// 使用自定义角色获取分类
QModelIndex index = sourceModel()->index(row, 0, parent);
QString category = sourceModel()->data(index, ProductModel::CategoryRole).toString();
return category == m_category;
}
};
自定义角色的最佳实践:
cpp
// 1. 在类中定义角色枚举
class MyModel : public QAbstractItemModel {
Q_OBJECT
public:
enum Roles {
IdRole = Qt::UserRole + 1,
TimestampRole,
StatusRole,
PriorityRole
// 依次递增,避免冲突
};
Q_ENUM(Roles) // Qt 5.5+ 可以注册枚举到元对象系统
};
// 2. QML 中使用自定义角色
// 在 QML 中,可以通过 roleNames() 暴露给 QML
QHash<int, QByteArray> roleNames() const override {
QHash<int, QByteArray> roles = QAbstractItemModel::roleNames();
roles[IdRole] = "itemId";
roles[TimestampRole] = "timestamp";
roles[StatusRole] = "status";
return roles;
}
2.2.4 同一数据项的多角色数据
综合示例:任务管理器
cpp
#include <QAbstractListModel>
#include <QDateTime>
#include <QFont>
#include <QColor>
class TaskModel : public QAbstractListModel {
Q_OBJECT
public:
struct Task {
QString title;
QString description;
QDateTime dueDate;
int priority; // 1-5
bool completed;
};
enum CustomRoles {
TitleRole = Qt::UserRole + 1,
DescriptionRole,
DueDateRole,
PriorityRole,
CompletedRole
};
private:
QList<Task> m_tasks;
public:
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
return parent.isValid() ? 0 : m_tasks.size();
}
QVariant data(const QModelIndex &index, int role) const override {
if (!index.isValid() || index.row() >= m_tasks.size())
return QVariant();
const Task &task = m_tasks[index.row()];
switch (role) {
// 显示角色:显示标题和截止日期
case Qt::DisplayRole:
return QString("%1 (截止: %2)")
.arg(task.title)
.arg(task.dueDate.toString("MM-dd"));
// 提示角色:显示完整信息
case Qt::ToolTipRole:
return QString(
"<b>%1</b><br>"
"描述: %2<br>"
"截止日期: %3<br>"
"优先级: %4<br>"
"状态: %5"
).arg(task.title)
.arg(task.description)
.arg(task.dueDate.toString("yyyy-MM-dd hh:mm"))
.arg(task.priority)
.arg(task.completed ? "已完成" : "进行中");
// 图标角色:根据优先级显示不同图标
case Qt::DecorationRole:
if (task.priority >= 4)
return QIcon(":/icons/high-priority.png");
else if (task.priority >= 2)
return QIcon(":/icons/medium-priority.png");
else
return QIcon(":/icons/low-priority.png");
// 背景角色:已完成任务灰色,逾期任务红色
case Qt::BackgroundRole:
if (task.completed)
return QColor(220, 220, 220);
else if (task.dueDate < QDateTime::currentDateTime())
return QColor(255, 200, 200); // 逾期
else if (task.dueDate.daysTo(QDateTime::currentDateTime()) <= 1)
return QColor(255, 255, 200); // 即将到期
break;
// 前景角色:已完成任务文字变灰
case Qt::ForegroundRole:
if (task.completed)
return QColor(Qt::gray);
break;
// 字体角色:高优先级加粗
case Qt::FontRole:
if (task.priority >= 4) {
QFont font;
font.setBold(true);
return font;
}
break;
// 复选框角色:完成状态
case Qt::CheckStateRole:
return task.completed ? Qt::Checked : Qt::Unchecked;
// 自定义角色:各个字段
case TitleRole:
return task.title;
case DescriptionRole:
return task.description;
case DueDateRole:
return task.dueDate;
case PriorityRole:
return task.priority;
case CompletedRole:
return task.completed;
}
return QVariant();
}
bool setData(const QModelIndex &index, const QVariant &value, int role) override {
if (!index.isValid() || index.row() >= m_tasks.size())
return false;
Task &task = m_tasks[index.row()];
if (role == Qt::CheckStateRole) {
task.completed = (value.toInt() == Qt::Checked);
emit dataChanged(index, index);
return true;
}
return false;
}
Qt::ItemFlags flags(const QModelIndex &index) const override {
Qt::ItemFlags flags = QAbstractListModel::flags(index);
if (index.isValid())
flags |= Qt::ItemIsUserCheckable; // 可选中
return flags;
}
};
使用效果:
- DisplayRole:列表中显示 "完成报告 (截止: 01-25)"
- ToolTipRole:鼠标悬停显示完整的任务信息
- DecorationRole:高优先级任务显示红色感叹号图标
- BackgroundRole:已完成任务灰色背景,逾期任务红色背景
- ForegroundRole:已完成任务灰色文字
- FontRole:高优先级任务加粗显示
- CheckStateRole:复选框显示完成状态
- 自定义角色:可以通过自定义角色访问各个字段
本节小结:
✅ 数据角色 允许一个数据项以多种形式呈现
✅ Qt::DisplayRole 是最常用的角色,用于显示文本
✅ Qt::EditRole 用于编辑,可以与 DisplayRole 不同
✅ 装饰性角色 (DecorationRole, BackgroundRole, ForegroundRole, FontRole)用于美化界面
✅ Qt::ToolTipRole 提供额外的提示信息
✅ Qt::CheckStateRole 实现复选框功能
✅ 自定义角色 从 Qt::UserRole 开始定义,用于特殊需求
✅ 同一数据项可以同时响应多个角色,提供丰富的展示效果
- 什么是数据角色
- 常用角色详解
- Qt::DisplayRole - 显示文本
- Qt::EditRole - 编辑数据
- Qt::DecorationRole - 图标
- Qt::ToolTipRole - 工具提示
- Qt::BackgroundRole - 背景色
- Qt::ForegroundRole - 前景色
- Qt::FontRole - 字体
- Qt::TextAlignmentRole - 对齐方式
- Qt::CheckStateRole - 复选框状态
- 自定义数据角色
- 同一数据项的多角色数据
2.3 数据访问机制
2.3.1 data()方法:读取数据
方法签名:
cpp
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const = 0;
作用:
- 这是 Model 的核心方法之一
- View/Delegate 通过此方法从 Model 获取数据
- 根据不同的
role返回不同类型的数据
基本使用:
cpp
class SimpleModel : public QAbstractListModel {
QStringList m_data;
public:
QVariant data(const QModelIndex &index, int role) const override {
// 1. 检查索引有效性
if (!index.isValid())
return QVariant();
// 2. 检查索引范围
if (index.row() < 0 || index.row() >= m_data.size())
return QVariant();
// 3. 根据角色返回数据
if (role == Qt::DisplayRole || role == Qt::EditRole) {
return m_data[index.row()];
}
// 4. 不支持的角色返回无效 QVariant
return QVariant();
}
};
完整示例:产品列表模型
cpp
class ProductModel : public QAbstractTableModel {
public:
struct Product {
int id;
QString name;
double price;
int stock;
bool available;
};
private:
QList<Product> m_products;
public:
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
return parent.isValid() ? 0 : m_products.size();
}
int columnCount(const QModelIndex &parent = QModelIndex()) const override {
return parent.isValid() ? 0 : 4; // ID, 名称, 价格, 库存
}
QVariant data(const QModelIndex &index, int role) const override {
if (!index.isValid())
return QVariant();
if (index.row() < 0 || index.row() >= m_products.size())
return QVariant();
const Product &product = m_products[index.row()];
// 根据列和角色返回不同数据
switch (index.column()) {
case 0: // ID 列
if (role == Qt::DisplayRole)
return product.id;
else if (role == Qt::TextAlignmentRole)
return Qt::AlignCenter;
break;
case 1: // 名称列
if (role == Qt::DisplayRole || role == Qt::EditRole)
return product.name;
else if (role == Qt::DecorationRole)
return QIcon(":/icons/product.png");
break;
case 2: // 价格列
if (role == Qt::DisplayRole)
return QString("¥%1").arg(product.price, 0, 'f', 2);
else if (role == Qt::EditRole)
return product.price; // 编辑时返回数值
else if (role == Qt::TextAlignmentRole)
return Qt::AlignRight | Qt::AlignVCenter;
else if (role == Qt::ForegroundRole) {
// 高价产品红色显示
return product.price > 1000 ? QColor(Qt::red) : QColor(Qt::black);
}
break;
case 3: // 库存列
if (role == Qt::DisplayRole)
return product.stock;
else if (role == Qt::BackgroundRole) {
// 库存预警:少于10件标红
if (product.stock < 10)
return QColor(255, 200, 200);
else if (product.stock < 50)
return QColor(255, 255, 200);
}
else if (role == Qt::TextAlignmentRole)
return Qt::AlignCenter;
break;
}
return QVariant();
}
};
data() 方法的最佳实践:
cpp
QVariant data(const QModelIndex &index, int role) const override {
// ✅ 好的做法:先检查有效性
if (!index.isValid())
return QVariant();
// ✅ 好的做法:检查边界
if (index.row() >= m_data.size() || index.column() >= columnCount())
return QVariant();
// ✅ 好的做法:使用 switch 语句处理角色
switch (role) {
case Qt::DisplayRole:
return getDisplayData(index);
case Qt::EditRole:
return getEditData(index);
case Qt::DecorationRole:
return getIcon(index);
default:
return QVariant();
}
}
// ❌ 坏的做法:不检查有效性
QVariant badData(const QModelIndex &index, int role) const {
return m_data[index.row()]; // 可能越界!
}
// ❌ 坏的做法:处理所有角色
QVariant data(const QModelIndex &index, int role) const override {
// 不要为每个角色都返回相同的数据
return m_data[index.row()]; // 错误!
}
2.3.2 setData()方法:修改数据
方法签名:
cpp
virtual bool setData(const QModelIndex &index,
const QVariant &value,
int role = Qt::EditRole);
作用:
- 修改模型中的数据
- View/Delegate 通过此方法更新数据
- 成功返回
true,失败返回false
基本实现:
cpp
bool setData(const QModelIndex &index, const QVariant &value, int role) override {
// 1. 检查索引有效性
if (!index.isValid())
return false;
// 2. 检查索引范围
if (index.row() < 0 || index.row() >= m_data.size())
return false;
// 3. 只处理 EditRole
if (role != Qt::EditRole)
return false;
// 4. 修改数据
m_data[index.row()] = value.toString();
// 5. 发送数据变化信号(非常重要!)
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
return true;
}
完整示例:可编辑的产品模型
cpp
class EditableProductModel : public QAbstractTableModel {
QList<Product> m_products;
public:
bool setData(const QModelIndex &index, const QVariant &value, int role) override {
if (!index.isValid() || role != Qt::EditRole)
return false;
if (index.row() < 0 || index.row() >= m_products.size())
return false;
Product &product = m_products[index.row()];
// 根据列设置不同字段
switch (index.column()) {
case 0: // ID 列不可编辑
return false;
case 1: // 名称列
{
QString newName = value.toString().trimmed();
if (newName.isEmpty()) {
qWarning() << "Product name cannot be empty";
return false;
}
product.name = newName;
}
break;
case 2: // 价格列
{
bool ok;
double newPrice = value.toDouble(&ok);
if (!ok || newPrice < 0) {
qWarning() << "Invalid price";
return false;
}
product.price = newPrice;
}
break;
case 3: // 库存列
{
bool ok;
int newStock = value.toInt(&ok);
if (!ok || newStock < 0) {
qWarning() << "Invalid stock";
return false;
}
product.stock = newStock;
}
break;
default:
return false;
}
// 通知视图数据已变化
// 注意:可能需要更新多个角色
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
return true;
}
};
处理 CheckStateRole:
cpp
bool setData(const QModelIndex &index, const QVariant &value, int role) override {
if (!index.isValid())
return false;
if (role == Qt::EditRole) {
// 处理文本编辑
m_data[index.row()] = value.toString();
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
return true;
}
else if (role == Qt::CheckStateRole) {
// 处理复选框
Qt::CheckState state = static_cast<Qt::CheckState>(value.toInt());
m_checked[index.row()] = (state == Qt::Checked);
emit dataChanged(index, index, {Qt::CheckStateRole});
return true;
}
return false;
}
dataChanged 信号的使用:
cpp
// 1. 单个单元格变化
emit dataChanged(index, index);
// 2. 指定变化的角色(Qt 5+)
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
// 3. 范围变化(如修改整行)
QModelIndex topLeft = this->index(row, 0);
QModelIndex bottomRight = this->index(row, columnCount() - 1);
emit dataChanged(topLeft, bottomRight);
// 4. 修改多行
QModelIndex topLeft = this->index(firstRow, 0);
QModelIndex bottomRight = this->index(lastRow, columnCount() - 1);
emit dataChanged(topLeft, bottomRight);
数据验证示例:
cpp
bool setData(const QModelIndex &index, const QVariant &value, int role) override {
if (!index.isValid() || role != Qt::EditRole)
return false;
Student &student = m_students[index.row()];
switch (index.column()) {
case 0: // 姓名
{
QString name = value.toString().trimmed();
// 验证:不能为空
if (name.isEmpty()) {
qWarning() << "Name cannot be empty";
return false;
}
// 验证:长度限制
if (name.length() > 50) {
qWarning() << "Name too long";
return false;
}
student.name = name;
}
break;
case 1: // 年龄
{
bool ok;
int age = value.toInt(&ok);
// 验证:必须是数字
if (!ok) {
qWarning() << "Age must be a number";
return false;
}
// 验证:范围检查
if (age < 0 || age > 150) {
qWarning() << "Age out of range";
return false;
}
student.age = age;
}
break;
case 2: // 邮箱
{
QString email = value.toString().trimmed();
// 验证:邮箱格式
QRegularExpression emailRegex("^[\\w\\.-]+@[\\w\\.-]+\\.\\w+$");
if (!emailRegex.match(email).hasMatch()) {
qWarning() << "Invalid email format";
return false;
}
student.email = email;
}
break;
default:
return false;
}
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
return true;
}
2.3.3 flags()方法:项的属性标志
方法签名:
cpp
virtual Qt::ItemFlags flags(const QModelIndex &index) const;
作用:
- 定义数据项的行为属性
- 控制项是否可选中、可编辑、可拖放等
常用标志:
| 标志 | 说明 |
|---|---|
Qt::ItemIsSelectable |
可选中(默认) |
Qt::ItemIsEditable |
可编辑 |
Qt::ItemIsEnabled |
启用(默认) |
Qt::ItemIsUserCheckable |
可复选 |
Qt::ItemIsDragEnabled |
可拖动 |
Qt::ItemIsDropEnabled |
可接受放置 |
Qt::ItemNeverHasChildren |
永不有子项(性能优化) |
基本使用:
cpp
Qt::ItemFlags flags(const QModelIndex &index) const override {
if (!index.isValid())
return Qt::NoItemFlags; // 无效索引无标志
// 默认标志:可选中、启用
Qt::ItemFlags flags = QAbstractItemModel::flags(index);
// 添加可编辑标志
flags |= Qt::ItemIsEditable;
return flags;
}
示例1:部分列可编辑
cpp
Qt::ItemFlags flags(const QModelIndex &index) const override {
if (!index.isValid())
return Qt::NoItemFlags;
Qt::ItemFlags flags = QAbstractTableModel::flags(index);
// ID 列(第0列)不可编辑,其他列可编辑
if (index.column() != 0) {
flags |= Qt::ItemIsEditable;
}
return flags;
}
示例2:根据数据动态设置标志
cpp
Qt::ItemFlags flags(const QModelIndex &index) const override {
if (!index.isValid())
return Qt::NoItemFlags;
Qt::ItemFlags flags = QAbstractListModel::flags(index);
const Task &task = m_tasks[index.row()];
// 已完成的任务不可编辑
if (!task.completed) {
flags |= Qt::ItemIsEditable;
} else {
// 禁用已完成的任务
flags &= ~Qt::ItemIsEnabled;
}
return flags;
}
示例3:复选框支持
cpp
Qt::ItemFlags flags(const QModelIndex &index) const override {
if (!index.isValid())
return Qt::NoItemFlags;
Qt::ItemFlags flags = QAbstractListModel::flags(index);
// 添加可复选标志
flags |= Qt::ItemIsUserCheckable;
// 第一列还可以编辑
if (index.column() == 0)
flags |= Qt::ItemIsEditable;
return flags;
}
示例4:拖放支持
cpp
Qt::ItemFlags flags(const QModelIndex &index) const override {
Qt::ItemFlags defaultFlags = QAbstractListModel::flags(index);
if (index.isValid()) {
// 可拖动和编辑
return Qt::ItemIsDragEnabled | Qt::ItemIsEditable | defaultFlags;
} else {
// 无效索引可接受放置(用于拖放到空白区域)
return Qt::ItemIsDropEnabled | defaultFlags;
}
}
示例5:完全禁用某些项
cpp
Qt::ItemFlags flags(const QModelIndex &index) const override {
if (!index.isValid())
return Qt::NoItemFlags;
const Item &item = m_items[index.row()];
// 锁定的项完全禁用
if (item.isLocked) {
return Qt::NoItemFlags; // 不可选中、不可编辑、不可交互
}
// 普通项的默认标志
Qt::ItemFlags flags = QAbstractListModel::flags(index);
flags |= Qt::ItemIsEditable;
return flags;
}
flags() 的执行时机:
cpp
// flags() 会被频繁调用:
// - View 绘制时
// - 鼠标悬停时
// - 尝试编辑时
// - 拖放操作时
// 因此,flags() 应该:
// ✅ 执行快速
// ✅ 不应有副作用
// ✅ 每次调用返回一致的结果(除非数据确实改变)
Qt::ItemFlags flags(const QModelIndex &index) const override {
// ✅ 好的做法:简单快速的判断
if (!index.isValid())
return Qt::NoItemFlags;
Qt::ItemFlags flags = QAbstractTableModel::flags(index);
if (index.column() > 0)
flags |= Qt::ItemIsEditable;
return flags;
}
// ❌ 坏的做法:复杂的计算
Qt::ItemFlags badFlags(const QModelIndex &index) const {
// 不要在 flags() 中做耗时操作
QThread::sleep(1); // 错误!
return QAbstractTableModel::flags(index);
}
2.3.4 headerData()方法:表头数据
方法签名:
cpp
virtual QVariant headerData(int section,
Qt::Orientation orientation,
int role = Qt::DisplayRole) const;
参数说明:
section:表头的索引(第几行/列)orientation:方向(Qt::Horizontal或Qt::Vertical)role:数据角色
基本使用:
cpp
QVariant headerData(int section, Qt::Orientation orientation, int role) const override {
if (role != Qt::DisplayRole)
return QVariant();
if (orientation == Qt::Horizontal) {
// 水平表头(列标题)
switch (section) {
case 0: return "ID";
case 1: return "姓名";
case 2: return "年龄";
case 3: return "邮箱";
}
} else {
// 垂直表头(行号)
return section + 1; // 从1开始的行号
}
return QVariant();
}
完整示例:丰富的表头
cpp
QVariant headerData(int section, Qt::Orientation orientation, int role) const override {
if (orientation == Qt::Horizontal) {
// 列表头
if (role == Qt::DisplayRole) {
QStringList headers = {"ID", "产品名称", "价格", "库存", "状态"};
if (section < headers.size())
return headers[section];
}
else if (role == Qt::ToolTipRole) {
// 表头提示
switch (section) {
case 0: return "产品唯一标识符";
case 1: return "产品的名称";
case 2: return "产品价格(单位:元)";
case 3: return "当前库存数量";
case 4: return "产品是否可用";
}
}
else if (role == Qt::FontRole) {
// 表头字体加粗
QFont font;
font.setBold(true);
return font;
}
else if (role == Qt::BackgroundRole) {
// 表头背景色
return QColor(220, 220, 220);
}
else if (role == Qt::ForegroundRole) {
// 表头文字色
return QColor(Qt::darkBlue);
}
else if (role == Qt::TextAlignmentRole) {
// 表头对齐
return Qt::AlignCenter;
}
}
else {
// 行表头
if (role == Qt::DisplayRole) {
return QString::number(section + 1); // 行号从1开始
}
else if (role == Qt::TextAlignmentRole) {
return Qt::AlignCenter;
}
}
return QVariant();
}
使用 QStringList 简化:
cpp
class MyModel : public QAbstractTableModel {
QStringList m_horizontalHeaders;
public:
MyModel() {
m_horizontalHeaders << "学号" << "姓名" << "语文" << "数学" << "英语";
}
QVariant headerData(int section, Qt::Orientation orientation, int role) const override {
if (role == Qt::DisplayRole) {
if (orientation == Qt::Horizontal) {
if (section < m_horizontalHeaders.size())
return m_horizontalHeaders[section];
} else {
return section + 1;
}
}
return QVariant();
}
// 动态修改表头
void setHorizontalHeaderLabels(const QStringList &labels) {
m_horizontalHeaders = labels;
emit headerDataChanged(Qt::Horizontal, 0, labels.size() - 1);
}
};
表头图标:
cpp
QVariant headerData(int section, Qt::Orientation orientation, int role) const override {
if (orientation == Qt::Horizontal) {
if (role == Qt::DisplayRole) {
QStringList headers = {"姓名", "电话", "邮箱", "地址"};
return headers.value(section);
}
else if (role == Qt::DecorationRole) {
// 为表头添加图标
switch (section) {
case 0: return QIcon(":/icons/user.png");
case 1: return QIcon(":/icons/phone.png");
case 2: return QIcon(":/icons/email.png");
case 3: return QIcon(":/icons/address.png");
}
}
}
return QVariant();
}
可编辑的表头(高级):
cpp
// 注意:标准 QTableView 的表头默认不可编辑
// 需要使用 setHeaderData() 和 headerDataChanged 信号
bool setHeaderData(int section, Qt::Orientation orientation,
const QVariant &value, int role) override {
if (role != Qt::EditRole || orientation != Qt::Horizontal)
return false;
if (section < 0 || section >= m_headers.size())
return false;
m_headers[section] = value.toString();
emit headerDataChanged(orientation, section, section);
return true;
}
QVariant headerData(int section, Qt::Orientation orientation, int role) const override {
if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
if (section < m_headers.size())
return m_headers[section];
}
return QVariant();
}
综合示例:完整的数据访问实现
cpp
#include <QAbstractTableModel>
#include <QDateTime>
#include <QColor>
#include <QFont>
class EmployeeModel : public QAbstractTableModel {
Q_OBJECT
public:
struct Employee {
int id;
QString name;
QString department;
double salary;
QDate hireDate;
bool active;
};
private:
QList<Employee> m_employees;
QStringList m_headers;
public:
EmployeeModel(QObject *parent = nullptr) : QAbstractTableModel(parent) {
m_headers << "ID" << "姓名" << "部门" << "薪资" << "入职日期" << "状态";
// 添加示例数据
m_employees = {
{1001, "张三", "技术部", 8000, QDate(2020, 1, 15), true},
{1002, "李四", "销售部", 7500, QDate(2019, 6, 10), true},
{1003, "王五", "人事部", 6000, QDate(2021, 3, 20), false}
};
}
// ===== 基本接口 =====
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
return parent.isValid() ? 0 : m_employees.size();
}
int columnCount(const QModelIndex &parent = QModelIndex()) const override {
return parent.isValid() ? 0 : 6;
}
// ===== data() 方法 =====
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
if (!index.isValid())
return QVariant();
if (index.row() < 0 || index.row() >= m_employees.size())
return QVariant();
const Employee &emp = m_employees[index.row()];
// 显示角色
if (role == Qt::DisplayRole) {
switch (index.column()) {
case 0: return emp.id;
case 1: return emp.name;
case 2: return emp.department;
case 3: return QString("¥%1").arg(emp.salary, 0, 'f', 2);
case 4: return emp.hireDate.toString("yyyy-MM-dd");
case 5: return emp.active ? "在职" : "离职";
}
}
// 编辑角色
else if (role == Qt::EditRole) {
switch (index.column()) {
case 1: return emp.name;
case 2: return emp.department;
case 3: return emp.salary; // 返回数值,非格式化字符串
case 4: return emp.hireDate;
}
}
// 对齐方式
else if (role == Qt::TextAlignmentRole) {
if (index.column() == 0 || index.column() == 3)
return Qt::AlignRight | Qt::AlignVCenter;
else if (index.column() == 5)
return Qt::AlignCenter;
else
return Qt::AlignLeft | Qt::AlignVCenter;
}
// 前景色
else if (role == Qt::ForegroundRole) {
if (index.column() == 5)
return emp.active ? QColor(Qt::darkGreen) : QColor(Qt::red);
}
// 背景色
else if (role == Qt::BackgroundRole) {
if (!emp.active)
return QColor(240, 240, 240); // 离职员工灰色背景
}
// 字体
else if (role == Qt::FontRole) {
if (index.column() == 1) {
QFont font;
font.setBold(true);
return font;
}
}
// 提示信息
else if (role == Qt::ToolTipRole) {
return QString("员工 %1 - %2\n部门: %3\n薪资: ¥%4\n入职: %5")
.arg(emp.id)
.arg(emp.name)
.arg(emp.department)
.arg(emp.salary)
.arg(emp.hireDate.toString("yyyy-MM-dd"));
}
return QVariant();
}
// ===== setData() 方法 =====
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override {
if (!index.isValid() || role != Qt::EditRole)
return false;
if (index.row() < 0 || index.row() >= m_employees.size())
return false;
Employee &emp = m_employees[index.row()];
switch (index.column()) {
case 0: // ID 不可编辑
return false;
case 1: // 姓名
emp.name = value.toString();
break;
case 2: // 部门
emp.department = value.toString();
break;
case 3: // 薪资
{
bool ok;
double salary = value.toDouble(&ok);
if (ok && salary >= 0) {
emp.salary = salary;
} else {
return false;
}
}
break;
case 4: // 入职日期
emp.hireDate = value.toDate();
break;
case 5: // 状态(通过复选框)
emp.active = value.toBool();
break;
default:
return false;
}
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
return true;
}
// ===== flags() 方法 =====
Qt::ItemFlags flags(const QModelIndex &index) const override {
if (!index.isValid())
return Qt::NoItemFlags;
Qt::ItemFlags flags = QAbstractTableModel::flags(index);
// ID 列不可编辑
if (index.column() != 0) {
flags |= Qt::ItemIsEditable;
}
return flags;
}
// ===== headerData() 方法 =====
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override {
if (orientation == Qt::Horizontal) {
if (role == Qt::DisplayRole) {
if (section < m_headers.size())
return m_headers[section];
}
else if (role == Qt::FontRole) {
QFont font;
font.setBold(true);
return font;
}
else if (role == Qt::BackgroundRole) {
return QColor(200, 220, 240);
}
}
else {
if (role == Qt::DisplayRole)
return section + 1;
}
return QVariant();
}
};
本节小结:
✅ data() 是读取数据的核心方法,根据不同角色返回不同数据
✅ setData() 用于修改数据,成功后必须发送 dataChanged 信号
✅ flags() 控制项的行为属性(可选中、可编辑、可拖放等)
✅ headerData() 提供表头数据,支持多种角色(文本、图标、样式等)
✅ 所有方法都应该检查索引有效性和范围
✅ 数据修改时要进行适当的验证
✅ 使用信号通知视图数据已变化
- data()方法:读取数据
- setData()方法:修改数据
- flags()方法:项的属性标志
- headerData()方法:表头数据
第2章小结:
✅ QModelIndex 是访问模型数据的索引,支持普通索引和持久索引
✅ 数据角色 允许同一数据项以多种形式呈现(显示、编辑、装饰等)
✅ data() 和 setData() 是数据读写的核心接口
✅ flags() 控制项的交互行为
✅ headerData() 定义表头显示内容
✅ 正确使用这些机制是实现自定义 Model 的基础
第3章 QAbstractItemModel深度解析
3.1 QAbstractItemModel核心接口
3.1.1 必须实现的纯虚函数
QAbstractItemModel 是所有 Model 的基类,定义了以下必须实现的纯虚函数:
3.1.1.1 rowCount() - 行数
方法签名:
cpp
virtual int rowCount(const QModelIndex &parent = QModelIndex()) const = 0;
作用:返回给定父索引下的行数
参数说明:
parent:父索引- 列表/表格模型:parent 通常是无效索引,返回总行数
- 树形模型:parent 指定父节点,返回该节点的子节点数量
列表/表格模型中的实现:
cpp
// 列表模型
int rowCount(const QModelIndex &parent) const override {
// 列表模型没有父子关系,parent 应该无效
if (parent.isValid())
return 0; // 子项没有行
return m_data.size(); // 返回总行数
}
// 表格模型
int rowCount(const QModelIndex &parent) const override {
// 表格模型也没有父子关系
return parent.isValid() ? 0 : m_rows.size();
}
树形模型中的实现:
cpp
int rowCount(const QModelIndex &parent) const override {
TreeItem *parentItem;
if (!parent.isValid()) {
// 无效索引表示根项
parentItem = m_rootItem;
} else {
// 从索引中获取节点
parentItem = static_cast<TreeItem*>(parent.internalPointer());
}
// 返回该节点的子节点数量
return parentItem->childCount();
}
关键点:
- ✅ 列表/表格模型中,parent 有效时应返回 0
- ✅ 树形模型中,parent 决定了返回哪个节点的子节点数
- ✅ 必须快速执行,会被频繁调用
3.1.1.2 columnCount() - 列数
方法签名:
cpp
virtual int columnCount(const QModelIndex &parent = QModelIndex()) const = 0;
作用:返回给定父索引下的列数
列表模型:
cpp
int columnCount(const QModelIndex &parent) const override {
// 列表模型通常只有1列
return parent.isValid() ? 0 : 1;
}
表格模型:
cpp
int columnCount(const QModelIndex &parent) const override {
// 表格模型返回固定列数
return parent.isValid() ? 0 : 5; // 假设5列
}
树形模型(多列树):
cpp
int columnCount(const QModelIndex &parent) const override {
// 树形模型所有节点的列数相同
Q_UNUSED(parent);
return 3; // 例如:名称、大小、日期
}
3.1.1.3 data() - 数据获取
方法签名:
cpp
virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const = 0;
作用:返回指定索引和角色的数据
这个方法在第2章已经详细讲解,这里补充一些要点:
cpp
QVariant data(const QModelIndex &index, int role) const override {
// 1. 始终检查有效性
if (!index.isValid())
return QVariant();
// 2. 检查边界
if (index.row() < 0 || index.row() >= rowCount())
return QVariant();
// 3. 处理角色
if (role == Qt::DisplayRole) {
// 返回显示数据
return m_data[index.row()];
}
return QVariant();
}
性能提示:
cpp
// ❌ 低效的做法
QVariant data(const QModelIndex &index, int role) const override {
if (role == Qt::DisplayRole) {
// 每次都重新计算,很慢
return calculateComplexData(index);
}
return QVariant();
}
// ✅ 高效的做法
QVariant data(const QModelIndex &index, int role) const override {
if (role == Qt::DisplayRole) {
// 使用缓存的数据
return m_cachedData[index.row()];
}
return QVariant();
}
3.1.1.4 index() - 创建索引
方法签名:
cpp
virtual QModelIndex index(int row, int column,
const QModelIndex &parent = QModelIndex()) const = 0;
作用:为指定的行、列和父索引创建模型索引
列表/表格模型:
cpp
QModelIndex index(int row, int column, const QModelIndex &parent) const override {
// 列表/表格没有父子关系
if (parent.isValid())
return QModelIndex();
// 检查边界
if (row < 0 || row >= rowCount() || column < 0 || column >= columnCount())
return QModelIndex();
// 创建索引(不需要 internalPointer)
return createIndex(row, column);
}
树形模型:
cpp
QModelIndex index(int row, int column, const QModelIndex &parent) const override {
// 使用 hasIndex() 检查有效性
if (!hasIndex(row, column, parent))
return QModelIndex();
TreeItem *parentItem;
if (!parent.isValid()) {
parentItem = m_rootItem; // 根节点
} else {
parentItem = static_cast<TreeItem*>(parent.internalPointer());
}
TreeItem *childItem = parentItem->child(row);
if (childItem) {
// 将子节点指针存储在索引中
return createIndex(row, column, childItem);
}
return QModelIndex();
}
重要规则:
- 一致性:相同的 (row, column, parent) 必须返回相同的索引
- 有效性:返回的索引必须有效或为 QModelIndex()
- 内部指针:树形模型必须正确设置 internalPointer
hasIndex() 辅助方法:
cpp
// Qt 提供的辅助方法,检查索引是否有效
bool hasIndex(int row, int column, const QModelIndex &parent) const {
if (row < 0 || column < 0)
return false;
if (row >= rowCount(parent) || column >= columnCount(parent))
return false;
return true;
}
3.1.1.5 parent() - 获取父索引
方法签名:
cpp
virtual QModelIndex parent(const QModelIndex &child) const = 0;
作用:返回给定子索引的父索引
列表/表格模型:
cpp
QModelIndex parent(const QModelIndex &child) const override {
// 列表/表格没有层级结构,总是返回无效索引
Q_UNUSED(child);
return QModelIndex();
}
树形模型:
cpp
QModelIndex parent(const QModelIndex &child) const override {
if (!child.isValid())
return QModelIndex();
// 从索引中获取子节点
TreeItem *childItem = static_cast<TreeItem*>(child.internalPointer());
TreeItem *parentItem = childItem->parentItem();
// 根节点的子项没有父索引
if (parentItem == m_rootItem)
return QModelIndex();
// 找到 parentItem 在其父节点中的行号
TreeItem *grandparentItem = parentItem->parentItem();
int row = grandparentItem->indexOf(parentItem);
return createIndex(row, 0, parentItem);
}
index() 和 parent() 的一致性:
cpp
// 重要约束:以下断言必须为真
QModelIndex idx = model->index(row, col, parent);
assert(model->parent(idx) == parent); // parent(index(r,c,p)) == p
// 对于顶层项
QModelIndex topLevel = model->index(0, 0);
assert(!model->parent(topLevel).isValid()); // 顶层项的 parent 无效
3.1.2 可选实现的虚函数
以下虚函数有默认实现,但通常需要重写以提供完整功能:
3.1.2.1 setData() - 数据修改
方法签名:
cpp
virtual bool setData(const QModelIndex &index, const QVariant &value,
int role = Qt::EditRole);
默认实现 :返回 false(不可编辑)
自定义实现:
cpp
bool setData(const QModelIndex &index, const QVariant &value, int role) override {
if (!index.isValid() || role != Qt::EditRole)
return false;
if (index.row() >= m_data.size())
return false;
// 修改数据
m_data[index.row()] = value.toString();
// 重要:发送信号
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
return true;
}
详见第2章 2.3.2 节。
3.1.2.2 flags() - 项标志
方法签名:
cpp
virtual Qt::ItemFlags flags(const QModelIndex &index) const;
默认实现:
cpp
Qt::ItemFlags flags(const QModelIndex &index) const {
if (!index.isValid())
return Qt::NoItemFlags;
return Qt::ItemIsSelectable | Qt::ItemIsEnabled;
}
自定义实现:
cpp
Qt::ItemFlags flags(const QModelIndex &index) const override {
if (!index.isValid())
return Qt::NoItemFlags;
Qt::ItemFlags flags = QAbstractTableModel::flags(index);
// 添加可编辑标志
flags |= Qt::ItemIsEditable;
// 第一列添加复选框
if (index.column() == 0)
flags |= Qt::ItemIsUserCheckable;
return flags;
}
详见第2章 2.3.3 节。
3.1.2.3 headerData() - 表头数据
方法签名:
cpp
virtual QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const;
默认实现:返回简单的节号
自定义实现:
cpp
QVariant headerData(int section, Qt::Orientation orientation, int role) const override {
if (role != Qt::DisplayRole)
return QVariant();
if (orientation == Qt::Horizontal) {
// 列表头
QStringList headers = {"ID", "Name", "Age", "Email"};
return headers.value(section);
} else {
// 行表头
return section + 1;
}
}
详见第2章 2.3.4 节。
3.1.2.4 insertRows() / removeRows() - 行操作
insertRows() 方法签名:
cpp
virtual bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex());
作用:在指定位置插入行
实现步骤:
cpp
bool insertRows(int row, int count, const QModelIndex &parent) override {
// 列表/表格模型:parent 必须无效
if (parent.isValid())
return false;
// 检查位置有效性
if (row < 0 || row > m_data.size())
return false;
// 1. 调用 beginInsertRows()
beginInsertRows(parent, row, row + count - 1);
// 2. 修改内部数据结构
for (int i = 0; i < count; ++i) {
m_data.insert(row, QString()); // 插入空数据
}
// 3. 调用 endInsertRows()
endInsertRows();
return true;
}
removeRows() 方法签名:
cpp
virtual bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex());
实现步骤:
cpp
bool removeRows(int row, int count, const QModelIndex &parent) override {
if (parent.isValid())
return false;
// 检查范围
if (row < 0 || row + count > m_data.size())
return false;
// 1. 调用 beginRemoveRows()
beginRemoveRows(parent, row, row + count - 1);
// 2. 修改内部数据结构
for (int i = 0; i < count; ++i) {
m_data.removeAt(row);
}
// 3. 调用 endRemoveRows()
endRemoveRows();
return true;
}
关键规则:
- 必须调用 begin/end 方法:否则 View 不会更新
- 调用顺序:beginXxx → 修改数据 → endXxx
- 参数一致:begin 和 end 之间不能调用其他 begin
错误示例:
cpp
// ❌ 错误:忘记调用 begin/end
bool insertRows(int row, int count, const QModelIndex &parent) override {
m_data.insert(row, QString()); // View 不会更新!
return true;
}
// ❌ 错误:顺序错误
bool insertRows(int row, int count, const QModelIndex &parent) override {
m_data.insert(row, QString()); // 先修改数据
beginInsertRows(parent, row, row); // 后调用 begin - 错误!
endInsertRows();
return true;
}
// ❌ 错误:嵌套调用
bool insertRows(int row, int count, const QModelIndex &parent) override {
beginInsertRows(parent, row, row);
beginRemoveRows(parent, 0, 0); // 不能嵌套!
// ...
}
3.1.2.5 insertColumns() / removeColumns() - 列操作
insertColumns() 方法签名:
cpp
virtual bool insertColumns(int column, int count, const QModelIndex &parent = QModelIndex());
实现:
cpp
bool insertColumns(int column, int count, const QModelIndex &parent) override {
if (parent.isValid())
return false;
if (column < 0 || column > m_columnCount)
return false;
beginInsertColumns(parent, column, column + count - 1);
m_columnCount += count;
// 为每行添加新列的数据
for (int row = 0; row < m_data.size(); ++row) {
for (int i = 0; i < count; ++i) {
// 在适当位置插入空数据
}
}
endInsertColumns();
return true;
}
removeColumns() 方法签名:
cpp
virtual bool removeColumns(int column, int count, const QModelIndex &parent = QModelIndex());
实现类似于 removeRows()。
完整示例:可增删的列表模型
cpp
#include <QAbstractListModel>
#include <QStringList>
class EditableListModel : public QAbstractListModel {
Q_OBJECT
private:
QStringList m_data;
public:
EditableListModel(QObject *parent = nullptr) : QAbstractListModel(parent) {
m_data << "Item 1" << "Item 2" << "Item 3";
}
// ===== 必须实现的方法 =====
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
return parent.isValid() ? 0 : m_data.size();
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
if (!index.isValid())
return QVariant();
if (index.row() >= m_data.size())
return QVariant();
if (role == Qt::DisplayRole || role == Qt::EditRole)
return m_data[index.row()];
return QVariant();
}
// ===== 可选实现的方法 =====
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override {
if (!index.isValid() || role != Qt::EditRole)
return false;
if (index.row() >= m_data.size())
return false;
m_data[index.row()] = value.toString();
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
return true;
}
Qt::ItemFlags flags(const QModelIndex &index) const override {
if (!index.isValid())
return Qt::NoItemFlags;
return QAbstractListModel::flags(index) | Qt::ItemIsEditable;
}
bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override {
if (parent.isValid())
return false;
if (row < 0 || row > m_data.size())
return false;
beginInsertRows(parent, row, row + count - 1);
for (int i = 0; i < count; ++i) {
m_data.insert(row, QString("New Item %1").arg(row + i));
}
endInsertRows();
return true;
}
bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override {
if (parent.isValid())
return false;
if (row < 0 || row + count > m_data.size())
return false;
beginRemoveRows(parent, row, row + count - 1);
for (int i = 0; i < count; ++i) {
m_data.removeAt(row);
}
endRemoveRows();
return true;
}
// ===== 便捷方法 =====
void appendRow(const QString &text) {
insertRows(m_data.size(), 1);
setData(index(m_data.size() - 1), text);
}
void clear() {
if (m_data.isEmpty())
return;
removeRows(0, m_data.size());
}
};
使用示例:
cpp
#include <QApplication>
#include <QListView>
#include <QPushButton>
#include <QVBoxLayout>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
EditableListModel *model = new EditableListModel;
QListView *view = new QListView;
view->setModel(model);
QPushButton *addButton = new QPushButton("Add Item");
QObject::connect(addButton, &QPushButton::clicked, [=]() {
model->appendRow("New Item");
});
QPushButton *removeButton = new QPushButton("Remove Selected");
QObject::connect(removeButton, &QPushButton::clicked, [=]() {
QModelIndex current = view->currentIndex();
if (current.isValid()) {
model->removeRows(current.row(), 1);
}
});
QWidget window;
QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(view);
layout->addWidget(addButton);
layout->addWidget(removeButton);
window.setLayout(layout);
window.resize(300, 400);
window.show();
return app.exec();
}
本节小结:
✅ 必须实现的纯虚函数:
rowCount()- 返回行数columnCount()- 返回列数data()- 返回数据index()- 创建索引parent()- 返回父索引
✅ 可选实现的虚函数:
setData()- 修改数据flags()- 设置项标志headerData()- 表头数据insertRows()/removeRows()- 行操作insertColumns()/removeColumns()- 列操作
✅ 关键规则:
-
index() 和 parent() 必须保持一致性
-
修改数据结构时必须调用 begin/end 方法
-
数据改变时必须发送 dataChanged 信号
-
必须实现的纯虚函数
- rowCount() - 行数
- columnCount() - 列数
- data() - 数据获取
- index() - 创建索引
- parent() - 获取父索引
-
可选实现的虚函数
- setData() - 数据修改
- flags() - 项标志
- headerData() - 表头数据
- insertRows() / removeRows() - 行操作
- insertColumns() / removeColumns() - 列操作
3.2 信号详解
QAbstractItemModel 提供了多个信号来通知 View 模型的变化。正确使用这些信号是实现自定义模型的关键。
3.2.1 dataChanged() - 数据变化通知
信号签名:
cpp
void dataChanged(const QModelIndex &topLeft,
const QModelIndex &bottomRight,
const QVector<int> &roles = QVector<int>());
作用:通知 View 指定范围内的数据已改变
参数说明:
topLeft:变化区域的左上角索引bottomRight:变化区域的右下角索引roles:变化的角色(Qt 5+,可选)
使用场景:
场景1:单个单元格变化
cpp
bool setData(const QModelIndex &index, const QVariant &value, int role) override {
if (!index.isValid() || role != Qt::EditRole)
return false;
m_data[index.row()] = value.toString();
// 通知单个单元格变化
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
return true;
}
场景2:整行变化
cpp
void updateRow(int row) {
QModelIndex topLeft = index(row, 0);
QModelIndex bottomRight = index(row, columnCount() - 1);
// 通知整行变化
emit dataChanged(topLeft, bottomRight);
}
场景3:多行变化
cpp
void updateRows(int firstRow, int lastRow) {
QModelIndex topLeft = index(firstRow, 0);
QModelIndex bottomRight = index(lastRow, columnCount() - 1);
emit dataChanged(topLeft, bottomRight);
}
场景4:指定角色变化(性能优化)
cpp
void updateColors() {
QModelIndex topLeft = index(0, 0);
QModelIndex bottomRight = index(rowCount() - 1, columnCount() - 1);
// 只通知背景色角色变化,View 只需重新绘制颜色
emit dataChanged(topLeft, bottomRight, {Qt::BackgroundRole});
}
最佳实践:
cpp
// ✅ 好的做法:指定具体的角色
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
// ⚠️ 可以但不推荐:不指定角色(View 会重新请求所有角色)
emit dataChanged(index, index);
// ❌ 错误:范围错误
emit dataChanged(bottomRight, topLeft); // 左上和右下搞反了
// ❌ 错误:忘记发送信号
bool setData(...) {
m_data[index.row()] = value;
// 忘记 emit dataChanged() - View 不会更新!
return true;
}
3.2.2 layoutChanged() 和 layoutAboutToBeChanged() - 布局变化
信号签名:
cpp
void layoutAboutToBeChanged(const QList<QPersistentModelIndex> &parents = ...,
QAbstractItemModel::LayoutChangeHint hint = ...);
void layoutChanged(const QList<QPersistentModelIndex> &parents = ...,
QAbstractItemModel::LayoutChangeHint hint = ...);
作用:通知 View 模型的布局(结构)即将/已经改变
使用场景:
场景1:排序
cpp
void sort(int column, Qt::SortOrder order) override {
// 1. 发送布局即将改变信号
emit layoutAboutToBeChanged();
// 2. 执行排序
if (order == Qt::AscendingOrder) {
std::sort(m_data.begin(), m_data.end(),
[column](const Row &a, const Row &b) {
return a[column] < b[column];
});
} else {
std::sort(m_data.begin(), m_data.end(),
[column](const Row &a, const Row &b) {
return a[column] > b[column];
});
}
// 3. 发送布局已改变信号
emit layoutChanged();
}
场景2:重新组织数据结构
cpp
void reorganize() {
emit layoutAboutToBeChanged();
// 重新组织数据
// 例如:将数据从按 ID 排序改为按名称排序
emit layoutChanged();
}
持久索引的处理:
cpp
void sort(int column, Qt::SortOrder order) override {
// 获取持久索引列表
QModelIndexList oldIndexes = persistentIndexList();
QList<QPersistentModelIndex> old;
for (const QModelIndex &idx : oldIndexes)
old.append(QPersistentModelIndex(idx));
emit layoutAboutToBeChanged();
// 执行排序...
std::sort(m_data.begin(), m_data.end(), ...);
// 更新持久索引映射
QModelIndexList newIndexes;
for (const QPersistentModelIndex &idx : old) {
// 找到旧数据在新位置的索引
int newRow = findNewRow(idx.row());
newIndexes.append(index(newRow, idx.column()));
}
changePersistentIndexList(oldIndexes, newIndexes);
emit layoutChanged();
}
何时使用 layoutChanged vs dataChanged:
| 操作类型 | 使用信号 | 原因 |
|---|---|---|
| 修改单个数据项 | dataChanged |
数据内容改变,位置不变 |
| 修改多个数据项 | dataChanged |
数据内容改变,位置不变 |
| 排序 | layoutChanged |
数据位置改变 |
| 过滤 | layoutChanged |
数据可见性改变 |
| 重新组织 | layoutChanged |
结构改变 |
3.2.3 rowsInserted() / rowsRemoved() - 行插入/删除
信号签名:
cpp
void rowsAboutToBeInserted(const QModelIndex &parent, int first, int last);
void rowsInserted(const QModelIndex &parent, int first, int last);
void rowsAboutToBeRemoved(const QModelIndex &parent, int first, int last);
void rowsRemoved(const QModelIndex &parent, int first, int last);
关键点:
- 不要手动发送这些信号
- 使用 begin/end 方法自动发送
正确用法:
cpp
bool insertRows(int row, int count, const QModelIndex &parent) override {
// ✅ 正确:调用 beginInsertRows()
beginInsertRows(parent, row, row + count - 1);
// 修改数据
for (int i = 0; i < count; ++i) {
m_data.insert(row, QString());
}
// endInsertRows() 会自动发送 rowsInserted() 信号
endInsertRows();
return true;
}
// ❌ 错误:手动发送信号
bool insertRows(int row, int count, const QModelIndex &parent) override {
for (int i = 0; i < count; ++i) {
m_data.insert(row, QString());
}
emit rowsInserted(parent, row, row + count - 1); // 错误!
return true;
}
begin/end 方法的内部机制:
cpp
// Qt 内部实现的伪代码
void QAbstractItemModel::beginInsertRows(const QModelIndex &parent, int first, int last) {
// 1. 发送 rowsAboutToBeInserted 信号
emit rowsAboutToBeInserted(parent, first, last);
// 2. 通知 View 准备更新
// 3. 保存视图状态
}
void QAbstractItemModel::endInsertRows() {
// 1. 发送 rowsInserted 信号
emit rowsInserted(parent, first, last);
// 2. View 更新显示
// 3. 恢复视图状态
}
3.2.4 columnsInserted() / columnsRemoved() - 列插入/删除
信号签名:
cpp
void columnsAboutToBeInserted(const QModelIndex &parent, int first, int last);
void columnsInserted(const QModelIndex &parent, int first, int last);
void columnsAboutToBeRemoved(const QModelIndex &parent, int first, int last);
void columnsRemoved(const QModelIndex &parent, int first, int last);
使用方式与行操作类似:
cpp
bool insertColumns(int column, int count, const QModelIndex &parent) override {
beginInsertColumns(parent, column, column + count - 1);
// 修改数据结构
m_columnCount += count;
endInsertColumns();
return true;
}
3.2.5 modelReset() - 模型重置
信号签名:
cpp
void modelAboutToBeReset();
void modelReset();
作用:通知 View 模型即将/已经完全重置
使用场景:
场景1:完全替换数据
cpp
void loadData(const QVector<QString> &newData) {
// 完全替换数据时使用 reset
beginResetModel();
m_data = newData;
endResetModel();
}
场景2:清空数据
cpp
void clear() {
beginResetModel();
m_data.clear();
endResetModel();
}
场景3:从数据库重新加载
cpp
void reloadFromDatabase() {
beginResetModel();
m_data.clear();
// 从数据库加载
QSqlQuery query("SELECT * FROM table");
while (query.next()) {
m_data.append(query.value(0).toString());
}
endResetModel();
}
何时使用 modelReset vs insertRows/removeRows:
| 操作 | 推荐方法 | 原因 |
|---|---|---|
| 添加少量行 | insertRows |
性能更好,保持选择 |
| 删除少量行 | removeRows |
性能更好,保持选择 |
| 完全替换数据 | modelReset |
简单直接 |
| 清空所有数据 | modelReset |
简单直接 |
| 重新加载所有数据 | modelReset |
重新初始化 |
性能对比:
cpp
// ❌ 低效:逐行删除
void clearSlow() {
while (rowCount() > 0) {
removeRows(0, 1); // 多次信号,很慢
}
}
// ✅ 高效:使用 modelReset
void clearFast() {
beginResetModel();
m_data.clear();
endResetModel();
}
// ✅ 中等:批量删除
void clearMedium() {
if (rowCount() > 0) {
removeRows(0, rowCount()); // 一次性删除
}
}
3.2.6 headerDataChanged() - 表头数据变化
信号签名:
cpp
void headerDataChanged(Qt::Orientation orientation, int first, int last);
使用场景:
cpp
void setHorizontalHeaderLabels(const QStringList &labels) {
m_headers = labels;
// 通知表头数据已改变
emit headerDataChanged(Qt::Horizontal, 0, labels.size() - 1);
}
void updateColumnHeader(int column, const QString &newLabel) {
m_headers[column] = newLabel;
// 通知单个表头改变
emit headerDataChanged(Qt::Horizontal, column, column);
}
3.2.7 信号的正确使用时机
决策树:
数据改变了?
│
├─ 是内容改变(值变化)?
│ └─ 使用 dataChanged()
│
├─ 是位置改变(排序/过滤)?
│ └─ 使用 layoutAboutToBeChanged() + layoutChanged()
│
├─ 是添加/删除行?
│ └─ 使用 beginInsertRows()/beginRemoveRows() + end 方法
│
├─ 是添加/删除列?
│ └─ 使用 beginInsertColumns()/beginRemoveColumns() + end 方法
│
├─ 是完全重置?
│ └─ 使用 beginResetModel() + endResetModel()
│
└─ 是表头改变?
└─ 使用 headerDataChanged()
综合示例:完整的信号使用
cpp
#include <QAbstractListModel>
#include <QStringList>
#include <algorithm>
class SignalDemoModel : public QAbstractListModel {
Q_OBJECT
private:
QStringList m_data;
public:
SignalDemoModel(QObject *parent = nullptr) : QAbstractListModel(parent) {
m_data << "Apple" << "Banana" << "Cherry";
}
// ===== 基本接口 =====
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
return parent.isValid() ? 0 : m_data.size();
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
if (!index.isValid() || index.row() >= m_data.size())
return QVariant();
if (role == Qt::DisplayRole)
return m_data[index.row()];
return QVariant();
}
// ===== 数据修改(使用 dataChanged)=====
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override {
if (!index.isValid() || role != Qt::EditRole)
return false;
m_data[index.row()] = value.toString();
// 发送 dataChanged 信号
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
return true;
}
// ===== 添加行(使用 beginInsertRows/endInsertRows)=====
void append(const QString &text) {
int row = m_data.size();
// begin 自动发送 rowsAboutToBeInserted
beginInsertRows(QModelIndex(), row, row);
m_data.append(text);
// end 自动发送 rowsInserted
endInsertRows();
}
// ===== 删除行(使用 beginRemoveRows/endRemoveRows)=====
void remove(int row) {
if (row < 0 || row >= m_data.size())
return;
// begin 自动发送 rowsAboutToBeRemoved
beginRemoveRows(QModelIndex(), row, row);
m_data.removeAt(row);
// end 自动发送 rowsRemoved
endRemoveRows();
}
// ===== 排序(使用 layoutAboutToBeChanged/layoutChanged)=====
void sortAscending() {
// 布局即将改变
emit layoutAboutToBeChanged();
// 排序
std::sort(m_data.begin(), m_data.end());
// 布局已改变
emit layoutChanged();
}
void sortDescending() {
emit layoutAboutToBeChanged();
std::sort(m_data.begin(), m_data.end(), std::greater<QString>());
emit layoutChanged();
}
// ===== 完全重置(使用 beginResetModel/endResetModel)=====
void loadNewData(const QStringList &newData) {
// 模型即将重置
beginResetModel();
m_data = newData;
// 模型已重置
endResetModel();
}
void clear() {
beginResetModel();
m_data.clear();
endResetModel();
}
};
使用示例:
cpp
#include <QApplication>
#include <QListView>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
SignalDemoModel *model = new SignalDemoModel;
QListView *view = new QListView;
view->setModel(model);
// 添加按钮
QPushButton *appendBtn = new QPushButton("Append");
QObject::connect(appendBtn, &QPushButton::clicked, [=]() {
model->append("New Item"); // 触发 insertRows 信号
});
// 删除按钮
QPushButton *removeBtn = new QPushButton("Remove First");
QObject::connect(removeBtn, &QPushButton::clicked, [=]() {
if (model->rowCount() > 0)
model->remove(0); // 触发 removeRows 信号
});
// 排序按钮
QPushButton *sortBtn = new QPushButton("Sort");
QObject::connect(sortBtn, &QPushButton::clicked, [=]() {
model->sortAscending(); // 触发 layoutChanged 信号
});
// 重置按钮
QPushButton *resetBtn = new QPushButton("Reset");
QObject::connect(resetBtn, &QPushButton::clicked, [=]() {
model->loadNewData({"X", "Y", "Z"}); // 触发 modelReset 信号
});
// 布局
QWidget window;
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addWidget(view);
QHBoxLayout *btnLayout = new QHBoxLayout;
btnLayout->addWidget(appendBtn);
btnLayout->addWidget(removeBtn);
btnLayout->addWidget(sortBtn);
btnLayout->addWidget(resetBtn);
mainLayout->addLayout(btnLayout);
window.setLayout(mainLayout);
window.resize(400, 300);
window.show();
return app.exec();
}
本节小结:
✅ dataChanged() :数据内容改变时使用
✅ layoutChanged() :数据位置/结构改变时使用(排序、过滤)
✅ rowsInserted/Removed :通过 begin/end 方法自动发送
✅ columnsInserted/Removed :通过 begin/end 方法自动发送
✅ modelReset() :完全重置模型时使用
✅ headerDataChanged():表头数据改变时使用
✅ 关键规则:
-
数据改变时必须发送相应信号,否则 View 不会更新
-
使用 begin/end 方法,不要手动发送 rows/columns 信号
-
根据操作类型选择合适的信号
-
指定具体的角色可以提高性能
-
dataChanged() - 数据变化通知
-
layoutChanged() - 布局变化
-
layoutAboutToBeChanged() - 布局即将变化
-
rowsInserted() / rowsRemoved() - 行插入/删除
-
columnsInserted() / columnsRemoved() - 列插入/删除
-
modelReset() - 模型重置
-
信号的正确使用时机
3.3 列表模型:QAbstractListModel
QAbstractListModel 是为一维列表数据设计的模型基类,它简化了 QAbstractItemModel 的接口。
3.3.1 QAbstractListModel特点
核心特点:
- 一维结构:只有行,没有层级关系
- 简化接口 :不需要实现
parent()和复杂的index() - 单列默认:通常只有一列(columnCount 默认返回 1)
- 自动实现 :
index()和parent()已有默认实现
继承关系:
cpp
QObject
└─ QAbstractItemModel
└─ QAbstractListModel
└─ QStringListModel (Qt 提供)
└─ (自定义列表模型)
与 QAbstractItemModel 的区别:
| 特性 | QAbstractItemModel | QAbstractListModel |
|---|---|---|
| 数据结构 | 支持列表/表格/树 | 仅列表 |
| parent() | 必须实现 | 已实现(总返回无效索引) |
| index() | 必须实现 | 已实现 |
| 层级支持 | 是 | 否 |
| 使用难度 | 较难 | 简单 |
3.3.2 需要实现的最小接口
必须实现的方法:
cpp
class MyListModel : public QAbstractListModel {
public:
// 1. 返回行数
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
// 2. 返回数据
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
};
可选但常用的方法:
cpp
// 使模型可编辑
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
// 使模型可增删
bool insertRows(int row, int count, const QModelIndex &parent) override;
bool removeRows(int row, int count, const QModelIndex &parent) override;
最小示例:
cpp
class SimpleListModel : public QAbstractListModel {
QStringList m_data;
public:
SimpleListModel() {
m_data << "Item 1" << "Item 2" << "Item 3";
}
// 必须实现:返回行数
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
return parent.isValid() ? 0 : m_data.size();
}
// 必须实现:返回数据
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
if (!index.isValid() || index.row() >= m_data.size())
return QVariant();
if (role == Qt::DisplayRole)
return m_data[index.row()];
return QVariant();
}
};
3.3.3 实战:自定义字符串列表模型
这个示例实现一个完整的可编辑、可增删的字符串列表模型。
cpp
#include <QAbstractListModel>
#include <QStringList>
#include <QColor>
#include <QFont>
class EditableStringListModel : public QAbstractListModel {
Q_OBJECT
private:
QStringList m_data;
QSet<int> m_highlightedRows; // 高亮的行
public:
EditableStringListModel(QObject *parent = nullptr)
: QAbstractListModel(parent) {
m_data << "Apple" << "Banana" << "Cherry" << "Date" << "Elderberry";
}
// ===== 必须实现的方法 =====
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
return parent.isValid() ? 0 : m_data.size();
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
if (!index.isValid() || index.row() >= m_data.size())
return QVariant();
switch (role) {
case Qt::DisplayRole:
case Qt::EditRole:
return m_data[index.row()];
case Qt::BackgroundRole:
// 高亮行显示黄色背景
if (m_highlightedRows.contains(index.row()))
return QColor(Qt::yellow).lighter(160);
break;
case Qt::FontRole:
// 高亮行加粗
if (m_highlightedRows.contains(index.row())) {
QFont font;
font.setBold(true);
return font;
}
break;
case Qt::ToolTipRole:
return QString("Item %1: %2").arg(index.row()).arg(m_data[index.row()]);
}
return QVariant();
}
// ===== 可编辑支持 =====
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override {
if (!index.isValid() || role != Qt::EditRole)
return false;
if (index.row() >= m_data.size())
return false;
QString newValue = value.toString().trimmed();
if (newValue.isEmpty())
return false; // 不允许空值
m_data[index.row()] = newValue;
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
return true;
}
Qt::ItemFlags flags(const QModelIndex &index) const override {
if (!index.isValid())
return Qt::NoItemFlags;
return QAbstractListModel::flags(index) | Qt::ItemIsEditable;
}
// ===== 增删支持 =====
bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override {
if (parent.isValid())
return false;
beginInsertRows(parent, row, row + count - 1);
for (int i = 0; i < count; ++i) {
m_data.insert(row, QString("New Item"));
}
endInsertRows();
return true;
}
bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override {
if (parent.isValid())
return false;
if (row < 0 || row + count > m_data.size())
return false;
beginRemoveRows(parent, row, row + count - 1);
for (int i = 0; i < count; ++i) {
m_data.removeAt(row);
// 更新高亮集合
m_highlightedRows.remove(row);
}
endRemoveRows();
return true;
}
// ===== 自定义功能 =====
void appendItem(const QString &text) {
int row = m_data.size();
insertRows(row, 1);
setData(index(row), text);
}
void highlightRow(int row) {
if (row >= 0 && row < m_data.size()) {
m_highlightedRows.insert(row);
QModelIndex idx = index(row);
emit dataChanged(idx, idx, {Qt::BackgroundRole, Qt::FontRole});
}
}
void clearHighlights() {
m_highlightedRows.clear();
if (m_data.isEmpty())
return;
emit dataChanged(index(0), index(m_data.size() - 1), {Qt::BackgroundRole, Qt::FontRole});
}
QStringList stringList() const {
return m_data;
}
};
使用示例:
cpp
#include <QApplication>
#include <QListView>
#include <QPushButton>
#include <QVBoxLayout>
#include <QInputDialog>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
EditableStringListModel *model = new EditableStringListModel;
QListView *view = new QListView;
view->setModel(model);
view->setEditTriggers(QAbstractItemView::DoubleClicked);
// 添加按钮
QPushButton *addBtn = new QPushButton("Add Item");
QObject::connect(addBtn, &QPushButton::clicked, [=]() {
bool ok;
QString text = QInputDialog::getText(nullptr, "Add Item", "Enter text:",
QLineEdit::Normal, "", &ok);
if (ok && !text.isEmpty()) {
model->appendItem(text);
}
});
// 删除按钮
QPushButton *removeBtn = new QPushButton("Remove Selected");
QObject::connect(removeBtn, &QPushButton::clicked, [=]() {
QModelIndex current = view->currentIndex();
if (current.isValid()) {
model->removeRows(current.row(), 1);
}
});
// 高亮按钮
QPushButton *highlightBtn = new QPushButton("Highlight Selected");
QObject::connect(highlightBtn, &QPushButton::clicked, [=]() {
QModelIndex current = view->currentIndex();
if (current.isValid()) {
model->highlightRow(current.row());
}
});
QWidget window;
QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(view);
layout->addWidget(addBtn);
layout->addWidget(removeBtn);
layout->addWidget(highlightBtn);
window.setLayout(layout);
window.resize(300, 400);
window.show();
return app.exec();
}
3.3.4 实战:联系人列表模型
这个示例展示如何管理复杂的结构化数据。
cpp
#include <QAbstractListModel>
#include <QDateTime>
#include <QIcon>
#include <QPixmap>
// 联系人数据结构
struct Contact {
int id;
QString name;
QString phone;
QString email;
QDateTime addedDate;
bool isFavorite;
QPixmap avatar; // 头像
Contact(int id, const QString &name, const QString &phone, const QString &email)
: id(id), name(name), phone(phone), email(email),
addedDate(QDateTime::currentDateTime()), isFavorite(false) {}
};
class ContactListModel : public QAbstractListModel {
Q_OBJECT
private:
QList<Contact> m_contacts;
int m_nextId = 1;
public:
// 自定义角色
enum Roles {
IdRole = Qt::UserRole + 1,
NameRole,
PhoneRole,
EmailRole,
FavoriteRole,
AvatarRole,
AddedDateRole
};
ContactListModel(QObject *parent = nullptr) : QAbstractListModel(parent) {
// 添加示例数据
addContact("Alice", "123-4567", "alice@example.com");
addContact("Bob", "234-5678", "bob@example.com");
addContact("Charlie", "345-6789", "charlie@example.com");
}
// ===== 基本接口 =====
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
return parent.isValid() ? 0 : m_contacts.size();
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
if (!index.isValid() || index.row() >= m_contacts.size())
return QVariant();
const Contact &contact = m_contacts[index.row()];
switch (role) {
case Qt::DisplayRole:
return QString("%1\n%2").arg(contact.name, contact.phone);
case Qt::EditRole:
case NameRole:
return contact.name;
case Qt::DecorationRole:
case AvatarRole:
if (!contact.avatar.isNull())
return contact.avatar;
// 默认头像
return QIcon::fromTheme("user");
case Qt::ToolTipRole:
return QString("<b>%1</b><br>Phone: %2<br>Email: %3<br>Added: %4")
.arg(contact.name, contact.phone, contact.email,
contact.addedDate.toString("yyyy-MM-dd"));
case Qt::BackgroundRole:
if (contact.isFavorite)
return QColor(255, 250, 205); // 收藏夹淡黄色
break;
case Qt::FontRole:
if (contact.isFavorite) {
QFont font;
font.setBold(true);
return font;
}
break;
case IdRole:
return contact.id;
case PhoneRole:
return contact.phone;
case EmailRole:
return contact.email;
case FavoriteRole:
return contact.isFavorite;
case AddedDateRole:
return contact.addedDate;
}
return QVariant();
}
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override {
if (!index.isValid() || index.row() >= m_contacts.size())
return false;
Contact &contact = m_contacts[index.row()];
switch (role) {
case Qt::EditRole:
case NameRole:
contact.name = value.toString();
emit dataChanged(index, index, {Qt::DisplayRole, NameRole});
return true;
case PhoneRole:
contact.phone = value.toString();
emit dataChanged(index, index, {Qt::DisplayRole, PhoneRole});
return true;
case EmailRole:
contact.email = value.toString();
emit dataChanged(index, index, {EmailRole});
return true;
case FavoriteRole:
contact.isFavorite = value.toBool();
emit dataChanged(index, index, {Qt::BackgroundRole, Qt::FontRole, FavoriteRole});
return true;
}
return false;
}
Qt::ItemFlags flags(const QModelIndex &index) const override {
if (!index.isValid())
return Qt::NoItemFlags;
return QAbstractListModel::flags(index) | Qt::ItemIsEditable;
}
// ===== QML 支持(可选)=====
QHash<int, QByteArray> roleNames() const override {
QHash<int, QByteArray> roles;
roles[IdRole] = "contactId";
roles[NameRole] = "name";
roles[PhoneRole] = "phone";
roles[EmailRole] = "email";
roles[FavoriteRole] = "isFavorite";
roles[AvatarRole] = "avatar";
return roles;
}
// ===== 联系人操作 =====
void addContact(const QString &name, const QString &phone, const QString &email) {
int row = m_contacts.size();
beginInsertRows(QModelIndex(), row, row);
m_contacts.append(Contact(m_nextId++, name, phone, email));
endInsertRows();
}
void removeContact(int row) {
if (row < 0 || row >= m_contacts.size())
return;
beginRemoveRows(QModelIndex(), row, row);
m_contacts.removeAt(row);
endRemoveRows();
}
void toggleFavorite(int row) {
if (row < 0 || row >= m_contacts.size())
return;
m_contacts[row].isFavorite = !m_contacts[row].isFavorite;
QModelIndex idx = index(row);
emit dataChanged(idx, idx, {Qt::BackgroundRole, Qt::FontRole, FavoriteRole});
}
Contact* getContact(int row) {
if (row >= 0 && row < m_contacts.size())
return &m_contacts[row];
return nullptr;
}
QList<Contact> getFavorites() const {
QList<Contact> favorites;
for (const Contact &c : m_contacts) {
if (c.isFavorite)
favorites.append(c);
}
return favorites;
}
};
本节小结:
✅ QAbstractListModel 专为一维列表设计,使用简单
✅ 最小接口 :只需实现 rowCount() 和 data()
✅ 扩展功能 :可添加编辑、增删、自定义角色等功能
✅ 适用场景:联系人、任务列表、聊天记录等线性数据
- QAbstractListModel特点
- 需要实现的最小接口
- 实战:自定义字符串列表模型
- 实战:联系人列表模型
3.4 表格模型:QAbstractTableModel
QAbstractTable Model 是为二维表格数据设计的模型基类,适用于类似电子表格的数据展示。
3.4.1 QAbstractTableModel特点
核心特点:
- 二维结构:有行和列,无层级关系
- 简化接口 :
parent()已实现,index()已简化 - 表格专用 :专为
QTableView优化 - 无子项:所有项都是平级的
与其他模型的对比:
| 特性 | QAbstractListModel | QAbstractTableModel | QAbstractItemModel |
|---|---|---|---|
| 维度 | 一维(行) | 二维(行+列) | 多维(树) |
| parent() | 已实现 | 已实现 | 必须实现 |
| columnCount() | 返回 1 | 必须实现 | 必须实现 |
| 适用视图 | QListView | QTableView | 所有视图 |
3.4.2 需要实现的最小接口
必须实现的方法:
cpp
class MyTableModel : public QAbstractTableModel {
public:
// 1. 返回行数
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
// 2. 返回列数
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
// 3. 返回数据
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
};
最小示例:
cpp
class SimpleTableModel : public QAbstractTableModel {
QVector<QVector<QString>> m_data; // 二维数组
public:
SimpleTableModel() {
// 初始化 3x3 表格
m_data = {
{"A1", "A2", "A3"},
{"B1", "B2", "B3"},
{"C1", "C2", "C3"}
};
}
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
return parent.isValid() ? 0 : m_data.size();
}
int columnCount(const QModelIndex &parent = QModelIndex()) const override {
return parent.isValid() ? 0 : (m_data.isEmpty() ? 0 : m_data[0].size());
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
if (!index.isValid())
return QVariant();
if (role == Qt::DisplayRole)
return m_data[index.row()][index.column()];
return QVariant();
}
};
3.4.3 实战:二维数组表格模型
完整的可编辑、可增删行列的表格模型。
cpp
#include <QAbstractTableModel>
#include <QVector>
#include <QColor>
class Matrix TableModel : public QAbstractTableModel {
Q_OBJECT
private:
QVector<QVector<double>> m_data;
int m_rows;
int m_cols;
QStringList m_headers;
public:
MatrixTableModel(int rows, int cols, QObject *parent = nullptr)
: QAbstractTableModel(parent), m_rows(rows), m_cols(cols) {
// 初始化数据
m_data.resize(m_rows);
for (int i = 0; i < m_rows; ++i) {
m_data[i].resize(m_cols);
for (int j = 0; j < m_cols; ++j) {
m_data[i][j] = 0.0;
}
}
// 初始化表头
for (int i = 0; i < m_cols; ++i) {
m_headers << QString("Col %1").arg(i);
}
}
// ===== 基本接口 =====
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
return parent.isValid() ? 0 : m_rows;
}
int columnCount(const QModelIndex &parent = QModelIndex()) const override {
return parent.isValid() ? 0 : m_cols;
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
if (!index.isValid())
return QVariant();
if (index.row() >= m_rows || index.column() >= m_cols)
return QVariant();
double value = m_data[index.row()][index.column()];
switch (role) {
case Qt::DisplayRole:
return QString::number(value, 'f', 2);
case Qt::EditRole:
return value;
case Qt::TextAlignmentRole:
return Qt::AlignRight | Qt::AlignVCenter;
case Qt::BackgroundRole:
// 正数绿色,负数红色
if (value > 0)
return QColor(200, 255, 200);
else if (value < 0)
return QColor(255, 200, 200);
break;
case Qt::ForegroundRole:
if (value == 0)
return QColor(Qt::gray);
break;
}
return QVariant();
}
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override {
if (!index.isValid() || role != Qt::EditRole)
return false;
bool ok;
double newValue = value.toDouble(&ok);
if (!ok)
return false;
m_data[index.row()][index.column()] = newValue;
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole, Qt::BackgroundRole});
return true;
}
Qt::ItemFlags flags(const QModelIndex &index) const override {
if (!index.isValid())
return Qt::NoItemFlags;
return QAbstractTableModel::flags(index) | Qt::ItemIsEditable;
}
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override {
if (role == Qt::DisplayRole) {
if (orientation == Qt::Horizontal) {
return m_headers.value(section);
} else {
return section + 1;
}
}
return QVariant();
}
// ===== 行列操作 =====
bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override {
if (parent.isValid())
return false;
beginInsertRows(parent, row, row + count - 1);
for (int i = 0; i < count; ++i) {
QVector<double> newRow(m_cols, 0.0);
m_data.insert(row, newRow);
}
m_rows += count;
endInsertRows();
return true;
}
bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override {
if (parent.isValid() || row < 0 || row + count > m_rows)
return false;
beginRemoveRows(parent, row, row + count - 1);
for (int i = 0; i < count; ++i) {
m_data.removeAt(row);
}
m_rows -= count;
endRemoveRows();
return true;
}
bool insertColumns(int column, int count, const QModelIndex &parent = QModelIndex()) override {
if (parent.isValid())
return false;
beginInsertColumns(parent, column, column + count - 1);
for (int i = 0; i < m_rows; ++i) {
for (int j = 0; j < count; ++j) {
m_data[i].insert(column, 0.0);
}
}
for (int i = 0; i < count; ++i) {
m_headers.insert(column, QString("Col %1").arg(m_cols + i));
}
m_cols += count;
endInsertColumns();
return true;
}
bool removeColumns(int column, int count, const QModelIndex &parent = QModelIndex()) override {
if (parent.isValid() || column < 0 || column + count > m_cols)
return false;
beginRemoveColumns(parent, column, column + count - 1);
for (int i = 0; i < m_rows; ++i) {
for (int j = 0; j < count; ++j) {
m_data[i].removeAt(column);
}
}
for (int i = 0; i < count; ++i) {
m_headers.removeAt(column);
}
m_cols -= count;
endRemoveColumns();
return true;
}
};
3.4.4 实战:学生成绩管理表格模型
这个示例展示如何管理结构化的学生成绩数据,包含统计功能。
cpp
#include <QAbstractTableModel>
#include <QVector>
#include <QColor>
#include <QFont>
// 学生数据结构
struct Student {
QString id; // 学号
QString name; // 姓名
int math; // 数学成绩
int english; // 英语成绩
int physics; // 物理成绩
// 计算总分
int total() const {
return math + english + physics;
}
// 计算平均分
double average() const {
return total() / 3.0;
}
};
class StudentScoreModel : public QAbstractTableModel {
Q_OBJECT
private:
QList<Student> m_students;
QStringList m_headers;
public:
StudentScoreModel(QObject *parent = nullptr) : QAbstractTableModel(parent) {
m_headers << "学号" << "姓名" << "数学" << "英语" << "物理" << "总分" << "平均分";
// 添加示例数据
m_students = {
{"2024001", "张三", 85, 90, 88, },
{"2024002", "李四", 78, 82, 75},
{"2024003", "王五", 92, 88, 95},
{"2024004", "赵六", 68, 72, 70},
{"2024005", "孙七", 95, 93, 97}
};
}
// ===== 基本接口 =====
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
return parent.isValid() ? 0 : m_students.size();
}
int columnCount(const QModelIndex &parent = QModelIndex()) const override {
return parent.isValid() ? 0 : 7; // 学号、姓名、3科成绩、总分、平均分
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
if (!index.isValid() || index.row() >= m_students.size())
return QVariant();
const Student &student = m_students[index.row()];
// 显示角色
if (role == Qt::DisplayRole) {
switch (index.column()) {
case 0: return student.id;
case 1: return student.name;
case 2: return student.math;
case 3: return student.english;
case 4: return student.physics;
case 5: return student.total();
case 6: return QString::number(student.average(), 'f', 1);
}
}
// 编辑角色
else if (role == Qt::EditRole) {
switch (index.column()) {
case 0: return student.id;
case 1: return student.name;
case 2: return student.math;
case 3: return student.english;
case 4: return student.physics;
}
}
// 对齐方式
else if (role == Qt::TextAlignmentRole) {
if (index.column() == 0 || index.column() == 1)
return Qt::AlignLeft | Qt::AlignVCenter;
else
return Qt::AlignCenter;
}
// 背景色:成绩预警
else if (role == Qt::BackgroundRole) {
if (index.column() >= 2 && index.column() <= 4) {
int score = data(index, Qt::DisplayRole).toInt();
if (score < 60)
return QColor(255, 200, 200); // 不及格红色
else if (score >= 90)
return QColor(200, 255, 200); // 优秀绿色
} else if (index.column() == 6) {
// 平均分颜色
double avg = student.average();
if (avg >= 90)
return QColor(150, 255, 150);
else if (avg < 60)
return QColor(255, 150, 150);
}
}
// 字体:高分加粗
else if (role == Qt::FontRole) {
if (index.column() >= 2 && index.column() <= 4) {
int score = data(index, Qt::DisplayRole).toInt();
if (score >= 95) {
QFont font;
font.setBold(true);
return font;
}
}
}
// 提示信息
else if (role == Qt::ToolTipRole) {
return QString("%1 (%2)\n总分: %3\n平均分: %4")
.arg(student.name, student.id)
.arg(student.total())
.arg(student.average(), 0, 'f', 1);
}
return QVariant();
}
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override {
if (!index.isValid() || role != Qt::EditRole)
return false;
if (index.row() >= m_students.size())
return false;
Student &student = m_students[index.row()];
switch (index.column()) {
case 0: // 学号
student.id = value.toString();
break;
case 1: // 姓名
student.name = value.toString();
break;
case 2: // 数学
case 3: // 英语
case 4: // 物理
{
bool ok;
int score = value.toInt(&ok);
if (!ok || score < 0 || score > 100)
return false;
switch (index.column()) {
case 2: student.math = score; break;
case 3: student.english = score; break;
case 4: student.physics = score; break;
}
// 成绩改变,总分和平均分也改变
QModelIndex totalIdx = this->index(index.row(), 5);
QModelIndex avgIdx = this->index(index.row(), 6);
emit dataChanged(index, index);
emit dataChanged(totalIdx, avgIdx);
return true;
}
case 5: // 总分(只读)
case 6: // 平均分(只读)
return false;
}
emit dataChanged(index, index);
return true;
}
Qt::ItemFlags flags(const QModelIndex &index) const override {
if (!index.isValid())
return Qt::NoItemFlags;
Qt::ItemFlags flags = QAbstractTableModel::flags(index);
// 总分和平均分列不可编辑
if (index.column() != 5 && index.column() != 6)
flags |= Qt::ItemIsEditable;
return flags;
}
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override {
if (role == Qt::DisplayRole) {
if (orientation == Qt::Horizontal) {
return m_headers.value(section);
} else {
return section + 1;
}
} else if (role == Qt::FontRole && orientation == Qt::Horizontal) {
QFont font;
font.setBold(true);
return font;
}
return QVariant();
}
// ===== 学生管理 =====
void addStudent(const QString &id, const QString &name, int math, int english, int physics) {
int row = m_students.size();
beginInsertRows(QModelIndex(), row, row);
Student student;
student.id = id;
student.name = name;
student.math = math;
student.english = english;
student.physics = physics;
m_students.append(student);
endInsertRows();
}
void removeStudent(int row) {
if (row < 0 || row >= m_students.size())
return;
beginRemoveRows(QModelIndex(), row, row);
m_students.removeAt(row);
endRemoveRows();
}
// ===== 统计功能 =====
double getClassAverage() const {
if (m_students.isEmpty())
return 0.0;
double sum = 0.0;
for (const Student &s : m_students) {
sum += s.average();
}
return sum / m_students.size();
}
Student getTopStudent() const {
if (m_students.isEmpty())
return Student();
const Student *top = &m_students[0];
for (const Student &s : m_students) {
if (s.total() > top->total())
top = &s;
}
return *top;
}
int getFailCount() const {
int count = 0;
for (const Student &s : m_students) {
if (s.math < 60 || s.english < 60 || s.physics < 60)
count++;
}
return count;
}
QList<Student> getStudentsByScore(int minScore) const {
QList<Student> result;
for (const Student &s : m_students) {
if (s.average() >= minScore)
result.append(s);
}
return result;
}
};
使用示例:
cpp
#include <QApplication>
#include <QTableView>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QMessageBox>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
StudentScoreModel *model = new StudentScoreModel;
QTableView *view = new QTableView;
view->setModel(model);
view->horizontalHeader()->setStretchLastSection(true);
view->setSelectionBehavior(QAbstractItemView::SelectRows);
view->setAlternatingRowColors(true);
// 统计标签
QLabel *statsLabel = new QLabel;
auto updateStats = [=]() {
QString stats = QString("班级平均分: %1 | 不及格人数: %2")
.arg(model->getClassAverage(), 0, 'f', 1)
.arg(model->getFailCount());
statsLabel->setText(stats);
};
updateStats();
// 显示第一名按钮
QPushButton *topBtn = new QPushButton("查看第一名");
QObject::connect(topBtn, &QPushButton::clicked, [=]() {
Student top = model->getTopStudent();
QMessageBox::information(nullptr, "第一名",
QString("%1 (%2)\n总分: %3\n平均分: %4")
.arg(top.name, top.id)
.arg(top.total())
.arg(top.average(), 0, 'f', 1));
});
// 删除按钮
QPushButton *delBtn = new QPushButton("删除选中");
QObject::connect(delBtn, &QPushButton::clicked, [=]() {
QModelIndex current = view->currentIndex();
if (current.isValid()) {
model->removeStudent(current.row());
updateStats();
}
});
// 布局
QWidget window;
QVBoxLayout *mainLayout = new QVBoxLayout;
mainLayout->addWidget(view);
mainLayout->addWidget(statsLabel);
QHBoxLayout *btnLayout = new QHBoxLayout;
btnLayout->addWidget(topBtn);
btnLayout->addWidget(delBtn);
mainLayout->addLayout(btnLayout);
window.setLayout(mainLayout);
window.resize(800, 400);
window.show();
return app.exec();
}
3.4.5 动态添加/删除行列
完整的动态操作示例:
cpp
// 在表格视图中添加右键菜单
QMenu *menu = new QMenu(view);
QAction *insertRowAction = menu->addAction("插入行");
QObject::connect(insertRowAction, &QAction::triggered, [=]() {
QModelIndex current = view->currentIndex();
int row = current.isValid() ? current.row() : model->rowCount();
model->insertRows(row, 1);
});
QAction *removeRowAction = menu->addAction("删除行");
QObject::connect(removeRowAction, &QAction::triggered, [=]() {
QModelIndex current = view->currentIndex();
if (current.isValid()) {
model->removeRows(current.row(), 1);
}
});
QAction *insertColAction = menu->addAction("插入列");
QObject::connect(insertColAction, &QAction::triggered, [=]() {
QModelIndex current = view->currentIndex();
int col = current.isValid() ? current.column() : model->columnCount();
model->insertColumns(col, 1);
});
QAction *removeColAction = menu->addAction("删除列");
QObject::connect(removeColAction, &QAction::triggered, [=]() {
QModelIndex current = view->currentIndex();
if (current.isValid()) {
model->removeColumns(current.column(), 1);
}
});
view->setContextMenuPolicy(Qt::CustomContextMenu);
QObject::connect(view, &QTableView::customContextMenuRequested, [=](const QPoint &pos) {
menu->exec(view->viewport()->mapToGlobal(pos));
});
本节小结:
✅ QAbstractTableModel 专为二维表格设计
✅ 必须实现 :rowCount(), columnCount(), data()
✅ 行列操作 :通过 insertRows/Columns() 和 removeRows/Columns() 实现
✅ 适用场景:成绩管理、数据表格、矩阵运算等二维数据展示
- QAbstractTableModel特点
- 需要实现的最小接口
- 实战:二维数组表格模型
- 实战:学生成绩管理表格模型
- 动态添加/删除行列
3.5 树形模型:QAbstractItemModel
树形模型是 Model/View 架构中最复杂但也最强大的部分,它可以表示任意层级的数据结构。
3.5.1 树形结构的表示方法
树节点类设计
要实现树形模型,首先需要设计树节点类来存储数据和维护父子关系:
cpp
// 基础树节点类
class TreeItem {
private:
QVector<TreeItem*> m_children; // 子节点列表
TreeItem* m_parent; // 父节点指针
QVector<QVariant> m_itemData; // 节点数据(可以是多列)
public:
explicit TreeItem(const QVector<QVariant> &data, TreeItem *parent = nullptr)
: m_itemData(data), m_parent(parent) {}
~TreeItem() {
qDeleteAll(m_children); // 递归删除所有子节点
}
// 添加子节点
void appendChild(TreeItem *child) {
m_children.append(child);
}
// 获取子节点
TreeItem *child(int row) {
if (row < 0 || row >= m_children.size())
return nullptr;
return m_children.at(row);
}
// 子节点数量
int childCount() const {
return m_children.size();
}
// 列数
int columnCount() const {
return m_itemData.size();
}
// 获取数据
QVariant data(int column) const {
if (column < 0 || column >= m_itemData.size())
return QVariant();
return m_itemData.at(column);
}
// 设置数据
bool setData(int column, const QVariant &value) {
if (column < 0 || column >= m_itemData.size())
return false;
m_itemData[column] = value;
return true;
}
// 获取父节点
TreeItem *parentItem() {
return m_parent;
}
// 获取当前节点在父节点中的行号
int row() const {
if (m_parent)
return m_parent->m_children.indexOf(const_cast<TreeItem*>(this));
return 0;
}
// 插入子节点
bool insertChildren(int position, int count, int columns) {
if (position < 0 || position > m_children.size())
return false;
for (int row = 0; row < count; ++row) {
QVector<QVariant> data(columns);
TreeItem *item = new TreeItem(data, this);
m_children.insert(position, item);
}
return true;
}
// 删除子节点
bool removeChildren(int position, int count) {
if (position < 0 || position + count > m_children.size())
return false;
for (int row = 0; row < count; ++row) {
delete m_children.takeAt(position);
}
return true;
}
};
树形结构示意图:
根节点 (不可见)
├── 节点A (row=0, parent=root)
│ ├── 节点A1 (row=0, parent=A)
│ └── 节点A2 (row=1, parent=A)
├── 节点B (row=1, parent=root)
│ ├── 节点B1 (row=0, parent=B)
│ ├── 节点B2 (row=1, parent=B)
│ └── 节点B3 (row=2, parent=B)
└── 节点C (row=2, parent=root)
3.5.2 parent()和index()的实现要点
关键约束:
cpp
// 对于任何有效索引 idx:
QModelIndex idx = model->index(row, col, parent);
assert(model->parent(idx) == parent); // 必须成立
// 对于顶层项:
QModelIndex topIdx = model->index(0, 0);
assert(!model->parent(topIdx).isValid()); // 父索引无效
index() 实现:
cpp
QModelIndex TreeModel::index(int row, int column, const QModelIndex &parent) const {
// 1. 检查参数有效性
if (!hasIndex(row, column, parent))
return QModelIndex();
// 2. 获取父节点
TreeItem *parentItem;
if (!parent.isValid())
parentItem = m_rootItem; // 无效索引 → 根节点
else
parentItem = static_cast<TreeItem*>(parent.internalPointer());
// 3. 获取子节点
TreeItem *childItem = parentItem->child(row);
if (childItem)
// 4. 创建索引,将子节点指针存储在 internalPointer 中
return createIndex(row, column, childItem);
else
return QModelIndex();
}
parent() 实现:
cpp
QModelIndex TreeModel::parent(const QModelIndex &child) const {
// 1. 检查索引有效性
if (!child.isValid())
return QModelIndex();
// 2. 从索引中获取子节点
TreeItem *childItem = static_cast<TreeItem*>(child.internalPointer());
TreeItem *parentItem = childItem->parentItem();
// 3. 根节点的子项没有父索引
if (parentItem == m_rootItem)
return QModelIndex();
// 4. 创建父节点的索引
// 需要找到父节点在其父节点中的行号
return createIndex(parentItem->row(), 0, parentItem);
}
实现要点:
- internalPointer 存储节点指针:这是关键!
- 根节点特殊处理:根节点通常不可见,其子项的 parent 返回无效索引
- row() 方法:节点需要知道自己在父节点中的位置
- 性能考虑:index() 和 parent() 会被频繁调用,必须快速执行
3.5.3 internalPointer的使用
internalPointer 的作用:
internalPointer 是 QModelIndex 的核心机制,用于在树形模型中快速定位节点。
cpp
// 存储指针
QModelIndex idx = createIndex(row, col, nodePointer);
// 恢复指针
TreeItem *item = static_cast<TreeItem*>(idx.internalPointer());
为什么需要 internalPointer?
不使用 internalPointer 的话,parent() 实现会非常困难:
cpp
// ❌ 没有 internalPointer 时的困境
QModelIndex parent(const QModelIndex &child) const {
// 只有 row 和 column,如何找到父节点?
int row = child.row();
int col = child.column();
// ??? 无法定位节点 ???
}
// ✅ 有了 internalPointer
QModelIndex parent(const QModelIndex &child) const {
TreeItem *childItem = static_cast<TreeItem*>(child.internalPointer());
TreeItem *parentItem = childItem->parentItem(); // 轻松获取父节点
// ...
}
internalPointer 的注意事项:
- 类型转换:必须转换为正确的指针类型
- 内存管理:Model 负责节点的内存管理,不是 internalPointer
- 持久性:普通索引失效后,internalPointer 可能指向已释放的内存
- 替代方案 :可以使用
quintptr存储整数 ID
3.5.4 实战:文件系统树形模型
这个示例实现一个简化的文件系统浏览器。
cpp
#include <QAbstractItemModel>
#include <QFileInfo>
#include <QDir>
#include <QIcon>
#include <QFileIconProvider>
// 文件树节点
class FileItem {
private:
QString m_filePath;
FileItem *m_parent;
QVector<FileItem*> m_children;
bool m_childrenLoaded; // 是否已加载子项
public:
explicit FileItem(const QString &path, FileItem *parent = nullptr)
: m_filePath(path), m_parent(parent), m_childrenLoaded(false) {}
~FileItem() {
qDeleteAll(m_children);
}
QString filePath() const { return m_filePath; }
QString fileName() const { return QFileInfo(m_filePath).fileName(); }
FileItem *parentItem() { return m_parent; }
// 懒加载子项
void loadChildren() {
if (m_childrenLoaded)
return;
QFileInfo info(m_filePath);
if (info.isDir()) {
QDir dir(m_filePath);
QFileInfoList entries = dir.entryInfoList(QDir::AllEntries | QDir::NoDotAndDotDot);
for (const QFileInfo &entry : entries) {
FileItem *child = new FileItem(entry.absoluteFilePath(), this);
m_children.append(child);
}
}
m_childrenLoaded = true;
}
FileItem *child(int row) {
loadChildren();
if (row < 0 || row >= m_children.size())
return nullptr;
return m_children.at(row);
}
int childCount() {
loadChildren();
return m_children.size();
}
int row() const {
if (m_parent)
return m_parent->m_children.indexOf(const_cast<FileItem*>(this));
return 0;
}
};
// 文件系统模型
class FileSystemTreeModel : public QAbstractItemModel {
Q_OBJECT
private:
FileItem *m_rootItem;
QFileIconProvider m_iconProvider;
public:
explicit FileSystemTreeModel(const QString &rootPath, QObject *parent = nullptr)
: QAbstractItemModel(parent) {
m_rootItem = new FileItem(rootPath);
}
~FileSystemTreeModel() {
delete m_rootItem;
}
// ===== 必须实现的方法 =====
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override {
if (!hasIndex(row, column, parent))
return QModelIndex();
FileItem *parentItem;
if (!parent.isValid())
parentItem = m_rootItem;
else
parentItem = static_cast<FileItem*>(parent.internalPointer());
FileItem *childItem = parentItem->child(row);
if (childItem)
return createIndex(row, column, childItem);
else
return QModelIndex();
}
QModelIndex parent(const QModelIndex &child) const override {
if (!child.isValid())
return QModelIndex();
FileItem *childItem = static_cast<FileItem*>(child.internalPointer());
FileItem *parentItem = childItem->parentItem();
if (parentItem == m_rootItem)
return QModelIndex();
return createIndex(parentItem->row(), 0, parentItem);
}
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
FileItem *parentItem;
if (parent.column() > 0)
return 0;
if (!parent.isValid())
parentItem = m_rootItem;
else
parentItem = static_cast<FileItem*>(parent.internalPointer());
return parentItem->childCount();
}
int columnCount(const QModelIndex &parent = QModelIndex()) const override {
Q_UNUSED(parent);
return 2; // 名称、路径
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
if (!index.isValid())
return QVariant();
FileItem *item = static_cast<FileItem*>(index.internalPointer());
QFileInfo info(item->filePath());
if (role == Qt::DisplayRole) {
if (index.column() == 0)
return item->fileName();
else if (index.column() == 1)
return item->filePath();
}
else if (role == Qt::DecorationRole && index.column() == 0) {
return m_iconProvider.icon(info);
}
else if (role == Qt::ToolTipRole) {
return QString("%1\n大小: %2 bytes\n修改时间: %3")
.arg(item->filePath())
.arg(info.size())
.arg(info.lastModified().toString());
}
return QVariant();
}
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override {
if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
if (section == 0)
return "名称";
else if (section == 1)
return "路径";
}
return QVariant();
}
};
使用示例:
cpp
#include <QApplication>
#include <QTreeView>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
FileSystemTreeModel *model = new FileSystemTreeModel("C:/");
QTreeView *view = new QTreeView;
view->setModel(model);
view->setColumnWidth(0, 250);
view->header()->setStretchLastSection(true);
view->resize(600, 400);
view->show();
return app.exec();
}
3.5.5 实战:组织结构树形模型
这个示例实现一个公司组织架构管理系统。
cpp
#include <QAbstractItemModel>
#include <QString>
#include <QVector>
// 员工/部门节点
class OrgItem {
public:
enum Type { Department, Employee };
private:
Type m_type;
QString m_name;
QString m_position; // 职位(仅员工)
QString m_email; // 邮箱(仅员工)
OrgItem *m_parent;
QVector<OrgItem*> m_children;
public:
OrgItem(Type type, const QString &name, OrgItem *parent = nullptr)
: m_type(type), m_name(name), m_parent(parent) {}
~OrgItem() {
qDeleteAll(m_children);
}
// Getters
Type type() const { return m_type; }
QString name() const { return m_name; }
QString position() const { return m_position; }
QString email() const { return m_email; }
OrgItem *parentItem() { return m_parent; }
// Setters
void setName(const QString &name) { m_name = name; }
void setPosition(const QString &pos) { m_position = pos; }
void setEmail(const QString &email) { m_email = email; }
// 子项管理
void appendChild(OrgItem *child) {
m_children.append(child);
}
OrgItem *child(int row) {
if (row < 0 || row >= m_children.size())
return nullptr;
return m_children.at(row);
}
int childCount() const {
return m_children.size();
}
int row() const {
if (m_parent)
return m_parent->m_children.indexOf(const_cast<OrgItem*>(this));
return 0;
}
int columnCount() const {
return 3; // 名称、职位、邮箱
}
QVariant data(int column) const {
switch (column) {
case 0: return m_name;
case 1: return m_type == Employee ? m_position : QString("部门");
case 2: return m_email;
}
return QVariant();
}
bool setData(int column, const QVariant &value) {
switch (column) {
case 0: m_name = value.toString(); return true;
case 1: if (m_type == Employee) { m_position = value.toString(); return true; } break;
case 2: if (m_type == Employee) { m_email = value.toString(); return true; } break;
}
return false;
}
};
// 组织架构模型
class OrganizationModel : public QAbstractItemModel {
Q_OBJECT
private:
OrgItem *m_rootItem;
public:
explicit OrganizationModel(QObject *parent = nullptr)
: QAbstractItemModel(parent) {
// 创建示例组织结构
m_rootItem = new OrgItem(OrgItem::Department, "公司");
// 技术部
OrgItem *techDept = new OrgItem(OrgItem::Department, "技术部", m_rootItem);
m_rootItem->appendChild(techDept);
OrgItem *emp1 = new OrgItem(OrgItem::Employee, "张三", techDept);
emp1->setPosition("技术总监");
emp1->setEmail("zhangsan@company.com");
techDept->appendChild(emp1);
OrgItem *emp2 = new OrgItem(OrgItem::Employee, "李四", techDept);
emp2->setPosition("高级工程师");
emp2->setEmail("lisi@company.com");
techDept->appendChild(emp2);
// 销售部
OrgItem *salesDept = new OrgItem(OrgItem::Department, "销售部", m_rootItem);
m_rootItem->appendChild(salesDept);
OrgItem *emp3 = new OrgItem(OrgItem::Employee, "王五", salesDept);
emp3->setPosition("销售经理");
emp3->setEmail("wangwu@company.com");
salesDept->appendChild(emp3);
}
~OrganizationModel() {
delete m_rootItem;
}
// ===== 基本接口 =====
QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override {
if (!hasIndex(row, column, parent))
return QModelIndex();
OrgItem *parentItem;
if (!parent.isValid())
parentItem = m_rootItem;
else
parentItem = static_cast<OrgItem*>(parent.internalPointer());
OrgItem *childItem = parentItem->child(row);
if (childItem)
return createIndex(row, column, childItem);
else
return QModelIndex();
}
QModelIndex parent(const QModelIndex &child) const override {
if (!child.isValid())
return QModelIndex();
OrgItem *childItem = static_cast<OrgItem*>(child.internalPointer());
OrgItem *parentItem = childItem->parentItem();
if (parentItem == m_rootItem)
return QModelIndex();
return createIndex(parentItem->row(), 0, parentItem);
}
int rowCount(const QModelIndex &parent = QModelIndex()) const override {
OrgItem *parentItem;
if (parent.column() > 0)
return 0;
if (!parent.isValid())
parentItem = m_rootItem;
else
parentItem = static_cast<OrgItem*>(parent.internalPointer());
return parentItem->childCount();
}
int columnCount(const QModelIndex &parent = QModelIndex()) const override {
Q_UNUSED(parent);
return 3;
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
if (!index.isValid())
return QVariant();
OrgItem *item = static_cast<OrgItem*>(index.internalPointer());
if (role == Qt::DisplayRole || role == Qt::EditRole) {
return item->data(index.column());
}
else if (role == Qt::DecorationRole && index.column() == 0) {
if (item->type() == OrgItem::Department)
return QIcon(":/icons/department.png");
else
return QIcon(":/icons/person.png");
}
else if (role == Qt::FontRole && index.column() == 0) {
if (item->type() == OrgItem::Department) {
QFont font;
font.setBold(true);
return font;
}
}
else if (role == Qt::BackgroundRole) {
if (item->type() == OrgItem::Department)
return QColor(230, 240, 255);
}
return QVariant();
}
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override {
if (!index.isValid() || role != Qt::EditRole)
return false;
OrgItem *item = static_cast<OrgItem*>(index.internalPointer());
bool result = item->setData(index.column(), value);
if (result)
emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
return result;
}
Qt::ItemFlags flags(const QModelIndex &index) const override {
if (!index.isValid())
return Qt::NoItemFlags;
Qt::ItemFlags flags = QAbstractItemModel::flags(index);
OrgItem *item = static_cast<OrgItem*>(index.internalPointer());
// 部门名称可编辑,员工所有信息可编辑
if (index.column() == 0 || item->type() == OrgItem::Employee)
flags |= Qt::ItemIsEditable;
return flags;
}
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override {
if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
switch (section) {
case 0: return "姓名/部门";
case 1: return "职位";
case 2: return "邮箱";
}
}
return QVariant();
}
// ===== 添加/删除节点 =====
bool addDepartment(const QString &name, const QModelIndex &parent) {
OrgItem *parentItem;
if (!parent.isValid())
parentItem = m_rootItem;
else
parentItem = static_cast<OrgItem*>(parent.internalPointer());
int row = parentItem->childCount();
beginInsertRows(parent, row, row);
OrgItem *newDept = new OrgItem(OrgItem::Department, name, parentItem);
parentItem->appendChild(newDept);
endInsertRows();
return true;
}
bool addEmployee(const QString &name, const QString &position,
const QString &email, const QModelIndex &parent) {
OrgItem *parentItem;
if (!parent.isValid())
parentItem = m_rootItem;
else
parentItem = static_cast<OrgItem*>(parent.internalPointer());
// 只能在部门下添加员工
if (parentItem->type() != OrgItem::Department)
return false;
int row = parentItem->childCount();
beginInsertRows(parent, row, row);
OrgItem *newEmp = new OrgItem(OrgItem::Employee, name, parentItem);
newEmp->setPosition(position);
newEmp->setEmail(email);
parentItem->appendChild(newEmp);
endInsertRows();
return true;
}
};
使用示例:
cpp
#include <QApplication>
#include <QTreeView>
#include <QPushButton>
#include <QVBoxLayout>
#include <QInputDialog>
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
OrganizationModel *model = new OrganizationModel;
QTreeView *view = new QTreeView;
view->setModel(model);
view->setEditTriggers(QAbstractItemView::DoubleClicked);
view->expandAll();
// 添加部门按钮
QPushButton *addDeptBtn = new QPushButton("添加部门");
QObject::connect(addDeptBtn, &QPushButton::clicked, [=]() {
bool ok;
QString name = QInputDialog::getText(nullptr, "添加部门", "部门名称:",
QLineEdit::Normal, "", &ok);
if (ok && !name.isEmpty()) {
QModelIndex parent = view->currentIndex();
model->addDepartment(name, parent);
view->expand(parent);
}
});
// 添加员工按钮
QPushButton *addEmpBtn = new QPushButton("添加员工");
QObject::connect(addEmpBtn, &QPushButton::clicked, [=]() {
QModelIndex parent = view->currentIndex();
// 简化版,实际应该用对话框输入完整信息
model->addEmployee("新员工", "职位", "email@company.com", parent);
view->expand(parent);
});
QWidget window;
QVBoxLayout *layout = new QVBoxLayout;
layout->addWidget(view);
layout->addWidget(addDeptBtn);
layout->addWidget(addEmpBtn);
window.setLayout(layout);
window.resize(600, 400);
window.show();
return app.exec();
}
本节小结:
✅ 树形模型 是最复杂但最强大的模型类型
✅ 核心机制 :使用 internalPointer 存储节点指针
✅ 关键方法 :正确实现 index() 和 parent() 是成功的关键
✅ 节点类设计 :需要维护父子关系和提供快速访问接口
✅ 适用场景:文件系统、组织架构、分类目录等层级数据
- 树形结构的表示方法
- parent()和index()的实现要点
- internalPointer的使用
- 实战:文件系统树形模型
- 实战:组织结构树形模型
第3章总结:
🎉 第3章 QAbstractItemModel深度解析 已全部完成!
本章涵盖了:
- ✅ 核心接口的详细讲解(必须实现和可选实现)
- ✅ 所有重要信号的使用时机和最佳实践
- ✅ 列表模型(QAbstractListModel)的完整实现
- ✅ 表格模型(QAbstractTableModel)的完整实现
- ✅ 树形模型(QAbstractItemModel)的高级技巧
通过本章学习,你已经掌握了:
- 如何选择合适的模型基类
- 如何正确实现各种虚函数
- 如何正确使用信号通知View更新
- 如何实现增删改查等完整功能
- 如何处理复杂的树形数据结构
接下来可以继续学习第4章"标准视图类的使用"!