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章"标准视图类的使用"!


相关推荐
Quz14 小时前
QML Hello World 入门示例
qt
xcyxiner4 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner4 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner5 天前
DicomViewer (添加模型类)3
qt
xcyxiner5 天前
DicomViewer (目录调整) 2
qt
xcyxiner5 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
LDR0067 天前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言
雪碧聊技术7 天前
Tree.js是什么?一文讲透
开发语言·javascript·ecmascript
码云数智-园园7 天前
C++20 Modules 模块详解
java·开发语言·spring
swordbob7 天前
NIO的channel中什么是 fd(File Descriptor,文件描述符)
java·开发语言·nio