Qt Model/View架构详解(一):基础理论

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 │
│ 表格   │           │ 列表   │
└────────┘           └────────┘

关键特性

  1. 单一数据源:数据只存储在Model中
  2. 多视图同步:多个View可以显示同一个Model的数据
  3. 自动更新:Model数据变化时,所有View自动刷新
  4. 职责清晰: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架构的必要性

  1. 解耦数据与界面:修改数据逻辑不影响界面,修改界面不影响数据
  2. 提高复用性:同一Model可以用于不同的View
  3. 便于测试:可以单独测试Model的数据逻辑
  4. 更好的性能:懒加载、按需渲染
  5. 灵活的定制:通过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架构

实战建议

  1. 学习路径

    • 先熟悉QStandardItemModel(最简单的现成Model)
    • 再学习自定义QAbstractTableModel
    • 最后掌握复杂的QAbstractItemModel(树形结构)
  2. 常见组合

    • 简单列表:QStringListModel + QListView
    • 通用表格:QStandardItemModel + QTableView
    • 文件浏览:QFileSystemModel + QTreeView
    • 数据库:QSqlTableModel + QTableView
  3. 性能优化

    • 大数据量使用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的核心特性

  1. 轻量级:QModelIndex 只是一个引用,不存储实际数据
  2. 临时性:索引可能随模型变化而失效
  3. 唯一性:每个数据项都有唯一的索引表示

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()

  1. 确保索引与模型正确关联
  2. 自动设置 model() 指针
  3. 支持内部指针(树形结构)

列表/表格模型中的用法

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();
}

示例要点

  1. 索引的创建和使用

    • index(row, column) 创建索引
    • data(index, role) 通过索引获取数据
  2. 索引的有效性检查

    • 使用 isValid() 确保索引有效
  3. 索引的遍历

    • 通过循环创建不同的索引来遍历数据
  4. 索引的导航

    • 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 - 背景色

作用:设置单元格的背景颜色

返回类型QBrushQColor

示例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 - 前景色(文字颜色)

作用:设置文本颜色

返回类型QBrushQColor

示例

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::HorizontalQt::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();
}

重要规则

  1. 一致性:相同的 (row, column, parent) 必须返回相同的索引
  2. 有效性:返回的索引必须有效或为 QModelIndex()
  3. 内部指针:树形模型必须正确设置 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;
}

关键规则

  1. 必须调用 begin/end 方法:否则 View 不会更新
  2. 调用顺序:beginXxx → 修改数据 → endXxx
  3. 参数一致: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);

关键点

  1. 不要手动发送这些信号
  2. 使用 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特点

核心特点

  1. 一维结构:只有行,没有层级关系
  2. 简化接口 :不需要实现 parent() 和复杂的 index()
  3. 单列默认:通常只有一列(columnCount 默认返回 1)
  4. 自动实现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特点

核心特点

  1. 二维结构:有行和列,无层级关系
  2. 简化接口parent() 已实现,index() 已简化
  3. 表格专用 :专为 QTableView 优化
  4. 无子项:所有项都是平级的

与其他模型的对比

特性 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);
}

实现要点

  1. internalPointer 存储节点指针:这是关键!
  2. 根节点特殊处理:根节点通常不可见,其子项的 parent 返回无效索引
  3. row() 方法:节点需要知道自己在父节点中的位置
  4. 性能考虑: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 的注意事项

  1. 类型转换:必须转换为正确的指针类型
  2. 内存管理:Model 负责节点的内存管理,不是 internalPointer
  3. 持久性:普通索引失效后,internalPointer 可能指向已释放的内存
  4. 替代方案 :可以使用 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)的高级技巧

通过本章学习,你已经掌握了:

  1. 如何选择合适的模型基类
  2. 如何正确实现各种虚函数
  3. 如何正确使用信号通知View更新
  4. 如何实现增删改查等完整功能
  5. 如何处理复杂的树形数据结构

接下来可以继续学习第4章"标准视图类的使用"!


相关推荐
Swift社区2 小时前
Java 实战 - 字符编码问题解决方案
java·开发语言
灰灰勇闯IT2 小时前
【Flutter for OpenHarmony--Dart 入门日记】第3篇:基础数据类型全解析——String、数字与布尔值
android·java·开发语言
天天睡大觉2 小时前
python命名规则(PEP8编码规则)
开发语言·前端·python
青火coding2 小时前
ai时代下的RPC传输——StreamObserver
qt·网络协议·microsoft·rpc
重生之我是Java开发战士2 小时前
【Python】基础语法入门:变量,数据类型,运算符
开发语言·python
proware2 小时前
qt与egl的那些事儿
qt·rockchip·3588·egl
csbysj20202 小时前
PHP 数组排序
开发语言
2501_944521592 小时前
Flutter for OpenHarmony 微动漫App实战:底部导航实现
android·开发语言·前端·javascript·redis·flutter·ecmascript
Java程序员威哥2 小时前
使用Java自动加载OpenCV来调用YOLO模型检测
java·开发语言·人工智能·python·opencv·yolo·c#