Qt Model/View架构详解(三):自定义模型

Qt Model/View架构详解

重要程度 : ⭐⭐⭐⭐⭐
实战价值 : 处理复杂数据展示(表格、树形结构、列表)
学习目标 : 掌握Qt的Model/View设计模式,能够自定义Model和Delegate处理复杂数据展示需求
本篇要点: 学习如何在Qt Model/View中自定义模型、选择模型、自定义Delegate(委托)。


📚 目录

第三部分:自定义开发 (第6-8章)

第6章 自定义Model实战

  • 6.1 设计自定义Model的步骤
  • 6.2 简单列表模型自定义
  • 6.3 表格模型自定义
  • 6.4 树形模型自定义
  • 6.5 可编辑模型
  • 6.6 可排序模型
  • 6.7 动态数据模型

第7章 选择模型(QItemSelectionModel)

  • 7.1 选择模型基础
  • 7.2 选择操作
  • 7.3 选择相关信号
  • 7.4 QItemSelection详解

第8章 Delegate(委托)详解

  • 8.1 委托的作用
  • 8.2 委托的核心方法
  • 8.3 自定义渲染委托
  • 8.4 自定义编辑委托
  • 8.5 复杂委托实战

第6章 自定义Model实战

6.1 设计自定义Model的步骤

设计一个自定义Model需要遵循一定的步骤和最佳实践,以确保模型的正确性和可维护性。

6.1.1 确定数据结构

第一步:明确需求

在设计自定义Model之前,首先要明确:

  • 需要存储什么数据?
  • 数据的组织方式是什么?(列表、表格、树形)
  • 数据是静态的还是动态的?
  • 是否需要编辑功能?
  • 是否需要排序、过滤等功能?

数据结构选择

cpp 复制代码
// 列表数据 - 使用QVector或QList
class SimpleListModel {
private:
    QVector<QString> m_data;
    // 或
    QList<MyDataType> m_items;
};

// 表格数据 - 使用二维数组或结构体列表
class TableModel {
private:
    QVector<QVector<QVariant>> m_tableData;
    // 或
    struct Student {
        QString name;
        int id;
        double score;
    };
    QVector<Student> m_students;
};

// 树形数据 - 使用自定义节点类
class TreeModel {
private:
    struct TreeNode {
        QString data;
        TreeNode* parent;
        QVector<TreeNode*> children;
    };
    TreeNode* m_root;
};

6.1.2 选择合适的基类

基类选择指南

数据类型 推荐基类 原因
简单字符串列表 QStringListModel 已实现,直接使用
一维列表 QAbstractListModel 简化了index()parent()
二维表格 QAbstractTableModel 简化了树形相关方法
树形结构 QAbstractItemModel 完全控制,需实现所有方法
通用场景 QStandardItemModel 功能完整,直接使用

基类对比

cpp 复制代码
// QAbstractListModel - 列表模型基类
class MyListModel : public QAbstractListModel {
    // 必须实现:
    // - rowCount()
    // - data()
    // 已实现:
    // - index() - 返回单列索引
    // - parent() - 始终返回无效索引
};

// QAbstractTableModel - 表格模型基类
class MyTableModel : public QAbstractTableModel {
    // 必须实现:
    // - rowCount()
    // - columnCount()
    // - data()
    // 已实现:
    // - index() - 返回表格索引
    // - parent() - 始终返回无效索引
};

// QAbstractItemModel - 完整模型基类
class MyTreeModel : public QAbstractItemModel {
    // 必须实现:
    // - rowCount()
    // - columnCount()
    // - data()
    // - index() - 需要自己实现
    // - parent() - 需要自己实现
};

6.1.3 实现必要的接口

只读模型的最小接口

cpp 复制代码
class ReadOnlyModel : public QAbstractTableModel {
public:
    // 必须实现的方法
    int rowCount(const QModelIndex &parent = QModelIndex()) const override {
        if (parent.isValid())
            return 0;  // 表格模型的父索引始终返回0
        return m_data.size();
    }
    
    int columnCount(const QModelIndex &parent = QModelIndex()) const override {
        if (parent.isValid())
            return 0;
        return 3;  // 3列
    }
    
    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();
    }
    
private:
    QVector<QVector<QString>> m_data;
};

可编辑模型的接口

cpp 复制代码
class EditableModel : public QAbstractTableModel {
public:
    // 在只读接口基础上添加:
    
    Qt::ItemFlags flags(const QModelIndex &index) const override {
        if (!index.isValid())
            return Qt::NoItemFlags;
        
        return Qt::ItemIsEditable | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
    }
    
    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()][index.column()] = value.toString();
        
        // 重要:发送数据变化信号
        emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
        
        return true;
    }
    
private:
    QVector<QVector<QString>> m_data;
};

支持动态增删的接口

cpp 复制代码
class DynamicModel : public QAbstractTableModel {
public:
    bool insertRows(int row, int count, 
                    const QModelIndex &parent = QModelIndex()) override {
        if (parent.isValid())
            return false;
        
        // 重要:调用begin/end方法
        beginInsertRows(parent, row, row + count - 1);
        
        for (int i = 0; i < count; ++i) {
            m_data.insert(row, QVector<QString>(columnCount()));
        }
        
        endInsertRows();
        return true;
    }
    
    bool removeRows(int row, int count,
                    const QModelIndex &parent = QModelIndex()) override {
        if (parent.isValid())
            return false;
        
        beginRemoveRows(parent, row, row + count - 1);
        
        for (int i = 0; i < count; ++i) {
            m_data.removeAt(row);
        }
        
        endRemoveRows();
        return true;
    }
};

6.1.4 发送正确的信号

关键信号和使用场景

cpp 复制代码
class SignalAwareModel : public QAbstractTableModel {
    // 1. 数据内容变化
    void updateData(int row, int col, const QString &value) {
        m_data[row][col] = value;
        
        QModelIndex index = this->index(row, col);
        emit dataChanged(index, index, {Qt::DisplayRole});
    }
    
    // 2. 批量数据变化
    void updateRange(int startRow, int endRow, int col) {
        QModelIndex topLeft = index(startRow, col);
        QModelIndex bottomRight = index(endRow, col);
        emit dataChanged(topLeft, bottomRight, {Qt::DisplayRole});
    }
    
    // 3. 插入行
    void addRow(const QVector<QString> &rowData) {
        int row = m_data.size();
        
        beginInsertRows(QModelIndex(), row, row);  // 必须调用
        m_data.append(rowData);
        endInsertRows();  // 必须调用
    }
    
    // 4. 删除行
    void deleteRow(int row) {
        beginRemoveRows(QModelIndex(), row, row);
        m_data.removeAt(row);
        endRemoveRows();
    }
    
    // 5. 布局改变(如排序)
    void sortData(int column) {
        emit layoutAboutToBeChanged();
        
        // 执行排序
        std::sort(m_data.begin(), m_data.end(), 
                 [column](const QVector<QString> &a, const QVector<QString> &b) {
            return a[column] < b[column];
        });
        
        emit layoutChanged();
    }
    
    // 6. 完全重置
    void resetData(const QVector<QVector<QString>> &newData) {
        beginResetModel();
        m_data = newData;
        endResetModel();
    }
    
private:
    QVector<QVector<QString>> m_data;
};

信号使用决策树

复制代码
数据变化类型?
├── 单个或少量单元格内容变化
│   └── emit dataChanged(topLeft, bottomRight, roles)
│
├── 插入新行/列
│   ├── beginInsertRows()/beginInsertColumns()
│   ├── 实际插入操作
│   └── endInsertRows()/endInsertColumns()
│
├── 删除行/列
│   ├── beginRemoveRows()/beginRemoveColumns()
│   ├── 实际删除操作
│   └── endRemoveRows()/endRemoveColumns()
│
├── 数据重新排序(不改变内容)
│   ├── emit layoutAboutToBeChanged()
│   ├── 执行排序
│   └── emit layoutChanged()
│
└── 完全重新加载数据
    ├── beginResetModel()
    ├── 重新加载
    └── endResetModel()

6.1.5 测试与调试

测试清单

cpp 复制代码
// 1. 基本功能测试
void testBasicFunctionality() {
    MyModel model;
    
    // 测试rowCount
    assert(model.rowCount() >= 0);
    
    // 测试columnCount
    assert(model.columnCount() >= 0);
    
    // 测试data
    QModelIndex index = model.index(0, 0);
    QVariant value = model.data(index);
    assert(value.isValid());
}

// 2. 边界条件测试
void testBoundaryConditions() {
    MyModel model;
    
    // 测试无效索引
    QModelIndex invalid;
    assert(!model.data(invalid).isValid());
    
    // 测试超出范围
    QModelIndex outOfRange = model.index(9999, 9999);
    assert(!outOfRange.isValid());
}

// 3. 信号测试
void testSignals() {
    MyModel model;
    QSignalSpy spy(&model, &MyModel::dataChanged);
    
    // 修改数据
    model.setData(model.index(0, 0), "New Value");
    
    // 验证信号发送
    assert(spy.count() == 1);
}

// 4. 性能测试
void testPerformance() {
    MyModel model;
    int rows = 10000;
    
    QElapsedTimer timer;
    timer.start();
    
    for (int i = 0; i < rows; ++i) {
        model.data(model.index(i, 0));
    }
    
    qint64 elapsed = timer.elapsed();
    qDebug() << "10000 data() calls took" << elapsed << "ms";
}

调试技巧

cpp 复制代码
class DebugModel : public QAbstractTableModel {
    QVariant data(const QModelIndex &index, int role) const override {
        // 添加调试输出
        qDebug() << "data() called: row" << index.row() 
                 << "col" << index.column() 
                 << "role" << role;
        
        // 检查索引有效性
        if (!index.isValid()) {
            qWarning() << "Invalid index requested!";
            return QVariant();
        }
        
        // 检查数据范围
        if (index.row() >= m_data.size()) {
            qWarning() << "Row out of range:" << index.row();
            return QVariant();
        }
        
        return m_data[index.row()][index.column()];
    }
    
    bool setData(const QModelIndex &index, const QVariant &value, 
                 int role) override {
        qDebug() << "setData() called:" << index << value << role;
        
        bool success = /* 实际实现 */;
        
        if (!success) {
            qWarning() << "setData() failed!";
        }
        
        return success;
    }
};

常见错误和解决方案

错误现象 可能原因 解决方案
视图不显示数据 rowCount()返回0 检查rowCount()实现
编辑不生效 未发送dataChanged信号 在setData()中emit dataChanged()
崩溃 index()返回无效父指针 检查internalPointer赋值
视图不更新 忘记调用begin/end方法 添加beginInsertRows()等
数据错位 layoutChanged未发送 排序后emit layoutChanged()

本节小结

确定数据结构 - 明确需求,选择合适的数据容器

选择合适基类 - 根据数据类型选择QAbstractListModel/TableModel/ItemModel

实现必要接口 - 只读模型最少3个方法,可编辑需要flags()和setData()

发送正确信号 - dataChanged、begin/end系列、layoutChanged等

测试与调试 - 基本功能、边界条件、信号、性能测试

设计模型的黄金法则

  1. 先设计后实现 - 明确数据结构和需求
  2. 最小化实现 - 只实现需要的功能
  3. 信号必须准确 - 每次数据变化都要发送正确的信号
  4. 充分测试 - 测试所有边界条件和异常情况
  • 确定数据结构
  • 选择合适的基类
  • 实现必要的接口
  • 发送正确的信号
  • 测试与调试

6.2 简单列表模型自定义

实战项目:任务列表模型

这个项目将实现一个功能完整的任务列表模型,支持优先级、完成状态等功能。

6.2.1 需求分析

功能需求

  • 存储任务列表(任务名称、优先级、完成状态、截止日期)
  • 支持添加、删除、编辑任务
  • 支持标记任务为已完成/未完成
  • 按优先级显示不同颜色
  • 显示任务图标(已完成/未完成)

数据需求

  • 任务标题(QString)
  • 优先级(枚举:低、中、高)
  • 完成状态(bool)
  • 截止日期(QDate)
  • 备注(QString)

6.2.2 数据结构设计
cpp 复制代码
// 任务数据结构
struct Task {
    QString title;
    int priority;  // 0=低, 1=中, 2=高
    bool completed;
    QDate dueDate;
    QString note;
    
    // 便利构造函数
    Task(const QString &t = "", int p = 1, bool c = false, 
         const QDate &d = QDate(), const QString &n = "")
        : title(t), priority(p), completed(c), dueDate(d), note(n) {}
};

// 声明元类型以便在QVariant中使用
Q_DECLARE_METATYPE(Task)

6.2.3 接口实现
cpp 复制代码
class TaskListModel : public QAbstractListModel {
    Q_OBJECT
    
public:
    // 自定义角色
    enum TaskRoles {
        TitleRole = Qt::UserRole + 1,
        PriorityRole,
        CompletedRole,
        DueDateRole,
        NoteRole
    };
    
    explicit TaskListModel(QObject *parent = nullptr)
        : QAbstractListModel(parent) {
        // 添加一些示例数据
        addTask(Task("完成项目文档", 2, false, 
                    QDate::currentDate().addDays(3), "重要"));
        addTask(Task("代码审查", 1, false, 
                    QDate::currentDate().addDays(7), ""));
        addTask(Task("修复bug #123", 0, true, 
                    QDate::currentDate(), "已完成"));
    }
    
    // 必须实现的方法
    int rowCount(const QModelIndex &parent = QModelIndex()) const override {
        if (parent.isValid())
            return 0;
        return m_tasks.size();
    }
    
    QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override {
        if (!index.isValid() || index.row() >= m_tasks.size())
            return QVariant();
        
        const Task &task = m_tasks[index.row()];
        
        switch (role) {
        case Qt::DisplayRole:
        case TitleRole:
            return task.title;
            
        case Qt::DecorationRole:
            // 根据完成状态返回不同图标
            return task.completed ? 
                QIcon(":/icons/checked.png") : 
                QIcon(":/icons/unchecked.png");
            
        case Qt::ForegroundRole:
            // 已完成的任务显示为灰色
            if (task.completed)
                return QColor(Qt::gray);
            // 根据优先级显示不同颜色
            switch (task.priority) {
            case 2: return QColor(Qt::red);      // 高优先级
            case 1: return QColor(Qt::black);    // 中优先级
            case 0: return QColor(Qt::darkGray); // 低优先级
            }
            break;
            
        case Qt::BackgroundRole:
            // 过期任务显示红色背景
            if (!task.completed && task.dueDate.isValid() && 
                task.dueDate < QDate::currentDate()) {
                return QColor(255, 200, 200);
            }
            break;
            
        case Qt::FontRole: {
            QFont font;
            if (task.completed)
                font.setStrikeOut(true);  // 已完成:删除线
            if (task.priority == 2)
                font.setBold(true);        // 高优先级:粗体
            return font;
        }
        
        case Qt::ToolTipRole: {
            QString tooltip = QString("<b>%1</b><br>").arg(task.title);
            tooltip += QString("优先级: %1<br>")
                .arg(task.priority == 2 ? "高" : task.priority == 1 ? "中" : "低");
            tooltip += QString("状态: %1<br>")
                .arg(task.completed ? "已完成" : "未完成");
            if (task.dueDate.isValid())
                tooltip += QString("截止日期: %1<br>")
                    .arg(task.dueDate.toString("yyyy-MM-dd"));
            if (!task.note.isEmpty())
                tooltip += QString("备注: %1").arg(task.note);
            return tooltip;
        }
        
        case PriorityRole:
            return task.priority;
            
        case CompletedRole:
            return task.completed;
            
        case DueDateRole:
            return task.dueDate;
            
        case NoteRole:
            return task.note;
        }
        
        return QVariant();
    }
    
    // 可编辑支持
    Qt::ItemFlags flags(const QModelIndex &index) const override {
        if (!index.isValid())
            return Qt::NoItemFlags;
        
        return Qt::ItemIsEditable | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
    }
    
    bool setData(const QModelIndex &index, const QVariant &value, 
                 int role = Qt::EditRole) override {
        if (!index.isValid() || index.row() >= m_tasks.size())
            return false;
        
        Task &task = m_tasks[index.row()];
        
        switch (role) {
        case Qt::EditRole:
        case TitleRole:
            task.title = value.toString();
            break;
        case PriorityRole:
            task.priority = value.toInt();
            break;
        case CompletedRole:
            task.completed = value.toBool();
            break;
        case DueDateRole:
            task.dueDate = value.toDate();
            break;
        case NoteRole:
            task.note = value.toString();
            break;
        default:
            return false;
        }
        
        emit dataChanged(index, index);
        return true;
    }
    
    // 添加任务
    void addTask(const Task &task) {
        int row = m_tasks.size();
        beginInsertRows(QModelIndex(), row, row);
        m_tasks.append(task);
        endInsertRows();
    }
    
    // 删除任务
    bool removeTask(int row) {
        if (row < 0 || row >= m_tasks.size())
            return false;
        
        beginRemoveRows(QModelIndex(), row, row);
        m_tasks.removeAt(row);
        endRemoveRows();
        return true;
    }
    
    // 切换完成状态
    void toggleCompleted(int row) {
        if (row < 0 || row >= m_tasks.size())
            return;
        
        m_tasks[row].completed = !m_tasks[row].completed;
        QModelIndex idx = index(row);
        emit dataChanged(idx, idx);
    }
    
    // 获取任务
    Task getTask(int row) const {
        if (row >= 0 && row < m_tasks.size())
            return m_tasks[row];
        return Task();
    }
    
private:
    QVector<Task> m_tasks;
};

6.2.4 完整代码示例
cpp 复制代码
#include <QApplication>
#include <QWidget>
#include <QListView>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QDate>

// Task结构体和TaskListModel类(如上所示)

// 使用示例
class TaskManagerWidget : public QWidget {
    Q_OBJECT
    
private:
    TaskListModel *m_model;
    QListView *m_view;
    
public:
    TaskManagerWidget(QWidget *parent = nullptr) : QWidget(parent) {
        setupUI();
    }
    
private:
    void setupUI() {
        // 创建模型和视图
        m_model = new TaskListModel(this);
        
        m_view = new QListView;
        m_view->setModel(m_model);
        m_view->setEditTriggers(QAbstractItemView::DoubleClicked);
        m_view->setAlternatingRowColors(true);
        
        // 按钮
        QPushButton *addBtn = new QPushButton("添加任务");
        QPushButton *deleteBtn = new QPushButton("删除任务");
        QPushButton *toggleBtn = new QPushButton("切换完成状态");
        QPushButton *setPriorityBtn = new QPushButton("设置优先级");
        
        connect(addBtn, &QPushButton::clicked, this, &TaskManagerWidget::addTask);
        connect(deleteBtn, &QPushButton::clicked, this, &TaskManagerWidget::deleteTask);
        connect(toggleBtn, &QPushButton::clicked, this, &TaskManagerWidget::toggleCompleted);
        connect(setPriorityBtn, &QPushButton::clicked, this, &TaskManagerWidget::setPriority);
        
        // 布局
        QVBoxLayout *mainLayout = new QVBoxLayout;
        mainLayout->addWidget(m_view);
        
        QHBoxLayout *btnLayout = new QHBoxLayout;
        btnLayout->addWidget(addBtn);
        btnLayout->addWidget(deleteBtn);
        btnLayout->addWidget(toggleBtn);
        btnLayout->addWidget(setPriorityBtn);
        
        mainLayout->addLayout(btnLayout);
        setLayout(mainLayout);
        
        resize(500, 400);
    }
    
private slots:
    void addTask() {
        bool ok;
        QString title = QInputDialog::getText(this, "添加任务", "任务标题:",
                                              QLineEdit::Normal, "", &ok);
        if (ok && !title.isEmpty()) {
            Task newTask(title, 1, false, QDate::currentDate().addDays(7));
            m_model->addTask(newTask);
        }
    }
    
    void deleteTask() {
        QModelIndex current = m_view->currentIndex();
        if (current.isValid()) {
            m_model->removeTask(current.row());
        }
    }
    
    void toggleCompleted() {
        QModelIndex current = m_view->currentIndex();
        if (current.isValid()) {
            m_model->toggleCompleted(current.row());
        }
    }
    
    void setPriority() {
        QModelIndex current = m_view->currentIndex();
        if (!current.isValid())
            return;
        
        QStringList items = {"低", "中", "高"};
        bool ok;
        QString item = QInputDialog::getItem(this, "设置优先级", "选择优先级:",
                                             items, 1, false, &ok);
        if (ok) {
            int priority = items.indexOf(item);
            m_model->setData(current, priority, TaskListModel::PriorityRole);
        }
    }
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    TaskManagerWidget widget;
    widget.setWindowTitle("任务管理器");
    widget.show();
    
    return app.exec();
}

#include "main.moc"

6.2.5 与QListView集成

集成要点

cpp 复制代码
// 1. 创建和设置模型
TaskListModel *model = new TaskListModel;
QListView *view = new QListView;
view->setModel(model);

// 2. 配置视图属性
view->setEditTriggers(QAbstractItemView::DoubleClicked);
view->setAlternatingRowColors(true);
view->setSpacing(2);

// 3. 监听选择变化
connect(view->selectionModel(), &QItemSelectionModel::currentChanged,
        [=](const QModelIndex &current, const QModelIndex &previous) {
    if (current.isValid()) {
        Task task = model->getTask(current.row());
        qDebug() << "Selected:" << task.title;
    }
});

// 4. 双击编辑
connect(view, &QListView::doubleClicked,
        [=](const QModelIndex &index) {
    // 可以打开自定义编辑对话框
    // 或者让视图进入编辑模式
    view->edit(index);
});

本节小结

需求明确 - 任务管理的完整功能需求

数据结构 - Task结构体,包含所有必要字段

接口实现 - 完整的QAbstractListModel实现

多角色支持 - DisplayRole、DecorationRole、ForegroundRole等

编辑功能 - flags()和setData()的完整实现

动态操作 - addTask()、removeTask()、toggleCompleted()

视图集成 - 与QListView的完美配合

关键要点

  1. 使用自定义角色存储额外数据
  2. 通过不同的Qt::ItemDataRole实现丰富的视觉效果
  3. 正确使用begin/endInsertRows和dataChanged信号
  4. 提供便利的操作方法(addTask、toggleCompleted等)
  • 需求分析
  • 数据结构设计
  • 接口实现
  • 完整代码示例
  • 与QListView集成

6.3 表格模型自定义

实战项目:学生信息管理系统

本项目将实现一个完整的学生信息管理系统,展示表格模型的所有核心功能。

6.3.1 需求分析

数据字段

  • 姓名(QString)
  • 学号(QString)
  • 成绩(double)
  • 年级(int)
  • 专业(QString)
  • 备注(QString)

功能需求

  • 显示学生列表(表格形式)
  • 添加/删除学生
  • 编辑学生信息
  • 根据成绩显示不同颜色
  • 计算平均分、最高分、最低分

6.3.2 数据结构:使用QVector
cpp 复制代码
// 学生数据结构
struct Student {
    QString name;
    QString studentId;
    double score;
    int grade;
    QString major;
    QString note;
    
    Student(const QString &n = "", const QString &id = "", 
            double s = 0.0, int g =  1, const QString &m = "",
            const QString &note = "")
        : name(n), studentId(id), score(s), grade(g), major(m), note(note) {}
};

6.3.3 rowCount和columnCount实现
cpp 复制代码
class StudentTableModel : public QAbstractTableModel {
    Q_OBJECT
    
public:
    explicit StudentTableModel(QObject *parent = nullptr)
        : QAbstractTableModel(parent) {
        // 添加示例数据
        m_students.append(Student("张三", "2021001", 85.5, 3, "计算机科学"));
        m_students.append(Student("李四", "2021002", 92.0, 3, "计算机科学"));
        m_students.append(Student("王五", "2021003", 78.5, 3, "软件工程"));
        m_students.append(Student("赵六", "2022001", 88.0, 2, "计算机科学"));
        m_students.append(Student("孙七", "2022002", 95.5, 2, "软件工程"));
    }
    
    // 行数 = 学生数量
    int rowCount(const QModelIndex &parent = QModelIndex()) const override {
        if (parent.isValid())
            return 0;
        return m_students.size();
    }
    
    // 列数 = 字段数量(6列)
    int columnCount(const QModelIndex &parent = QModelIndex()) const override {
        if (parent.isValid())
            return 0;
        return 6;  // 姓名、学号、成绩、年级、专业、备注
    }
    
private:
    QVector<Student> m_students;
};

6.3.4 data()的多角色实现
cpp 复制代码
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()];
    int col = index.column();
    
    // DisplayRole - 显示文本
    if (role == Qt::DisplayRole || role == Qt::EditRole) {
        switch (col) {
        case 0: return student.name;
        case 1: return student.studentId;
        case 2: return QString::number(student.score, 'f', 1);
        case 3: return student.grade;
        case 4: return student.major;
        case 5: return student.note;
        }
    }
    
    // TextAlignmentRole - 文本对齐
    else if (role == Qt::TextAlignmentRole) {
        if (col == 2 || col == 3) {
            // 成绩和年级右对齐
            return QVariant(Qt::AlignRight | Qt::AlignVCenter);
        }
        return QVariant(Qt::AlignLeft | Qt::AlignVCenter);
    }
    
    // ForegroundRole - 前景色(根据成绩)
    else if (role == Qt::ForegroundRole) {
        if (col == 2) {  // 成绩列
            if (student.score >= 90) {
                return QColor(0, 150, 0);  // 优秀:绿色
            } else if (student.score >= 80) {
                return QColor(0, 0, 200);  // 良好:蓝色
            } else if (student.score >= 60) {
                return QColor(0, 0, 0);    // 及格:黑色
            } else {
                return QColor(200, 0, 0);  // 不及格:红色
            }
        }
    }
    
    // BackgroundRole - 背景色
    else if (role == Qt::BackgroundRole) {
        if (col == 2 && student.score < 60) {
            return QColor(255, 220, 220);  // 不及格背景为浅红色
        }
        if (col == 2 && student.score >= 90) {
            return QColor(220, 255, 220);  // 优秀背景为浅绿色
        }
    }
    
    // FontRole - 字体
    else if (role == Qt::FontRole) {
        if (col == 2 && student.score >= 90) {
            QFont font;
            font.setBold(true);  // 优秀成绩加粗
            return font;
        }
    }
    
    // ToolTipRole - 提示信息
    else if (role == Qt::ToolTipRole) {
        QString tooltip = QString("<b>%1</b> (%2)<br>").arg(student.name, student.studentId);
        tooltip += QString("成绩: %1 分<br>").arg(student.score, 0, 'f', 1);
        tooltip += QString("年级: %1<br>").arg(student.grade);
        tooltip += QString("专业: %1").arg(student.major);
        if (!student.note.isEmpty()) {
            tooltip += QString("<br>备注: %1").arg(student.note);
        }
        return tooltip;
    }
    
    return QVariant();
}

6.3.5 setData()实现数据编辑
cpp 复制代码
Qt::ItemFlags flags(const QModelIndex &index) const override {
    if (!index.isValid())
        return Qt::NoItemFlags;
    
    // 学号列不可编辑
    if (index.column() == 1)
        return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
    
    return Qt::ItemIsEditable | Qt::ItemIsEnabled | Qt::ItemIsSelectable;
}

bool setData(const QModelIndex &index, const QVariant &value, 
             int role = Qt::EditRole) override {
    if (!index.isValid() || index.row() >= m_students.size())
        return false;
    
    if (role != Qt::EditRole)
        return false;
    
    Student &student = m_students[index.row()];
    int col = index.column();
    
    bool changed = false;
    
    switch (col) {
    case 0:  // 姓名
        if (value.toString() != student.name) {
            student.name = value.toString();
            changed = true;
        }
        break;
        
    case 1:  // 学号(不可编辑,这里不应该到达)
        return false;
        
    case 2:  // 成绩
        {
            bool ok;
            double score = value.toDouble(&ok);
            if (ok && score >= 0 && score <= 100 && score != student.score) {
                student.score = score;
                changed = true;
            }
        }
        break;
        
    case 3:  // 年级
        {
            bool ok;
            int grade = value.toInt(&ok);
            if (ok && grade >= 1 && grade <= 4 && grade != student.grade) {
                student.grade = grade;
                changed = true;
            }
        }
        break;
        
    case 4:  // 专业
        if (value.toString() != student.major) {
            student.major = value.toString();
            changed = true;
        }
        break;
        
    case 5:  // 备注
        if (value.toString() != student.note) {
            student.note = value.toString();
            changed = true;
        }
        break;
    }
    
    if (changed) {
        // 发送数据变化信号
        emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
        return true;
    }
    
    return false;
}

6.3.6 headerData()实现表头
cpp 复制代码
QVariant headerData(int section, Qt::Orientation orientation, 
                    int role = Qt::DisplayRole) const override {
    if (role == Qt::DisplayRole) {
        if (orientation == Qt::Horizontal) {
            // 列表头
            switch (section) {
            case 0: return "姓名";
            case 1: return "学号";
            case 2: return "成绩";
            case 3: return "年级";
            case 4: return "专业";
            case 5: return "备注";
            }
        } else {
            // 行表头(行号)
            return QString::number(section + 1);
        }
    }
    
    // 表头对齐
    else if (role == Qt::TextAlignmentRole) {
        return Qt::AlignCenter;
    }
    
    // 表头字体
    else if (role == Qt::FontRole) {
        QFont font;
        font.setBold(true);
        return font;
    }
    
    // 表头提示
    else if (role == Qt::ToolTipRole && orientation == Qt::Horizontal) {
        switch (section) {
        case 0: return "学生姓名";
        case 1: return "学号(不可编辑)";
        case 2: return "成绩(0-100分)";
        case 3: return "年级(1-4)";
        case 4: return "专业";
        case 5: return "备注信息";
        }
    }
    
    return QVariant();
}

6.3.7 行的增删功能
cpp 复制代码
// 添加学生
void addStudent(const Student &student) {
    int row = m_students.size();
    beginInsertRows(QModelIndex(), row, row);
    m_students.append(student);
    endInsertRows();
}

// 删除学生
bool removeStudent(int row) {
    if (row < 0 || row >= m_students.size())
        return false;
    
    beginRemoveRows(QModelIndex(), row, row);
    m_students.removeAt(row);
    endRemoveRows();
    
    return true;
}

// 批量删除
bool removeStudents(const QList<int> &rows) {
    // 从后往前删除,避免索引变化
    QList<int> sortedRows = rows;
    std::sort(sortedRows.begin(), sortedRows.end(), std::greater<int>());
    
    for (int row : sortedRows) {
        if (!removeStudent(row))
            return false;
    }
    
    return true;
}

// 插入行的标准接口
bool insertRows(int row, int count, 
                const QModelIndex &parent = QModelIndex()) override {
    if (parent.isValid() || row < 0 || row > m_students.size())
        return false;
    
    beginInsertRows(parent, row, row + count - 1);
    
    for (int i = 0; i < count; ++i) {
        m_students.insert(row, Student("新学生", "", 0.0, 1, ""));
    }
    
    endInsertRows();
    return true;
}

// 删除行的标准接口
bool removeRows(int row, int count,
                const QModelIndex &parent = QModelIndex()) override {
    if (parent.isValid() || row < 0 || row + count > m_students.size())
        return false;
    
    beginRemoveRows(parent, row, row + count - 1);
    
    for (int i = 0; i < count; ++i) {
        m_students.removeAt(row);
    }
    
    endRemoveRows();
    return true;
}

// 统计功能
double getAverageScore() const {
    if (m_students.isEmpty())
        return 0.0;
    
    double sum = 0.0;
    for (const Student &s : m_students) {
        sum += s.score;
    }
    return sum / m_students.size();
}

double getMaxScore() const {
    if (m_students.isEmpty())
        return 0.0;
    
    double max = m_students.first().score;
    for (const Student &s : m_students) {
        if (s.score > max)
            max = s.score;
    }
    return max;
}

double getMinScore() const {
    if (m_students.isEmpty())
        return 0.0;
    
    double min = m_students.first().score;
    for (const Student &s : m_students) {
        if (s.score < min)
            min = s.score;
    }
    return min;
}

6.3.8 完整代码示例
cpp 复制代码
#include <QApplication>
#include <QWidget>
#include <QTableView>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QInputDialog>
#include <QMessageBox>
#include <QHeaderView>
#include <QLabel>

// Student结构体和StudentTableModel类(如上所示)

class StudentManagerWidget : public QWidget {
    Q_OBJECT
    
private:
    StudentTableModel *m_model;
    QTableView *m_view;
    QLabel *m_statsLabel;
    
public:
    StudentManagerWidget(QWidget *parent = nullptr) : QWidget(parent) {
        setupUI();
        setWindowTitle("学生信息管理系统");
        resize(900, 600);
    }
    
private:
    void setupUI() {
        // 创建模型
        m_model = new StudentTableModel(this);
        
        // 创建视图
        m_view = new QTableView;
        m_view->setModel(m_model);
        m_view->setSelectionBehavior(QAbstractItemView::SelectRows);
        m_view->setSelectionMode(QAbstractItemView::ExtendedSelection);
        m_view->setAlternatingRowColors(true);
        m_view->setSortingEnabled(true);
        
        // 设置列宽
        m_view->setColumnWidth(0, 100);  // 姓名
        m_view->setColumnWidth(1, 120);  // 学号
        m_view->setColumnWidth(2, 80);   // 成绩
        m_view->setColumnWidth(3, 60);   // 年级
        m_view->setColumnWidth(4, 150);  // 专业
        m_view->horizontalHeader()->setStretchLastSection(true);  // 备注列自动拉伸
        
        // 统计标签
        m_statsLabel = new QLabel;
        updateStats();
        
        // 按钮
        QPushButton *addBtn = new QPushButton("添加学生");
        QPushButton *deleteBtn = new QPushButton("删除学生");
        QPushButton *refreshStatsBtn = new QPushButton("刷新统计");
        
        connect(addBtn, &QPushButton::clicked, this, &StudentManagerWidget::addStudent);
        connect(deleteBtn, &QPushButton::clicked, this, &StudentManagerWidget::deleteStudent);
        connect(refreshStatsBtn, &QPushButton::clicked, this, &StudentManagerWidget::updateStats);
        
        // 布局
        QVBoxLayout *mainLayout = new QVBoxLayout;
        mainLayout->addWidget(m_view);
        mainLayout->addWidget(m_statsLabel);
        
        QHBoxLayout *btnLayout = new QHBoxLayout;
        btnLayout->addWidget(addBtn);
        btnLayout->addWidget(deleteBtn);
        btnLayout->addWidget(refreshStatsBtn);
        btnLayout->addStretch();
        
        mainLayout->addLayout(btnLayout);
        setLayout(mainLayout);
    }
    
    void updateStats() {
        double avg = m_model->getAverageScore();
        double max = m_model->getMaxScore();
        double min = m_model->getMinScore();
        int count = m_model->rowCount();
        
        QString stats = QString("学生总数: %1 | 平均分: %2 | 最高分: %3 | 最低分: %4")
            .arg(count)
            .arg(avg, 0, 'f', 1)
            .arg(max, 0, 'f', 1)
            .arg(min, 0, 'f', 1);
        
        m_statsLabel->setText(stats);
    }
    
private slots:
    void addStudent() {
        bool ok;
        QString name = QInputDialog::getText(this, "添加学生", "姓名:",
                                             QLineEdit::Normal, "", &ok);
        if (!ok || name.isEmpty())
            return;
        
        QString studentId = QInputDialog::getText(this, "添加学生", "学号:",
                                                  QLineEdit::Normal, "", &ok);
        if (!ok || studentId.isEmpty())
            return;
        
        double score = QInputDialog::getDouble(this, "添加学生", "成绩:",
                                               0, 0, 100, 1, &ok);
        if (!ok)
            return;
        
        int grade = QInputDialog::getInt(this, "添加学生", "年级:",
                                         1, 1, 4, 1, &ok);
        if (!ok)
            return;
        
        QString major = QInputDialog::getText(this, "添加学生", "专业:",
                                              QLineEdit::Normal, "计算机科学", &ok);
        if (!ok)
            return;
        
        Student newStudent(name, studentId, score, grade, major);
        m_model->addStudent(newStudent);
        updateStats();
    }
    
    void deleteStudent() {
        QModelIndexList selected = m_view->selectionModel()->selectedRows();
        if (selected.isEmpty()) {
            QMessageBox::warning(this, "提示", "请选择要删除的学生");
            return;
        }
        
        QMessageBox::StandardButton reply = QMessageBox::question(
            this, "确认删除",
            QString("确定要删除选中的 %1 名学生吗?").arg(selected.size()),
            QMessageBox::Yes | QMessageBox::No);
        
        if (reply == QMessageBox::Yes) {
            QList<int> rows;
            for (const QModelIndex &index : selected) {
                rows.append(index.row());
            }
            m_model->removeStudents(rows);
            updateStats();
        }
    }
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    StudentManagerWidget widget;
    widget.show();
    
    return app.exec();
}

#include "main.moc"

本节小结

完整的表格模型 - 从数据结构到视图展示

rowCount/columnCount - 控制表格尺寸

多角色data() - 丰富的视觉效果(颜色、字体、对齐)

setData()编辑 - 支持单元格编辑和数据验证

headerData() - 自定义表头样式

增删操作 - insertRows/removeRows的标准实现

统计功能 - 数据汇总和分析

关键技术点

  1. 使用结构体存储完整的行数据
  2. 在data()中根据角色返回不同类型的数据
  3. 在setData()中进行数据验证
  4. 正确使用begin/endInsertRows和begin/endRemoveRows
  5. 根据数据值动态设置显示样式
  • 需求分析:姓名、学号、成绩、备注
  • 数据结构:使用QVector
  • rowCount和columnCount实现
  • data()的多角色实现
  • setData()实现数据编辑
  • headerData()实现表头
  • 行的增删功能
  • 完整代码示例

6.4 树形模型自定义

  • 实战项目:部门员工层级结构
    • 需求分析
    • 树节点类设计(TreeItem)
    • 父子关系的维护
    • index()的实现技巧
    • parent()的实现技巧
    • internalPointer的妙用
    • 动态添加/删除节点
    • 完整代码示例

6.5 可编辑模型

  • flags()返回可编辑标志
  • setData()完整实现
  • dataChanged信号的触发
  • 实战:可编辑的配置项管理器

6.6 可排序模型

  • sort()方法实现
  • layoutAboutToBeChanged和layoutChanged的使用
  • QPersistentModelIndex的必要性
  • 实战:带排序的数据表格

6.7 动态数据模型

  • insertRows()和removeRows()实现
  • beginInsertRows()和endInsertRows()
  • beginRemoveRows()和endRemoveRows()
  • beginResetModel()和endResetModel()
  • 实战:实时日志查看器

第7章 选择模型(QItemSelectionModel)

7.1 选择模型基础

选择模型(QItemSelectionModel)是Model/View架构中非常重要的一个组件,负责管理视图中的选择状态。

7.1.1 QItemSelectionModel的作用

核心职责

  1. 跟踪选择状态 - 记录哪些项被选中
  2. 跟踪当前项 - 记录当前焦点项
  3. 发送选择信号 - 通知视图选择变化
  4. 提供选择接口 - 允许程序化地控制选择

为什么需要独立的选择模型

cpp 复制代码
// 如果没有独立的选择模型:
QTableView *view1 = new QTableView;
QTableView *view2 = new QTableView;
view1->setModel(model);
view2->setModel(model);

// 问题:view1和view2的选择状态是独立的,无法同步
// view1选择了某些行,view2不知道

// 有了独立的选择模型:
QItemSelectionModel *selectionModel = new QItemSelectionModel(model);
view1->setSelectionModel(selectionModel);
view2->setSelectionModel(selectionModel);

// 现在view1和view2共享同一个选择模型
// 在任一视图中选择项,另一个视图也会同步更新

选择模型的优点

  • ✅ 分离关注点 - 数据、显示、选择各自独立
  • ✅ 支持共享 - 多个视图可以共享同一个选择模型
  • ✅ 灵活控制 - 可以程序化地控制选择
  • ✅ 便于扩展 - 可以自定义选择行为

7.1.2 选择模型与视图的关系

架构关系图

复制代码
┌─────────────┐
│   Model     │ ◄─────┐
│  (数据)      │       │
└─────────────┘       │
                      │
                      │ 关联
┌─────────────┐       │
│ Selection   │       │
│   Model     │ ◄─────┤
│  (选择状态)  │       │
└─────────────┘       │
       ▲              │
       │              │
       │ 使用          │
       │              │
┌─────────────┐       │
│    View     │ ──────┘
│  (视图显示)  │
└─────────────┘

关系说明

cpp 复制代码
// 创建模型
QStandardItemModel *model = new QStandardItemModel(10, 3);

// 创建视图
QTableView *view = new QTableView;
view->setModel(model);

// 此时会自动创建选择模型
// view内部:
// m_selectionModel = new QItemSelectionModel(model);

// 视图、模型、选择模型之间的关系:
// 1. 视图持有模型的指针
// 2. 视图持有选择模型的指针
// 3. 选择模型持有模型的指针

一对多关系

cpp 复制代码
// 一个模型可以有多个视图
QStandardItemModel *model = new QStandardItemModel;

QTableView *tableView = new QTableView;
tableView->setModel(model);

QListView *listView = new QListView;
listView->setModel(model);

// 每个视图有自己的选择模型(默认情况)
// tableView->selectionModel() != listView->selectionModel()

// 但可以共享选择模型
QItemSelectionModel *sharedSelection = new QItemSelectionModel(model);
tableView->setSelectionModel(sharedSelection);
listView->setSelectionModel(sharedSelection);

// 现在两个视图的选择是同步的

7.1.3 获取视图的选择模型

自动创建的选择模型

cpp 复制代码
QTableView *view = new QTableView;
view->setModel(someModel);

// 获取自动创建的选择模型
QItemSelectionModel *selectionModel = view->selectionModel();

// 注意:在调用setModel()之前,selectionModel()返回nullptr

手动设置选择模型

cpp 复制代码
// 创建自定义选择模型
QItemSelectionModel *customSelection = new QItemSelectionModel(model);

// 设置到视图
view->setSelectionModel(customSelection);

// 获取
QItemSelectionModel *sm = view->selectionModel();
// sm == customSelection

选择模型的生命周期

cpp 复制代码
// 方式1:自动管理(推荐)
QTableView *view = new QTableView(parent);
view->setModel(model);
// 选择模型由视图自动创建和管理,视图销毁时自动销毁

// 方式2:手动管理
QItemSelectionModel *selection = new QItemSelectionModel(model);
view->setSelectionModel(selection);
// 需要注意选择模型的生命周期,通常设置父对象:
QItemSelectionModel *selection = new QItemSelectionModel(model, view);

基本使用示例

cpp 复制代码
#include <QApplication>
#include <QTableView>
#include <QStandardItemModel>
#include <QVBoxLayout>
#include <QLabel>
#include <QWidget>

class SelectionDemo : public QWidget {
    Q_OBJECT
    
private:
    QTableView *m_view;
    QStandardItemModel *m_model;
    QLabel *m_statusLabel;
    
public:
    SelectionDemo(QWidget *parent = nullptr) : QWidget(parent) {
        // 创建模型
        m_model = new QStandardItemModel(5, 3, this);
        for (int row = 0; row < 5; ++row) {
            for (int col = 0; col < 3; ++col) {
                QStandardItem *item = new QStandardItem(
                    QString("(%1, %2)").arg(row).arg(col));
                m_model->setItem(row, col, item);
            }
        }
        
        // 创建视图
        m_view = new QTableView;
        m_view->setModel(m_model);
        
        // 状态标签
        m_statusLabel = new QLabel("未选择任何项");
        
        // 获取选择模型并连接信号
        QItemSelectionModel *selectionModel = m_view->selectionModel();
        
        connect(selectionModel, &QItemSelectionModel::selectionChanged,
                this, &SelectionDemo::onSelectionChanged);
        
        connect(selectionModel, &QItemSelectionModel::currentChanged,
                this, &SelectionDemo::onCurrentChanged);
        
        // 布局
        QVBoxLayout *layout = new QVBoxLayout;
        layout->addWidget(m_view);
        layout->addWidget(m_statusLabel);
        setLayout(layout);
        
        resize(500, 400);
    }
    
private slots:
    void onSelectionChanged(const QItemSelection &selected, 
                           const QItemSelection &deselected) {
        QItemSelectionModel *sm = m_view->selectionModel();
        QModelIndexList indexes = sm->selectedIndexes();
        
        m_statusLabel->setText(QString("已选择 %1 个单元格").arg(indexes.size()));
    }
    
    void onCurrentChanged(const QModelIndex &current, 
                         const QModelIndex &previous) {
        if (current.isValid()) {
            QString text = m_model->data(current).toString();
            qDebug() << "当前项:" << text;
        }
    }
};

获取选择信息的常用方法

cpp 复制代码
QItemSelectionModel *sm = view->selectionModel();

// 1. 获取当前项(焦点项)
QModelIndex current = sm->currentIndex();

// 2. 获取所有选中的索引
QModelIndexList selected = sm->selectedIndexes();

// 3. 获取选中的行
QModelIndexList rows = sm->selectedRows();

// 4. 获取选中的列
QModelIndexList cols = sm->selectedColumns();

// 5. 获取选中的行号(去重)
QSet<int> rowSet;
for (const QModelIndex &index : sm->selectedIndexes()) {
    rowSet.insert(index.row());
}

// 6. 检查某个索引是否被选中
bool isSelected = sm->isSelected(index);

// 7. 检查某行是否被选中
bool isRowSelected = sm->isRowSelected(row, QModelIndex());

// 8. 检查某列是否被选中
bool isColumnSelected = sm->isColumnSelected(column, QModelIndex());

// 9. 检查是否有选择
bool hasSelection = sm->hasSelection();

本节小结

QItemSelectionModel - Model/View架构的选择管理器

分离关注点 - 数据、显示、选择各自独立

自动创建 - setModel()时自动创建选择模型

可共享 - 多个视图可共享同一选择模型

丰富接口 - 提供多种获取选择信息的方法

关键要点

  1. 选择模型是视图的一部分,但独立于数据模型
  2. 每个视图默认有自己的选择模型
  3. 可以通过setSelectionModel()共享选择模型
  4. 使用selectionModel()获取并监听选择变化
  • QItemSelectionModel的作用
  • 选择模型与视图的关系
  • 获取视图的选择模型

7.2 选择操作

通过QItemSelectionModel,我们可以程序化地控制视图中的选择状态。

7.2.1 选择项、行、列

选择单个项

cpp 复制代码
QItemSelectionModel *sm = view->selectionModel();

// 选择一个索引
QModelIndex index = model->index(2, 1);
sm->select(index, QItemSelectionModel::Select);

// 选择标志说明:
// Select - 选中指定项
// Deselect - 取消选中指定项
// Toggle - 切换选中状态
// Current - 设置为当前项(焦点项)
// Rows - 选择整行
// Columns - 选择整列
// Clear - 清除所有选择

选择标志组合

cpp 复制代码
// 选中并设为当前项
sm->select(index, QItemSelectionModel::Select | 
                  QItemSelectionModel::Current);

// 选择整行
sm->select(index, QItemSelectionModel::Select | 
                  QItemSelectionModel::Rows);

// 选择整列
sm->select(index, QItemSelectionModel::Select | 
                  QItemSelectionModel::Columns);

// 清除所有选择并选中新项
sm->select(index, QItemSelectionModel::ClearAndSelect);

// 清除所有选择并选中整行
sm->select(index, QItemSelectionModel::ClearAndSelect | 
                  QItemSelectionModel::Rows);

选择行

cpp 复制代码
// 方法1:使用Rows标志
QModelIndex rowIndex = model->index(2, 0);  // 任意列都可以
sm->select(rowIndex, QItemSelectionModel::Select | 
                     QItemSelectionModel::Rows);

// 方法2:手动选择该行的所有索引
QItemSelection selection;
int row = 2;
for (int col = 0; col < model->columnCount(); ++col) {
    QModelIndex idx = model->index(row, col);
    selection.select(idx, idx);
}
sm->select(selection, QItemSelectionModel::Select);

// 方法3:使用便利方法(如果模型支持)
sm->select(model->index(row, 0), 
           QItemSelectionModel::Select | QItemSelectionModel::Rows);

选择列

cpp 复制代码
// 使用Columns标志
QModelIndex colIndex = model->index(0, 2);  // 任意行都可以
sm->select(colIndex, QItemSelectionModel::Select | 
                     QItemSelectionModel::Columns);

选择范围

cpp 复制代码
// 选择一个矩形区域
QModelIndex topLeft = model->index(1, 1);
QModelIndex bottomRight = model->index(3, 3);

QItemSelection selection(topLeft, bottomRight);
sm->select(selection, QItemSelectionModel::Select);

// 或者直接使用索引范围
sm->select(QItemSelection(topLeft, bottomRight), 
           QItemSelectionModel::Select);

7.2.2 清除选择

清除所有选择

cpp 复制代码
// 方法1:使用Clear标志
sm->select(QModelIndex(), QItemSelectionModel::Clear);

// 方法2:使用clearSelection()
sm->clearSelection();

// 方法3:使用reset()(同时清除选择和当前项)
sm->reset();

// 方法4:清除选择但保留当前项
sm->clearSelection();
// 当前项仍然保留,只是没有被选中

清除当前项

cpp 复制代码
// 清除当前项
sm->setCurrentIndex(QModelIndex(), QItemSelectionModel::Clear);

// 或者
sm->clearCurrentIndex();

取消选择特定项

cpp 复制代码
// 取消选择单个索引
sm->select(index, QItemSelectionModel::Deselect);

// 取消选择整行
sm->select(index, QItemSelectionModel::Deselect | 
                  QItemSelectionModel::Rows);

// 取消选择范围
QItemSelection selection(topLeft, bottomRight);
sm->select(selection, QItemSelectionModel::Deselect);

7.2.3 当前项 vs 选中项

概念区别

复制代码
┌────────────────────────────┐
│  当前项 (Current Item)       │
│  - 有焦点的项(蓝色边框)     │
│  - 只能有一个                │
│  - 用于键盘导航              │
│  - 不一定被选中              │
└────────────────────────────┘

┌────────────────────────────┐
│  选中项 (Selected Items)    │
│  - 高亮显示的项              │
│  - 可以有多个                │
│  - 用于批量操作              │
│  - 当前项可能不在其中        │
└────────────────────────────┘

设置和获取当前项

cpp 复制代码
// 设置当前项(不改变选择)
QModelIndex index = model->index(2, 1);
sm->setCurrentIndex(index, QItemSelectionModel::NoUpdate);

// 设置当前项并选中
sm->setCurrentIndex(index, QItemSelectionModel::Select);

// 设置当前项并清除其他选择
sm->setCurrentIndex(index, QItemSelectionModel::ClearAndSelect);

// 获取当前项
QModelIndex current = sm->currentIndex();

// 检查是否有当前项
bool hasCurrent = current.isValid();

选中项操作

cpp 复制代码
// 获取所有选中的索引
QModelIndexList selected = sm->selectedIndexes();

// 获取选中的行(每行只返回一个索引)
QModelIndexList rows = sm->selectedRows();

// 获取选中的列(每列只返回一个索引)
QModelIndexList columns = sm->selectedColumns();

// 获取第2列中被选中的索引
QModelIndexList col2Selected = sm->selectedRows(2);

// 检查是否有选择
bool hasSelection = sm->hasSelection();

示例:当前项与选中项的区别

cpp 复制代码
class CurrentVsSelectedDemo : public QWidget {
    Q_OBJECT
    
private:
    QTableView *m_view;
    QLabel *m_currentLabel;
    QLabel *m_selectedLabel;
    
public:
    CurrentVsSelectedDemo(QWidget *parent = nullptr) : QWidget(parent) {
        QStandardItemModel *model = new QStandardItemModel(5, 3, this);
        for (int r = 0; r < 5; ++r) {
            for (int c = 0; c < 3; ++c) {
                model->setItem(r, c, new QStandardItem(
                    QString("(%1,%2)").arg(r).arg(c)));
            }
        }
        
        m_view = new QTableView;
        m_view->setModel(model);
        
        m_currentLabel = new QLabel("当前项: 无");
        m_selectedLabel = new QLabel("选中项: 0 个");
        
        QItemSelectionModel *sm = m_view->selectionModel();
        
        connect(sm, &QItemSelectionModel::currentChanged,
                [=](const QModelIndex &current, const QModelIndex &) {
            if (current.isValid()) {
                m_currentLabel->setText(QString("当前项: (%1, %2)")
                    .arg(current.row()).arg(current.column()));
            } else {
                m_currentLabel->setText("当前项: 无");
            }
        });
        
        connect(sm, &QItemSelectionModel::selectionChanged,
                [=]() {
            int count = sm->selectedIndexes().size();
            m_selectedLabel->setText(QString("选中项: %1 个").arg(count));
        });
        
        QVBoxLayout *layout = new QVBoxLayout;
        layout->addWidget(m_view);
        layout->addWidget(m_currentLabel);
        layout->addWidget(m_selectedLabel);
        setLayout(layout);
    }
};

7.2.4 选择模式

选择模式类型

cpp 复制代码
// 设置选择模式
view->setSelectionMode(QAbstractItemView::SingleSelection);

// 可用的选择模式:
// SingleSelection - 单选(一次只能选一个项)
// MultiSelection - 多选(Ctrl+点击添加/移除选择)
// ExtendedSelection - 扩展选择(Ctrl多选,Shift范围选择) - 默认
// ContiguousSelection - 连续选择(只能选择连续的项)
// NoSelection - 不可选择

SingleSelection - 单选模式

cpp 复制代码
view->setSelectionMode(QAbstractItemView::SingleSelection);

// 特点:
// - 一次只能选择一个项
// - 选择新项时,自动取消之前的选择
// - 适用于只需要一个选择的场景

MultiSelection - 多选模式

cpp 复制代码
view->setSelectionMode(QAbstractItemView::MultiSelection);

// 特点:
// - 每次点击都会切换该项的选中状态
// - 不需要按Ctrl键
// - 无法通过拖拽选择范围
// - 适用于复选框式的多选

ExtendedSelection - 扩展选择模式(默认)

cpp 复制代码
view->setSelectionMode(QAbstractItemView::ExtendedSelection);

// 特点:
// - 单击选择单个项
// - Ctrl+点击:添加/移除单个项到选择
// - Shift+点击:选择从当前项到点击项的范围
// - 拖拽:选择矩形区域
// - 最常用的模式

ContiguousSelection - 连续选择模式

cpp 复制代码
view->setSelectionMode(QAbstractItemView::ContiguousSelection);

// 特点:
// - 只能选择连续的项
// - Shift+点击选择范围
// - 不支持Ctrl多选
// - 适用于需要连续选择的场景

NoSelection - 不可选择

cpp 复制代码
view->setSelectionMode(QAbstractItemView::NoSelection);

// 特点:
// - 用户无法通过鼠标/键盘选择项
// - 仍然可以通过程序设置选择
// - 适用于只读显示的场景

选择行为

cpp 复制代码
// 设置选择行为
view->setSelectionBehavior(QAbstractItemView::SelectRows);

// 可用的选择行为:
// SelectItems - 选择单个单元格(默认)
// SelectRows - 选择整行
// SelectColumns - 选择整列

完整示例

cpp 复制代码
class SelectionModeDemo : public QWidget {
    Q_OBJECT
    
private:
    QTableView *m_view;
    QComboBox *m_modeCombo;
    QComboBox *m_behaviorCombo;
    
public:
    SelectionModeDemo(QWidget *parent = nullptr) : QWidget(parent) {
        // 创建模型
        QStandardItemModel *model = new QStandardItemModel(10, 5, this);
        for (int r = 0; r < 10; ++r) {
            for (int c = 0; c < 5; ++c) {
                model->setItem(r, c, new QStandardItem(
                    QString("(%1,%2)").arg(r).arg(c)));
            }
        }
        
        // 创建视图
        m_view = new QTableView;
        m_view->setModel(model);
        
        // 选择模式下拉框
        m_modeCombo = new QComboBox;
        m_modeCombo->addItem("单选", QAbstractItemView::SingleSelection);
        m_modeCombo->addItem("多选", QAbstractItemView::MultiSelection);
        m_modeCombo->addItem("扩展选择", QAbstractItemView::ExtendedSelection);
        m_modeCombo->addItem("连续选择", QAbstractItemView::ContiguousSelection);
        m_modeCombo->addItem("不可选择", QAbstractItemView::NoSelection);
        m_modeCombo->setCurrentIndex(2);  // 默认扩展选择
        
        connect(m_modeCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
                [=](int index) {
            auto mode = m_modeCombo->itemData(index).value<QAbstractItemView::SelectionMode>();
            m_view->setSelectionMode(mode);
        });
        
        // 选择行为下拉框
        m_behaviorCombo = new QComboBox;
        m_behaviorCombo->addItem("选择单元格", QAbstractItemView::SelectItems);
        m_behaviorCombo->addItem("选择行", QAbstractItemView::SelectRows);
        m_behaviorCombo->addItem("选择列", QAbstractItemView::SelectColumns);
        
        connect(m_behaviorCombo, QOverload<int>::of(&QComboBox::currentIndexChanged),
                [=](int index) {
            auto behavior = m_behaviorCombo->itemData(index)
                .value<QAbstractItemView::SelectionBehavior>();
            m_view->setSelectionBehavior(behavior);
        });
        
        // 布局
        QVBoxLayout *layout = new QVBoxLayout;
        
        QHBoxLayout *controlLayout = new QHBoxLayout;
        controlLayout->addWidget(new QLabel("选择模式:"));
        controlLayout->addWidget(m_modeCombo);
        controlLayout->addWidget(new QLabel("选择行为:"));
        controlLayout->addWidget(m_behaviorCombo);
        controlLayout->addStretch();
        
        layout->addLayout(controlLayout);
        layout->addWidget(m_view);
        setLayout(layout);
        
        resize(700, 500);
    }
};

本节小结

选择操作 - select()方法及各种标志组合

清除选择 - clearSelection()、reset()等方法

当前项vs选中项 - 两个独立的概念

选择模式 - 5种模式满足不同需求

选择行为 - Items/Rows/Columns三种行为

关键要点

  1. 使用QItemSelectionModel::SelectionFlags控制选择行为
  2. 当前项(焦点)和选中项是两个不同的概念
  3. 选择模式决定用户如何进行选择操作
  4. 选择行为决定选择的粒度(单元格/行/列)
  • 选择项、行、列
  • 清除选择
  • 当前项 vs 选中项
  • 选择模式:单选、多选、扩展选择、连续选择

7.3 选择相关信号

QItemSelectionModel提供了多个信号来通知选择状态的变化,这些信号对于构建交互式应用非常重要。

7.3.1 currentChanged() - 当前项变化

信号定义

cpp 复制代码
void currentChanged(const QModelIndex &current, const QModelIndex &previous);

使用场景

  • 跟踪用户的焦点变化
  • 更新详情面板
  • 响应键盘导航

示例

cpp 复制代码
QItemSelectionModel *sm = view->selectionModel();

connect(sm, &QItemSelectionModel::currentChanged,
        [=](const QModelIndex &current, const QModelIndex &previous) {
    if (current.isValid()) {
        qDebug() << "当前项变为:" << current.row() << current.column();
        // 显示当前项的详细信息
        showDetails(current);
    }
    
    if (previous.isValid()) {
        qDebug() << "之前的当前项:" << previous.row() << previous.column();
    }
});

7.3.2 selectionChanged() - 选择变化

信号定义

cpp 复制代码
void selectionChanged(const QItemSelection &selected, 
                      const QItemSelection &deselected);

参数说明

  • selected - 新选中的项
  • deselected - 取消选中的项

使用场景

  • 跟踪选择集合的变化
  • 更新批量操作按钮的可用性
  • 实时统计选中项数量

示例

cpp 复制代码
connect(sm, &QItemSelectionModel::selectionChanged,
        [=](const QItemSelection &selected, const QItemSelection &deselected) {
    qDebug() << "新选中" << selected.indexes().size() << "个项";
    qDebug() << "取消选中" << deselected.indexes().size() << "个项";
    
    // 获取所有选中的项
    QModelIndexList allSelected = sm->selectedIndexes();
    qDebug() << "总共选中" << allSelected.size() << "个项";
    
    // 更新UI状态
    deleteButton->setEnabled(allSelected.size() > 0);
});

高级用法

cpp 复制代码
// 检查特定行是否在选择变化中
connect(sm, &QItemSelectionModel::selectionChanged,
        [=](const QItemSelection &selected, const QItemSelection &deselected) {
    // 遍历新选中的范围
    for (const QItemSelectionRange &range : selected) {
        qDebug() << "选中范围:" << range.top() << "到" << range.bottom();
        
        // 遍历范围内的所有索引
        for (int row = range.top(); row <= range.bottom(); ++row) {
            for (int col = range.left(); col <= range.right(); ++col) {
                QModelIndex index = model->index(row, col);
                qDebug() << "选中:" << model->data(index).toString();
            }
        }
    }
});

7.3.3 实战:根据选择更新详情面板

完整示例:实现主从视图,选择改变时更新详情面板。

cpp 复制代码
#include <QApplication>
#include <QWidget>
#include <QTableView>
#include <QTextEdit>
#include <QSplitter>
#include <QVBoxLayout>
#include <QStandardItemModel>
#include <QLabel>

class MasterDetailView : public QWidget {
    Q_OBJECT
    
private:
    QTableView *m_tableView;
    QStandardItemModel *m_model;
    QTextEdit *m_detailsPanel;
    QLabel *m_statusLabel;
    
public:
    MasterDetailView(QWidget *parent = nullptr) : QWidget(parent) {
        setupUI();
        loadData();
        connectSignals();
        
        setWindowTitle("主从视图示例");
        resize(900, 600);
    }
    
private:
    void setupUI() {
        // 创建主视图(表格)
        m_tableView = new QTableView;
        m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
        m_tableView->setSelectionMode(QAbstractItemView::SingleSelection);
        m_tableView->setAlternatingRowColors(true);
        
        // 创建模型
        m_model = new QStandardItemModel(this);
        m_model->setHorizontalHeaderLabels({"ID", "姓名", "职位", "部门"});
        m_tableView->setModel(m_model);
        
        // 创建详情面板
        m_detailsPanel = new QTextEdit;
        m_detailsPanel->setReadOnly(true);
        m_detailsPanel->setPlaceholderText("选择一行查看详情...");
        
        // 状态标签
        m_statusLabel = new QLabel("未选择任何项");
        
        // 使用分割器
        QSplitter *splitter = new QSplitter(Qt::Horizontal);
        splitter->addWidget(m_tableView);
        splitter->addWidget(m_detailsPanel);
        splitter->setStretchFactor(0, 2);
        splitter->setStretchFactor(1, 1);
        
        // 布局
        QVBoxLayout *layout = new QVBoxLayout;
        layout->addWidget(splitter);
        layout->addWidget(m_statusLabel);
        setLayout(layout);
    }
    
    void loadData() {
        // 添加示例数据
        QList<QList<QString>> data = {
            {"01", "张三", "工程师", "技术部"},
            {"02", "李四", "设计师", "设计部"},
            {"03", "王五", "经理", "销售部"},
            {"04", "赵六", "分析师", "技术部"},
            {"05", "孙七", "主管", "人事部"}
        };
        
        for (const QList<QString> &row : data) {
            QList<QStandardItem*> items;
            for (const QString &text : row) {
                items << new QStandardItem(text);
            }
            m_model->appendRow(items);
        }
        
        // 设置列宽
        m_tableView->setColumnWidth(0, 60);
        m_tableView->setColumnWidth(1, 120);
        m_tableView->setColumnWidth(2, 120);
        m_tableView->setColumnWidth(3, 120);
    }
    
    void connectSignals() {
        QItemSelectionModel *sm = m_tableView->selectionModel();
        
        // 监听当前项变化
        connect(sm, &QItemSelectionModel::currentChanged,
                this, &MasterDetailView::onCurrentChanged);
        
        // 监听选择变化
        connect(sm, &QItemSelectionModel::selectionChanged,
                this, &MasterDetailView::onSelectionChanged);
    }
    
private slots:
    void onCurrentChanged(const QModelIndex &current, const QModelIndex &previous) {
        if (!current.isValid()) {
            m_detailsPanel->clear();
            return;
        }
        
        // 构建详情HTML
        QString html = "<h2>员工详细信息</h2>";
        html += "<table border='1' cellpadding='5'>";
        
        // 获取该行的所有数据
        int row = current.row();
        html += "<tr><td><b>ID:</b></td><td>" + 
                m_model->item(row, 0)->text() + "</td></tr>";
        html += "<tr><td><b>姓名:</b></td><td>" + 
                m_model->item(row, 1)->text() + "</td></tr>";
        html += "<tr><td><b>职位:</b></td><td>" + 
                m_model->item(row, 2)->text() + "</td></tr>";
        html += "<tr><td><b>部门:</b></td><td>" + 
                m_model->item(row, 3)->text() + "</td></tr>";
        
        html += "</table>";
        
        // 添加虚拟的额外信息
        html += "<h3>其他信息</h3>";
        html += "<p><b>入职日期:</b> 2020-01-15</p>";
        html += "<p><b>联系电话:</b> 138-1234-5678</p>";
        html += "<p><b>电子邮件:</b> " + 
                m_model->item(row, 1)->text().toLower() + "@company.com</p>";
        
        m_detailsPanel->setHtml(html);
    }
    
    void onSelectionChanged(const QItemSelection &selected, 
                           const QItemSelection &deselected) {
        QItemSelectionModel *sm = m_tableView->selectionModel();
        int count = sm->selectedRows().size();
        
        if (count == 0) {
            m_statusLabel->setText("未选择任何项");
        } else if (count == 1) {
            QModelIndex current = sm->currentIndex();
            QString name = m_model->item(current.row(), 1)->text();
            m_statusLabel->setText(QString("已选择: %1").arg(name));
        } else {
            m_statusLabel->setText(QString("已选择 %1 项").arg(count));
        }
    }
};

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    MasterDetailView widget;
    widget.show();
    
    return app.exec();
}

#include "main.moc"

信号使用技巧

cpp 复制代码
// 1. 只在必要时更新UI
connect(sm, &QItemSelectionModel::selectionChanged,
        [=]() {
    // 检查是否真的需要更新
    if (!sm->hasSelection()) {
        clearDetails();
        return;
    }
    updateDetails();
});

// 2. 防抖动(避免频繁更新)
QTimer *updateTimer = new QTimer(this);
updateTimer->setSingleShot(true);
updateTimer->setInterval(100);  // 100ms延迟

connect(sm, &QItemSelectionModel::selectionChanged,
        [=]() {
    updateTimer->start();
});

connect(updateTimer, &QTimer::timeout,
        [=]() {
    // 真正的更新操作
    updateHeavyDetails();
});

// 3. 批量处理选择变化
connect(sm, &QItemSelectionModel::selectionChanged,
        [=](const QItemSelection &selected, const QItemSelection &deselected) {
    QSet<int> affectedRows;
    
    // 收集所有受影响的行
    for (const QItemSelectionRange &range : selected) {
        for (int row = range.top(); row <= range.bottom(); ++row) {
            affectedRows.insert(row);
        }
    }
    
    for (const QItemSelectionRange &range : deselected) {
        for (int row = range.top(); row <= range.bottom(); ++row) {
            affectedRows.insert(row);
        }
    }
    
    // 批量更新
    for (int row : affectedRows) {
        updateRow(row);
    }
});

本节小结

currentChanged - 跟踪焦点项变化

selectionChanged - 跟踪选择集合变化

实战应用 - 主从视图的完整实现

性能优化 - 防抖动、批量处理

关键要点

  1. currentChanged用于单个焦点项的跟踪
  2. selectionChanged用于多选场景
  3. 可以组合使用两个信号以实现复杂交互
  4. 注意性能优化,避免频繁的UI更新
  • currentChanged() - 当前项变化
  • selectionChanged() - 选择变化
  • 实战:根据选择更新详情面板

7.4 QItemSelection

QItemSelection是一个用于表示选择范围集合的类,它使得批量选择操作更加方便。

7.4.1 QItemSelection的使用

基本概念

cpp 复制代码
// QItemSelection是QList<QItemSelectionRange>的typedef
// 它包含多个选择范围

// 创建选择
QItemSelection selection;

// 添加单个范围
QModelIndex topLeft = model->index(0, 0);
QModelIndex bottomRight = model->index(2, 2);
selection.select(topLeft, bottomRight);

// 或者直接构造
QItemSelection selection2(topLeft, bottomRight);

// 应用选择
sm->select(selection, QItemSelectionModel::Select);

QItemSelectionRange

cpp 复制代码
// 选择范围表示一个矩形区域
QItemSelectionRange range(topLeft, bottomRight);

// 获取范围信息
int top = range.top();        // 最上行
int bottom = range.bottom();  // 最下行
int left = range.left();      // 最左列
int right = range.right();    // 最右列
int width = range.width();    // 宽度
int height = range.height();  // 高度

// 检查是否包含某个索引
bool contains = range.contains(index);

// 获取所有索引
QModelIndexList indexes = range.indexes();

// 检查是否为空
bool isEmpty = range.isEmpty();
bool isValid = range.isValid();

7.4.2 选择范围的操作

合并选择

cpp 复制代码
QItemSelection selection1(index1, index2);
QItemSelection selection2(index3, index4);

// 合并两个选择
selection1.merge(selection2, QItemSelectionModel::Select);

// 应用合并后的选择
sm->select(selection1, QItemSelectionModel::Select);

选择的交集、并集、差集

cpp 复制代码
// 获取当前选择
QItemSelection current = sm->selection();

// 创建新选择
QItemSelection newSelection(topLeft, bottomRight);

// 并集:选中current和newSelection的所有项
QItemSelection united = current;
united.merge(newSelection, QItemSelectionModel::Select);
sm->select(united, QItemSelectionModel::ClearAndSelect);

// 差集:从current中移除newSelection
QItemSelection difference = current;
difference.merge(newSelection, QItemSelectionModel::Deselect);
sm->select(difference, QItemSelectionModel::ClearAndSelect);

遍历选择

cpp 复制代码
QItemSelection selection = sm->selection();

// 方法1:遍历范围
for (const QItemSelectionRange &range : selection) {
    qDebug() << "范围:" << range.top() << "-" << range.bottom()
             << "," << range.left() << "-" << range.right();
    
    // 遍历范围内的所有索引
    for (int row = range.top(); row <= range.bottom(); ++row) {
        for (int col = range.left(); col <= range.right(); ++col) {
            QModelIndex index = model->index(row, col);
            processIndex(index);
        }
    }
}

// 方法2:直接获取所有索引
QModelIndexList allIndexes = selection.indexes();
for (const QModelIndex &index : allIndexes) {
    processIndex(index);
}

7.4.3 实战:批量操作选中项

完整示例:实现批量编辑、批量导出等功能。

cpp 复制代码
class BatchOperationDemo : public QWidget {
    Q_OBJECT
    
private:
    QTableView *m_view;
    QStandardItemModel *m_model;
    
public:
    BatchOperationDemo(QWidget *parent = nullptr) : QWidget(parent) {
        setupUI();
        setWindowTitle("批量操作示例");
        resize(700, 500);
    }
    
private:
    void setupUI() {
        // 创建模型
        m_model = new QStandardItemModel(10, 4, this);
        m_model->setHorizontalHeaderLabels({"名称", "状态", "优先级", "备注"});
        
        // 添加示例数据
        for (int row = 0; row < 10; ++row) {
            m_model->setItem(row, 0, new QStandardItem(QString("任务%1").arg(row + 1)));
            m_model->setItem(row, 1, new QStandardItem("进行中"));
            m_model->setItem(row, 2, new QStandardItem("中"));
            m_model->setItem(row, 3, new QStandardItem(""));
        }
        
        // 创建视图
        m_view = new QTableView;
        m_view->setModel(m_model);
        m_view->setSelectionBehavior(QAbstractItemView::SelectRows);
        m_view->setSelectionMode(QAbstractItemView::ExtendedSelection);
        
        // 按钮
        QPushButton *selectAllBtn = new QPushButton("全选");
        QPushButton *selectNoneBtn = new QPushButton("取消全选");
        QPushButton *invertBtn = new QPushButton("反选");
        QPushButton *setStatusBtn = new QPushButton("批量设置状态");
        QPushButton *setPriorityBtn = new QPushButton("批量设置优先级");
        QPushButton *exportBtn = new QPushButton("导出选中项");
        
        connect(selectAllBtn, &QPushButton::clicked, this, &BatchOperationDemo::selectAll);
        connect(selectNoneBtn, &QPushButton::clicked, this, &BatchOperationDemo::selectNone);
        connect(invertBtn, &QPushButton::clicked, this, &BatchOperationDemo::invertSelection);
        connect(setStatusBtn, &QPushButton::clicked, this, &BatchOperationDemo::batchSetStatus);
        connect(setPriorityBtn, &QPushButton::clicked, this, &BatchOperationDemo::batchSetPriority);
        connect(exportBtn, &QPushButton::clicked, this, &BatchOperationDemo::exportSelected);
        
        // 布局
        QVBoxLayout *layout = new QVBoxLayout;
        layout->addWidget(m_view);
        
        QHBoxLayout *btnLayout1 = new QHBoxLayout;
        btnLayout1->addWidget(selectAllBtn);
        btnLayout1->addWidget(selectNoneBtn);
        btnLayout1->addWidget(invertBtn);
        
        QHBoxLayout *btnLayout2 = new QHBoxLayout;
        btnLayout2->addWidget(setStatusBtn);
        btnLayout2->addWidget(setPriorityBtn);
        btnLayout2->addWidget(exportBtn);
        btnLayout2->addStretch();
        
        layout->addLayout(btnLayout1);
        layout->addLayout(btnLayout2);
        setLayout(layout);
    }
    
private slots:
    void selectAll() {
        // 选择所有行
        QItemSelection selection;
        QModelIndex topLeft = m_model->index(0, 0);
        QModelIndex bottomRight = m_model->index(m_model->rowCount() - 1, 
                                                  m_model->columnCount() - 1);
        selection.select(topLeft, bottomRight);
        m_view->selectionModel()->select(selection, 
            QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
    }
    
    void selectNone() {
        m_view->selectionModel()->clearSelection();
    }
    
    void invertSelection() {
        // 反选
        QItemSelectionModel *sm = m_view->selectionModel();
        QSet<int> selectedRows;
        
        // 获取当前选中的行
        for (const QModelIndex &index : sm->selectedRows()) {
            selectedRows.insert(index.row());
        }
        
        // 清除当前选择
        sm->clearSelection();
        
        // 选择未选中的行
        for (int row = 0; row < m_model->rowCount(); ++row) {
            if (!selectedRows.contains(row)) {
                QModelIndex index = m_model->index(row, 0);
                sm->select(index, QItemSelectionModel::Select | 
                                  QItemSelectionModel::Rows);
            }
        }
    }
    
    void batchSetStatus() {
        QModelIndexList rows = m_view->selectionModel()->selectedRows();
        if (rows.isEmpty()) {
            QMessageBox::warning(this, "提示", "请先选择要修改的行");
            return;
        }
        
        QStringList items = {"进行中", "已完成", "暂停", "取消"};
        bool ok;
        QString status = QInputDialog::getItem(this, "设置状态", "选择状态:",
                                               items, 0, false, &ok);
        if (ok) {
            for (const QModelIndex &index : rows) {
                m_model->setData(m_model->index(index.row(), 1), status);
            }
        }
    }
    
    void batchSetPriority() {
        QModelIndexList rows = m_view->selectionModel()->selectedRows();
        if (rows.isEmpty()) {
            QMessageBox::warning(this, "提示", "请先选择要修改的行");
            return;
        }
        
        QStringList items = {"低", "中", "高"};
        bool ok;
        QString priority = QInputDialog::getItem(this, "设置优先级", "选择优先级:",
                                                 items, 1, false, &ok);
        if (ok) {
            for (const QModelIndex &index : rows) {
                m_model->setData(m_model->index(index.row(), 2), priority);
            }
        }
    }
    
    void exportSelected() {
        QItemSelection selection = m_view->selectionModel()->selection();
        if (selection.isEmpty()) {
            QMessageBox::warning(this, "提示", "请先选择要导出的行");
            return;
        }
        
        QString output;
        QModelIndexList rows = m_view->selectionModel()->selectedRows();
        
        // 表头
        output += "名称,状态,优先级,备注\n";
        
        // 数据行
        for (const QModelIndex &index : rows) {
            int row = index.row();
            output += m_model->item(row, 0)->text() + ",";
            output += m_model->item(row, 1)->text() + ",";
            output += m_model->item(row, 2)->text() + ",";
            output += m_model->item(row, 3)->text() + "\n";
        }
        
        QMessageBox::information(this, "导出结果", 
            QString("已导出 %1 行数据:\n\n%2").arg(rows.size()).arg(output));
    }
};

本节小结

QItemSelection - 选择范围的集合类

QItemSelectionRange - 表示矩形选择区域

范围操作 - 合并、遍历、检查包含关系

批量操作 - 全选、反选、批量编辑、导出

关键要点

  1. QItemSelection用于表示多个不连续的选择范围
  2. 可以通过merge()合并多个选择
  3. indexes()方法获取所有选中的索引
  4. 适合实现批量操作功能
  • QItemSelection的使用
  • 选择范围的操作
  • 实战:批量操作选中项

第7章总结

🎉 第7章 选择模型(QItemSelectionModel) 已全部完成!

本章涵盖了:

  • ✅ 7.1 选择模型基础(作用、关系、获取)
  • ✅ 7.2 选择操作(选择、清除、模式)
  • ✅ 7.3 选择相关信号(currentChanged、selectionChanged)
  • ✅ 7.4 QItemSelection(范围操作、批量处理)

核心知识点

  1. 选择模型独立于数据模型和视图
  2. 当前项和选中项是两个不同的概念
  3. 5种选择模式满足不同交互需求
  4. 信号机制用于跟踪选择变化
  5. QItemSelection简化批量操作

接下来可以继续学习第8章"Delegate(委托)详解"!


第8章 Delegate(委托)详解

8.1 委托的作用

委托(Delegate)是Model/View架构中负责数据展示和编辑的组件,它定义了数据如何显示以及如何编辑。

8.1.1 什么是委托

Model/View/Delegate三元架构

复制代码
┌─────────────┐
│   Model     │ ◄──────┐ 提供数据
│  (数据层)    │        │
└─────────────┘        │
                       │
                       │
┌─────────────┐        │
│    View     │ ───────┤ 显示数据
│  (视图层)    │        │
└─────────────┘        │
       │               │
       │ 使用           │
       ▼               │
┌─────────────┐        │
│  Delegate   │ ───────┘ 控制显示和编辑
│  (委托层)    │
└─────────────┘

委托的定义

委托是一个负责在视图中绘制单个项和提供编辑器的对象。每个视图项都通过委托来渲染和编辑。

cpp 复制代码
// 默认情况下,视图使用QStyledItemDelegate
QTableView *view = new QTableView;
// view内部已经有一个默认委托

// 获取当前委托
QAbstractItemDelegate *delegate = view->itemDelegate();

// 设置自定义委托
MyCustomDelegate *customDelegate = new MyCustomDelegate(view);
view->setItemDelegate(customDelegate);

8.1.2 委托的职责:渲染和编辑

双重职责

  1. 渲染(Rendering) - 如何显示数据
  2. 编辑(Editing) - 如何编辑数据

渲染职责

cpp 复制代码
// 委托控制每个项的视觉表现
class RenderDelegate : public QStyledItemDelegate {
protected:
    void paint(QPainter *painter, 
               const QStyleOptionViewItem &option,
               const QModelIndex &index) const override {
        // 自定义绘制逻辑
        // - 绘制背景
        // - 绘制图标
        // - 绘制文本
        // - 绘制装饰元素
    }
    
    QSize sizeHint(const QStyleOptionViewItem &option,
                   const QModelIndex &index) const override {
        // 返回项的建议大小
        return QSize(200, 40);
    }
};

编辑职责

cpp 复制代码
// 委托控制如何编辑数据
class EditorDelegate : public QStyledItemDelegate {
protected:
    // 1. 创建编辑器
    QWidget* createEditor(QWidget *parent,
                         const QStyleOptionViewItem &option,
                         const QModelIndex &index) const override {
        // 返回合适的编辑控件
        return new QLineEdit(parent);
    }
    
    // 2. 将模型数据设置到编辑器
    void setEditorData(QWidget *editor,
                       const QModelIndex &index) const override {
        QString value = index.data(Qt::EditRole).toString();
        QLineEdit *lineEdit = static_cast<QLineEdit*>(editor);
        lineEdit->setText(value);
    }
    
    // 3. 将编辑器数据写回模型
    void setModelData(QWidget *editor,
                      QAbstractItemModel *model,
                      const QModelIndex &index) const override {
        QLineEdit *lineEdit = static_cast<QLineEdit*>(editor);
        model->setData(index, lineEdit->text(), Qt::EditRole);
    }
    
    // 4. 更新编辑器的几何位置
    void updateEditorGeometry(QWidget *editor,
                             const QStyleOptionViewItem &option,
                             const QModelIndex &index) const override {
        editor->setGeometry(option.rect);
    }
};

使用场景对比

场景 使用默认委托 需要自定义委托
显示普通文本
显示图标+文本
显示进度条
显示星级评分
显示复杂布局
使用QLineEdit编辑
使用QComboBox选择
使用QDateEdit选择日期
使用QSlider编辑

8.1.3 QStyledItemDelegate vs QItemDelegate

两种委托基类

cpp 复制代码
// QStyledItemDelegate(推荐)
class MyDelegate : public QStyledItemDelegate {
    // 使用当前样式来绘制
    // 更现代,支持样式表
};

// QItemDelegate(过时)
class OldDelegate : public QItemDelegate {
    // 使用固定样式绘制
    // 向后兼容,不推荐新项目使用
};

主要区别

特性 QStyledItemDelegate QItemDelegate
推荐度 ✓ 推荐 ✗ 过时
样式表支持 ✓ 支持 ✗ 不支持
系统样式 ✓ 使用当前样式 ✗ 固定样式
性能 稍慢(样式计算) 稍快
使用场景 现代应用 遗留项目

QStyledItemDelegate的优势

cpp 复制代码
// 1. 自动适应系统样式
QStyledItemDelegate *delegate = new QStyledItemDelegate;
view->setItemDelegate(delegate);
// 在Windows/Mac/Linux上自动使用对应的原生样式

// 2. 支持样式表
view->setStyleSheet(R"(
    QTableView::item {
        padding: 5px;
        border: 1px solid #ccc;
    }
    QTableView::item:selected {
        background: #007ACC;
    }
)");
// QStyledItemDelegate会遵守这些样式

// 3. 更好的编辑器支持
// QStyledItemDelegate对各种编辑器的支持更完善

何时使用QItemDelegate

cpp 复制代码
// 只有以下情况才考虑使用QItemDelegate:
// 1. 维护遗留代码
// 2. 需要完全自定义绘制,不希望样式干扰
// 3. 性能敏感场景(极少见)

// 一般情况下,始终使用QStyledItemDelegate

8.1.4 何时需要自定义委托

默认委托已足够的场景

cpp 复制代码
// 1. 简单文本显示
model->setData(index, "Some Text");

// 2. 带图标的文本
QStandardItem *item = new QStandardItem(QIcon(":/icon.png"), "Text");

// 3. 基本的文本编辑
// 双击即可编辑,使用QLineEdit

// 这些场景使用默认的QStyledItemDelegate即可

需要自定义委托的场景

场景1:自定义渲染

cpp 复制代码
// 示例:显示进度条
class ProgressDelegate : public QStyledItemDelegate {
protected:
    void paint(QPainter *painter,
               const QStyleOptionViewItem &option,
               const QModelIndex &index) const override {
        int progress = index.data().toInt();
        
        // 绘制进度条
        QStyleOptionProgressBar progressBarOption;
        progressBarOption.rect = option.rect;
        progressBarOption.minimum = 0;
        progressBarOption.maximum = 100;
        progressBarOption.progress = progress;
        progressBarOption.text = QString::number(progress) + "%";
        progressBarOption.textVisible = true;
        
        QApplication::style()->drawControl(QStyle::CE_ProgressBar,
                                          &progressBarOption, painter);
    }
};

场景2:自定义编辑器

cpp 复制代码
// 示例:下拉选择框
class ComboBoxDelegate : public QStyledItemDelegate {
protected:
    QWidget* createEditor(QWidget *parent,
                         const QStyleOptionViewItem &option,
                         const QModelIndex &index) const override {
        QComboBox *comboBox = new QComboBox(parent);
        comboBox->addItems({"选项1", "选项2", "选项3"});
        return comboBox;
    }
    
    void setEditorData(QWidget *editor,
                       const QModelIndex &index) const override {
        QComboBox *comboBox = static_cast<QComboBox*>(editor);
        QString value = index.data(Qt::EditRole).toString();
        comboBox->setCurrentText(value);
    }
    
    void setModelData(QWidget *editor,
                      QAbstractItemModel *model,
                      const QModelIndex &index) const override {
        QComboBox *comboBox = static_cast<QComboBox*>(editor);
        model->setData(index, comboBox->currentText());
    }
};

场景3:复杂的复合显示

cpp 复制代码
// 示例:联系人卡片(头像+姓名+电话+邮箱)
class ContactDelegate : public QStyledItemDelegate {
protected:
    void paint(QPainter *painter,
               const QStyleOptionViewItem &option,
               const QModelIndex &index) const override {
        // 获取数据
        QString name = index.data(Qt::UserRole).toString();
        QString phone = index.data(Qt::UserRole + 1).toString();
        QString email = index.data(Qt::UserRole + 2).toString();
        QPixmap avatar = index.data(Qt::UserRole + 3).value<QPixmap>();
        
        // 绘制背景
        painter->fillRect(option.rect, option.palette.base());
        
        // 绘制头像
        painter->drawPixmap(option.rect.left() + 5, 
                          option.rect.top() + 5,
                          40, 40, avatar);
        
        // 绘制文本信息
        int textLeft = option.rect.left() + 55;
        painter->drawText(textLeft, option.rect.top() + 15, name);
        painter->drawText(textLeft, option.rect.top() + 30, phone);
        painter->drawText(textLeft, option.rect.top() + 45, email);
    }
    
    QSize sizeHint(const QStyleOptionViewItem &option,
                   const QModelIndex &index) const override {
        return QSize(300, 60);  // 固定高度60像素
    }
};

决策树:是否需要自定义委托

复制代码
需要显示什么?
├── 纯文本
│   └── 使用默认委托 ✓
│
├── 文本 + 图标
│   └── 使用默认委托 ✓
│
├── 特殊控件(进度条、星级、图表等)
│   └── 需要自定义渲染委托 ✓
│
└── 复合布局(多个元素组合)
    └── 需要自定义渲染委托 ✓

需要如何编辑?
├── 简单文本输入
│   └── 使用默认委托 ✓
│
├── 选择(下拉框、单选按钮等)
│   └── 需要自定义编辑委托 ✓
│
├── 特殊输入(日期、颜色、滑块等)
│   └── 需要自定义编辑委托 ✓
│
└── 复合编辑(多个控件组合)
    └── 需要自定义编辑委托 ✓

委托的设置范围

cpp 复制代码
// 1. 为整个视图设置委托
MyDelegate *delegate = new MyDelegate(view);
view->setItemDelegate(delegate);

// 2. 为特定列设置委托
ProgressDelegate *progressDelegate = new ProgressDelegate(view);
view->setItemDelegateForColumn(2, progressDelegate);  // 第2列使用进度条

// 3. 为特定行设置委托
ComboBoxDelegate *comboDelegate = new ComboBoxDelegate(view);
view->setItemDelegateForRow(5, comboDelegate);  // 第5行使用下拉框

本节小结

委托概念 - Model/View/Delegate三元架构的第三部分

双重职责 - 渲染(显示)和编辑

QStyledItemDelegate - 现代推荐的委托基类

自定义场景 - 特殊显示和编辑需求

关键要点

  1. 委托负责数据的显示和编辑方式
  2. 默认委托已能满足大部分简单需求
  3. QStyledItemDelegate优于QItemDelegate
  4. 可以为视图、列、行分别设置不同的委托
  5. 自定义委托通过重写paint()、createEditor()等方法实现
  • 什么是委托
  • 委托的职责:渲染和编辑
  • QStyledItemDelegate vs QItemDelegate
  • 何时需要自定义委托

8.2 委托的核心方法

QStyledItemDelegate提供了6个核心方法用于自定义渲染和编辑行为。

8.2.1 paint() - 自定义绘制

方法签名

cpp 复制代码
virtual void paint(QPainter *painter,
                  const QStyleOptionViewItem &option,
                  const QModelIndex &index) const;

参数说明

  • painter - 用于绘制的QPainter对象
  • option - 包含样式选项和矩形区域
  • index - 要绘制的项的模型索引

option常用属性

cpp 复制代码
void paint(QPainter *painter,
          const QStyleOptionViewItem &option,
          const QModelIndex &index) const override {
    // option.rect - 项的绘制区域
    QRect rect = option.rect;
    
    // option.state - 项的状态标志
    bool isSelected = option.state & QStyle::State_Selected;
    bool hasHover = option.state & QStyle::State_MouseOver;
    bool hasFocus = option.state & QStyle::State_HasFocus;
    bool isEnabled = option.state & QStyle::State_Enabled;
    
    // option.palette - 调色板
    QBrush background = option.palette.base();
    QBrush highlight = option.palette.highlight();
    
    // option.font - 字体
    QFont font = option.font;
    
    // option.decorationSize - 装饰(图标)大小
    QSize iconSize = option.decorationSize;
}

基本绘制示例

cpp 复制代码
class SimpleDelegate : public QStyledItemDelegate {
protected:
    void paint(QPainter *painter,
               const QStyleOptionViewItem &option,
               const QModelIndex &index) const override {
        // 1. 保存painter状态
        painter->save();
        
        // 2. 绘制背景
        if (option.state & QStyle::State_Selected) {
            painter->fillRect(option.rect, option.palette.highlight());
        } else {
            painter->fillRect(option.rect, option.palette.base());
        }
        
        // 3. 绘制文本
        QString text = index.data(Qt::DisplayRole).toString();
        QRect textRect = option.rect.adjusted(5, 0, -5, 0);  // 留5px边距
        
        if (option.state & QStyle::State_Selected) {
            painter->setPen(option.palette.highlightedText().color());
        } else {
            painter->setPen(option.palette.text().color());
        }
        
        painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, text);
        
        // 4. 恢复painter状态
        painter->restore();
    }
};

高级绘制技巧

cpp 复制代码
// 技巧1:使用样式绘制
void paint(QPainter *painter,
          const QStyleOptionViewItem &option,
          const QModelIndex &index) const override {
    QStyleOptionViewItem opt = option;
    initStyleOption(&opt, index);  // 初始化样式选项
    
    // 使用系统样式绘制
    QStyle *style = opt.widget ? opt.widget->style() : QApplication::style();
    style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget);
}

// 技巧2:抗锯齿绘制
void paint(QPainter *painter, ...) const override {
    painter->setRenderHint(QPainter::Antialiasing, true);
    painter->setRenderHint(QPainter::TextAntialiasing, true);
    
    // 绘制圆角矩形等需要抗锯齿
    painter->drawRoundedRect(rect, 5, 5);
}

// 技巧3:裁剪区域
void paint(QPainter *painter, ...) const override {
    painter->setClipRect(option.rect);  // 确保不绘制到区域外
    // 绘制代码...
}

8.2.2 sizeHint() - 项的大小提示

方法签名

cpp 复制代码
virtual QSize sizeHint(const QStyleOptionViewItem &option,
                      const QModelIndex &index) const;

返回值:该项的建议大小

基本示例

cpp 复制代码
class FixedSizeDelegate : public QStyledItemDelegate {
protected:
    QSize sizeHint(const QStyleOptionViewItem &option,
                   const QModelIndex &index) const override {
        // 返回固定大小
        return QSize(200, 40);
    }
};

class DynamicSizeDelegate : public QStyledItemDelegate {
protected:
    QSize sizeHint(const QStyleOptionViewItem &option,
                   const QModelIndex &index) const override {
        // 根据内容计算大小
        QString text = index.data().toString();
        
        QFontMetrics fm(option.font);
        int width = fm.horizontalAdvance(text) + 20;  // 加20px边距
        int height = fm.height() + 10;
        
        return QSize(width, height);
    }
};

结合数据的大小计算

cpp 复制代码
QSize sizeHint(const QStyleOptionViewItem &option,
               const QModelIndex &index) const override {
    // 获取数据
    QString text = index.data(Qt::DisplayRole).toString();
    QPixmap icon = index.data(Qt::DecorationRole).value<QPixmap>();
    
    // 计算文本大小
    QFontMetrics fm(option.font);
    QSize textSize = fm.size(Qt::TextSingleLine, text);
    
    // 计算总大小
    int width = textSize.width() + icon.width() + 30;  // 边距
    int height = qMax(textSize.height(), icon.height()) + 10;
    
    return QSize(width, height);
}

8.2.3 createEditor() - 创建编辑器

方法签名

cpp 复制代码
virtual QWidget* createEditor(QWidget *parent,
                              const QStyleOptionViewItem &option,
                              const QModelIndex &index) const;

参数说明

  • parent - 编辑器的父控件
  • option - 样式选项
  • index - 要编辑的项的索引

返回值:编辑器控件(QLineEdit、QComboBox等)

常用编辑器示例

cpp 复制代码
// 1. 文本编辑器
QWidget* createEditor(QWidget *parent, ...) const override {
    QLineEdit *editor = new QLineEdit(parent);
    editor->setFrame(false);
    return editor;
}

// 2. 下拉选择框
QWidget* createEditor(QWidget *parent, ...) const override {
    QComboBox *comboBox = new QComboBox(parent);
    comboBox->addItems({"选项A", "选项B", "选项C"});
    return comboBox;
}

// 3. 数字调节器
QWidget* createEditor(QWidget *parent, ...) const override {
    QSpinBox *spinBox = new QSpinBox(parent);
    spinBox->setRange(0, 100);
    spinBox->setSingleStep(1);
    return spinBox;
}

// 4. 日期选择器
QWidget* createEditor(QWidget *parent, ...) const override {
    QDateEdit *dateEdit = new QDateEdit(parent);
    dateEdit->setCalendarPopup(true);
    dateEdit->setDisplayFormat("yyyy-MM-dd");
    return dateEdit;
}

// 5. 滑块
QWidget* createEditor(QWidget *parent, ...) const override {
    QSlider *slider = new QSlider(Qt::Horizontal, parent);
    slider->setRange(0, 100);
    return slider;
}

根据列选择不同的编辑器

cpp 复制代码
QWidget* createEditor(QWidget *parent,
                     const QStyleOptionViewItem &option,
                     const QModelIndex &index) const override {
    int column = index.column();
    
    switch (column) {
    case 0:  // 第0列:文本
        return new QLineEdit(parent);
        
    case 1:  // 第1列:选择框
        {
            QComboBox *comboBox = new QComboBox(parent);
            comboBox->addItems({"待定", "进行中", "已完成"});
            return comboBox;
        }
        
    case 2:  // 第2列:数字
        {
            QSpinBox *spinBox = new QSpinBox(parent);
            spinBox->setRange(0, 100);
            return spinBox;
        }
        
    case 3:  // 第3列:日期
        {
            QDateEdit *dateEdit = new QDateEdit(parent);
            dateEdit->setCalendarPopup(true);
            return dateEdit;
        }
        
    default:
        return QStyledItemDelegate::createEditor(parent, option, index);
    }
}

8.2.4 setEditorData() - 设置编辑器数据

方法签名

cpp 复制代码
virtual void setEditorData(QWidget *editor,
                          const QModelIndex &index) const;

作用:将模型中的数据设置到编辑器控件中

基本示例

cpp 复制代码
void setEditorData(QWidget *editor, const QModelIndex &index) const override {
    // 1. QLineEdit
    QLineEdit *lineEdit = qobject_cast<QLineEdit*>(editor);
    if (lineEdit) {
        QString value = index.data(Qt::EditRole).toString();
        lineEdit->setText(value);
        return;
    }
    
    // 2. QComboBox
    QComboBox *comboBox = qobject_cast<QComboBox*>(editor);
    if (comboBox) {
        QString value = index.data(Qt::EditRole).toString();
        comboBox->setCurrentText(value);
        return;
    }
    
    // 3. QSpinBox
    QSpinBox *spinBox = qobject_cast<QSpinBox*>(editor);
    if (spinBox) {
        int value = index.data(Qt::EditRole).toInt();
        spinBox->setValue(value);
        return;
    }
    
    // 4. QDateEdit
    QDateEdit *dateEdit = qobject_cast<QDateEdit*>(editor);
    if (dateEdit) {
        QDate date = index.data(Qt::EditRole).toDate();
        dateEdit->setDate(date);
        return;
    }
    
    // 默认处理
    QStyledItemDelegate::setEditorData(editor, index);
}

8.2.5 setModelData() - 将编辑器数据写回模型

方法签名

cpp 复制代码
virtual void setModelData(QWidget *editor,
                         QAbstractItemModel *model,
                         const QModelIndex &index) const;

作用:将编辑器中的数据写回到模型

基本示例

cpp 复制代码
void setModelData(QWidget *editor,
                 QAbstractItemModel *model,
                 const QModelIndex &index) const override {
    // 1. QLineEdit
    QLineEdit *lineEdit = qobject_cast<QLineEdit*>(editor);
    if (lineEdit) {
        model->setData(index, lineEdit->text(), Qt::EditRole);
        return;
    }
    
    // 2. QComboBox
    QComboBox *comboBox = qobject_cast<QComboBox*>(editor);
    if (comboBox) {
        model->setData(index, comboBox->currentText(), Qt::EditRole);
        return;
    }
    
    // 3. QSpinBox
    QSpinBox *spinBox = qobject_cast<QSpinBox*>(editor);
    if (spinBox) {
        model->setData(index, spinBox->value(), Qt::EditRole);
        return;
    }
    
    // 4. QDateEdit
    QDateEdit *dateEdit = qobject_cast<QDateEdit*>(editor);
    if (dateEdit) {
        model->setData(index, dateEdit->date(), Qt::EditRole);
        return;
    }
    
    // 默认处理
    QStyledItemDelegate::setModelData(editor, model, index);
}

数据验证

cpp 复制代码
void setModelData(QWidget *editor,
                 QAbstractItemModel *model,
                 const QModelIndex &index) const override {
    QLineEdit *lineEdit = qobject_cast<QLineEdit*>(editor);
    if (lineEdit) {
        QString text = lineEdit->text();
        
        // 验证:不能为空
        if (text.trimmed().isEmpty()) {
            QMessageBox::warning(editor, "错误", "内容不能为空");
            return;
        }
        
        // 验证:长度限制
        if (text.length() > 50) {
            QMessageBox::warning(editor, "错误", "内容不能超过50个字符");
            return;
        }
        
        // 数据有效,写入模型
        model->setData(index, text, Qt::EditRole);
    }
}

8.2.6 updateEditorGeometry() - 更新编辑器位置

方法签名

cpp 复制代码
virtual void updateEditorGeometry(QWidget *editor,
                                 const QStyleOptionViewItem &option,
                                 const QModelIndex &index) const;

作用:设置编辑器的位置和大小

基本示例

cpp 复制代码
void updateEditorGeometry(QWidget *editor,
                         const QStyleOptionViewItem &option,
                         const QModelIndex &index) const override {
    // 最简单:使用项的完整区域
    editor->setGeometry(option.rect);
}

高级布局

cpp 复制代码
void updateEditorGeometry(QWidget *editor,
                         const QStyleOptionViewItem &option,
                         const QModelIndex &index) const override {
    QRect rect = option.rect;
    
    // 1. 留出边距
    rect = rect.adjusted(2, 2, -2, -2);
    
    // 2. 根据编辑器类型调整
    if (qobject_cast<QComboBox*>(editor)) {
        // 下拉框需要更多高度
        rect.setHeight(rect.height() + 10);
    }
    
    // 3. 设置几何
    editor->setGeometry(rect);
}

完整示例:自定义编辑委托

cpp 复制代码
class CustomEditDelegate : public QStyledItemDelegate {
public:
    explicit CustomEditDelegate(QObject *parent = nullptr)
        : QStyledItemDelegate(parent) {}
    
protected:
    QWidget* createEditor(QWidget *parent,
                         const QStyleOptionViewItem &option,
                         const QModelIndex &index) const override {
        int column = index.column();
        
        if (column == 1) {  // 状态列
            QComboBox *comboBox = new QComboBox(parent);
            comboBox->addItems({"待办", "进行中", "已完成"});
            return comboBox;
        } else if (column == 2) {  // 优先级列
            QSpinBox *spinBox = new QSpinBox(parent);
            spinBox->setRange(1, 5);
            return spinBox;
        }
        
        return QStyledItemDelegate::createEditor(parent, option, index);
    }
    
    void setEditorData(QWidget *editor,
                       const QModelIndex &index) const override {
        int column = index.column();
        
        if (column == 1) {
            QComboBox *comboBox = static_cast<QComboBox*>(editor);
            comboBox->setCurrentText(index.data().toString());
        } else if (column == 2) {
            QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
            spinBox->setValue(index.data().toInt());
        } else {
            QStyledItemDelegate::setEditorData(editor, index);
        }
    }
    
    void setModelData(QWidget *editor,
                     QAbstractItemModel *model,
                     const QModelIndex &index) const override {
        int column = index.column();
        
        if (column == 1) {
            QComboBox *comboBox = static_cast<QComboBox*>(editor);
            model->setData(index, comboBox->currentText());
        } else if (column == 2) {
            QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
            model->setData(index, spinBox->value());
        } else {
            QStyledItemDelegate::setModelData(editor, model, index);
        }
    }
    
    void updateEditorGeometry(QWidget *editor,
                             const QStyleOptionViewItem &option,
                             const QModelIndex &index) const override {
        editor->setGeometry(option.rect);
    }
};

本节小结

paint() - 自定义项的绘制

sizeHint() - 指定项的建议大小

createEditor() - 创建合适的编辑器控件

setEditorData() - 模型数据→编辑器

setModelData() - 编辑器数据→模型

updateEditorGeometry() - 设置编辑器位置和大小

关键要点

  1. paint()负责渲染,sizeHint()控制大小
  2. 编辑流程:createEditor → setEditorData → 用户编辑 → setModelData
  3. 使用qobject_cast安全地转换编辑器类型
  4. 可以在setModelData()中进行数据验证
  5. 为不同列设置不同编辑器可实现丰富的编辑体验
  • paint() - 自定义绘制
  • sizeHint() - 项的大小提示
  • createEditor() - 创建编辑器
  • setEditorData() - 设置编辑器数据
  • setModelData() - 将编辑器数据写回模型
  • updateEditorGeometry() - 更新编辑器位置

8.3 自定义渲染委托

自定义渲染委托可以让项显示复杂的视觉效果,如进度条、星级评分、图表等。

8.3.1 重写paint()方法

基本结构

cpp 复制代码
class CustomRenderDelegate : public QStyledItemDelegate {
protected:
    void paint(QPainter *painter,
               const QStyleOptionViewItem &option,
               const QModelIndex &index) const override {
        // 1. 保存painter状态
        painter->save();
        
        // 2. 设置渲染提示(抗锯齿等)
        painter->setRenderHint(QPainter::Antialiasing);
        
        // 3. 绘制背景
        drawBackground(painter, option);
        
        // 4. 绘制自定义内容
        drawCustomContent(painter, option, index);
        
        // 5. 绘制焦点框(可选)
        if (option.state & QStyle::State_HasFocus) {
            drawFocusRect(painter, option);
        }
        
        // 6. 恢复painter状态
        painter->restore();
    }
    
private:
    void drawBackground(QPainter *painter, 
                       const QStyleOptionViewItem &option) const {
        if (option.state & QStyle::State_Selected) {
            painter->fillRect(option.rect, option.palette.highlight());
        } else {
            painter->fillRect(option.rect, option.palette.base());
        }
    }
    
    void drawCustomContent(QPainter *painter,
                          const QStyleOptionViewItem &option,
                          const QModelIndex &index) const {
        // 子类实现
    }
    
    void drawFocusRect(QPainter *painter,
                      const QStyleOptionViewItem &option) const {
        QStyleOptionFocusRect focusOption;
        focusOption.rect = option.rect;
        focusOption.state = option.state;
        focusOption.backgroundColor = option.palette.background().color();
        
        QStyle *style = option.widget ? option.widget->style() 
                                       : QApplication::style();
        style->drawPrimitive(QStyle::PE_FrameFocusRect, 
                           &focusOption, painter);
    }
};

8.3.2 QPainter的使用

常用绘制方法

cpp 复制代码
void paint(QPainter *painter, ...) const override {
    painter->save();
    
    // 1. 绘制矩形
    painter->drawRect(QRect(10, 10, 100, 50));
    painter->fillRect(QRect(10, 10, 100, 50), Qt::blue);
    
    // 2. 绘制圆角矩形
    painter->drawRoundedRect(QRect(10, 10, 100, 50), 5, 5);
    
    // 3. 绘制椭圆/圆
    painter->drawEllipse(QRect(10, 10, 80, 80));
    
    // 4. 绘制文本
    painter->drawText(QRect(10, 10, 200, 30), 
                     Qt::AlignCenter, "Hello");
    
    // 5. 绘制图片
    QPixmap pixmap(":/icon.png");
    painter->drawPixmap(10, 10, pixmap);
    
    // 6. 绘制线条
    painter->drawLine(QPoint(0, 0), QPoint(100, 100));
    
    // 7. 绘制路径
    QPainterPath path;
    path.moveTo(10, 10);
    path.lineTo(100, 10);
    path.lineTo(100, 100);
    painter->drawPath(path);
    
    painter->restore();
}

设置画笔和画刷

cpp 复制代码
// 画笔(轮廓)
QPen pen(Qt::red);
pen.setWidth(2);
pen.setStyle(Qt::DashLine);
painter->setPen(pen);

// 画刷(填充)
QBrush brush(Qt::blue);
brush.setStyle(Qt::Dense4Pattern);
painter->setBrush(brush);

// 渐变画刷
QLinearGradient gradient(0, 0, 100, 100);
gradient.setColorAt(0, Qt::white);
gradient.setColorAt(1, Qt::blue);
painter->setBrush(QBrush(gradient));

8.3.3 绘制文本、图标、进度条等

多行文本绘制

cpp 复制代码
void paint(QPainter *painter,
          const QStyleOptionViewItem &option,
          const QModelIndex &index) const override {
    painter->save();
    
    QString text = index.data(Qt::DisplayRole).toString();
    QRect textRect = option.rect.adjusted(5, 5, -5, -5);
    
    // 设置字体
    QFont font = option.font;
    font.setPointSize(10);
    painter->setFont(font);
    
    // 设置颜色
    painter->setPen(option.palette.text().color());
    
    // 绘制多行文本(自动换行)
    painter->drawText(textRect, 
                     Qt::AlignLeft | Qt::AlignVCenter | Qt::TextWordWrap, 
                     text);
    
    painter->restore();
}

图标+文本组合

cpp 复制代码
void paint(QPainter *painter,
          const QStyleOptionViewItem &option,
          const QModelIndex &index) const override {
    painter->save();
    
    // 获取数据
    QIcon icon = index.data(Qt::DecorationRole).value<QIcon>();
    QString text = index.data(Qt::DisplayRole).toString();
    
    // 绘制图标
    QRect iconRect = QRect(option.rect.left() + 5, 
                          option.rect.top() + 5,
                          32, 32);
    icon.paint(painter, iconRect);
    
    // 绘制文本
    QRect textRect = QRect(option.rect.left() + 45,
                          option.rect.top(),
                          option.rect.width() - 50,
                          option.rect.height());
    painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, text);
    
    painter->restore();
}

8.3.4 实战:星级评分显示委托

完整实现一个5星评分显示委托。

cpp 复制代码
class StarRatingDelegate : public QStyledItemDelegate {
    Q_OBJECT
    
public:
    explicit StarRatingDelegate(QObject *parent = nullptr)
        : QStyledItemDelegate(parent) {}
    
protected:
    void paint(QPainter *painter,
               const QStyleOptionViewItem &option,
               const QModelIndex &index) const override {
        painter->save();
        painter->setRenderHint(QPainter::Antialiasing);
        
        // 绘制背景
        if (option.state & QStyle::State_Selected) {
            painter->fillRect(option.rect, option.palette.highlight());
        }
        
        // 获取评分值(0-5)
        double rating = index.data(Qt::DisplayRole).toDouble();
        
        // 绘制星星
        int starSize = 16;
        int spacing = 2;
        int startX = option.rect.left() + 5;
        int startY = option.rect.top() + (option.rect.height() - starSize) / 2;
        
        for (int i = 0; i < 5; ++i) {
            QRect starRect(startX + i * (starSize + spacing), 
                          startY, starSize, starSize);
            
            // 计算这颗星的填充程度
            double fill = qBound(0.0, rating - i, 1.0);
            
            drawStar(painter, starRect, fill, 
                    option.state & QStyle::State_Selected);
        }
        
        // 绘制评分数字
        QString ratingText = QString::number(rating, 'f', 1);
        QRect textRect(startX + 5 * (starSize + spacing) + 10,
                      option.rect.top(),
                      option.rect.width(),
                      option.rect.height());
        
        if (option.state & QStyle::State_Selected) {
            painter->setPen(option.palette.highlightedText().color());
        } else {
            painter->setPen(option.palette.text().color());
        }
        
        painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, 
                         ratingText);
        
        painter->restore();
    }
    
    QSize sizeHint(const QStyleOptionViewItem &option,
                   const QModelIndex &index) const override {
        return QSize(150, 30);
    }
    
private:
    void drawStar(QPainter *painter, const QRect &rect, 
                 double fill, bool selected) const {
        // 创建星形路径
        QPainterPath starPath = createStarPath(rect);
        
        // 绘制填充部分
        if (fill > 0) {
            painter->save();
            
            // 设置裁剪区域(只绘制填充部分)
            QRect fillRect = rect;
            fillRect.setWidth(rect.width() * fill);
            painter->setClipRect(fillRect);
            
            // 填充颜色
            painter->setPen(Qt::NoPen);
            painter->setBrush(selected ? Qt::white : QColor(255, 180, 0));
            painter->drawPath(starPath);
            
            painter->restore();
        }
        
        // 绘制未填充部分
        if (fill < 1.0) {
            painter->save();
            
            QRect emptyRect = rect;
            emptyRect.setLeft(rect.left() + rect.width() * fill);
            painter->setClipRect(emptyRect);
            
            painter->setPen(QPen(selected ? Qt::white : Qt::gray, 1));
            painter->setBrush(Qt::NoBrush);
            painter->drawPath(starPath);
            
            painter->restore();
        }
    }
    
    QPainterPath createStarPath(const QRect &rect) const {
        // 创建五角星路径
        QPointF center(rect.center());
        double outerRadius = rect.width() / 2.0;
        double innerRadius = outerRadius * 0.4;
        
        QPainterPath path;
        
        for (int i = 0; i < 10; ++i) {
            double angle = (i * 36 - 90) * M_PI / 180.0;
            double radius = (i % 2 == 0) ? outerRadius : innerRadius;
            
            QPointF point(center.x() + radius * cos(angle),
                         center.y() + radius * sin(angle));
            
            if (i == 0)
                path.moveTo(point);
            else
                path.lineTo(point);
        }
        
        path.closeSubpath();
        return path;
    }
};

8.3.5 实战:进度条委托

实现一个美观的进度条显示委托。

cpp 复制代码
class ProgressBarDelegate : public QStyledItemDelegate {
    Q_OBJECT
    
public:
    explicit ProgressBarDelegate(QObject *parent = nullptr)
        : QStyledItemDelegate(parent) {}
    
protected:
    void paint(QPainter *painter,
               const QStyleOptionViewItem &option,
               const QModelIndex &index) const override {
        // 获取进度值(0-100)
        int progress = index.data(Qt::DisplayRole).toInt();
        
        // 使用Qt样式绘制进度条
        QStyleOptionProgressBar progressBarOption;
        progressBarOption.rect = option.rect.adjusted(2, 2, -2, -2);
        progressBarOption.minimum = 0;
        progressBarOption.maximum = 100;
        progressBarOption.progress = progress;
        progressBarOption.text = QString::number(progress) + "%";
        progressBarOption.textVisible = true;
        
        // 设置状态
        if (option.state & QStyle::State_Selected) {
            progressBarOption.state = QStyle::State_Enabled | QStyle::State_Selected;
        } else {
            progressBarOption.state = QStyle::State_Enabled;
        }
        
        // 绘制
        QApplication::style()->drawControl(QStyle::CE_ProgressBar,
                                          &progressBarOption, painter);
    }
    
    QSize sizeHint(const QStyleOptionViewItem &option,
                   const QModelIndex &index) const override {
        return QSize(200, 25);
    }
};

// 自定义样式的进度条
class CustomProgressBarDelegate : public QStyledItemDelegate {
public:
    explicit CustomProgressBarDelegate(QObject *parent = nullptr)
        : QStyledItemDelegate(parent) {}
    
protected:
    void paint(QPainter *painter,
               const QStyleOptionViewItem &option,
               const QModelIndex &index) const override {
        painter->save();
        painter->setRenderHint(QPainter::Antialiasing);
        
        int progress = index.data(Qt::DisplayRole).toInt();
        
        // 绘制背景
        QRect bgRect = option.rect.adjusted(5, 5, -5, -5);
        painter->setPen(Qt::NoPen);
        painter->setBrush(QColor(230, 230, 230));
        painter->drawRoundedRect(bgRect, 3, 3);
        
        // 绘制进度
        if (progress > 0) {
            QRect progressRect = bgRect;
            progressRect.setWidth(bgRect.width() * progress / 100);
            
            // 渐变色
            QLinearGradient gradient(progressRect.topLeft(), 
                                    progressRect.bottomLeft());
            
            // 根据进度值改变颜色
            if (progress < 30) {
                gradient.setColorAt(0, QColor(255, 100, 100));
                gradient.setColorAt(1, QColor(200, 50, 50));
            } else if (progress < 70) {
                gradient.setColorAt(0, QColor(255, 200, 100));
                gradient.setColorAt(1, QColor(200, 150, 50));
            } else {
                gradient.setColorAt(0, QColor(100, 255, 100));
                gradient.setColorAt(1, QColor(50, 200, 50));
            }
            
            painter->setBrush(gradient);
            painter->drawRoundedRect(progressRect, 3, 3);
        }
        
        // 绘制文本
        painter->setPen(Qt::black);
        QFont font = option.font;
        font.setBold(true);
        painter->setFont(font);
        
        QString text = QString::number(progress) + "%";
        painter->drawText(bgRect, Qt::AlignCenter, text);
        
        painter->restore();
    }
};

8.3.6 实战:自定义背景和边框

实现带圆角、阴影、边框的美观委托。

cpp 复制代码
class StyledDelegate : public QStyledItemDelegate {
public:
    explicit StyledDelegate(QObject *parent = nullptr)
        : QStyledItemDelegate(parent) {}
    
protected:
    void paint(QPainter *painter,
               const QStyleOptionViewItem &option,
               const QModelIndex &index) const override {
        painter->save();
        painter->setRenderHint(QPainter::Antialiasing);
        
        QRect rect = option.rect.adjusted(3, 3, -3, -3);
        
        // 绘制阴影
        if (option.state & QStyle::State_MouseOver) {
            QRect shadowRect = rect.adjusted(2, 2, 2, 2);
            painter->setPen(Qt::NoPen);
            painter->setBrush(QColor(0, 0, 0, 30));
            painter->drawRoundedRect(shadowRect, 5, 5);
        }
        
        // 绘制背景
        if (option.state & QStyle::State_Selected) {
            // 选中状态:渐变背景
            QLinearGradient gradient(rect.topLeft(), rect.bottomLeft());
            gradient.setColorAt(0, QColor(100, 150, 255));
            gradient.setColorAt(1, QColor(80, 120, 200));
            painter->setBrush(gradient);
        } else if (option.state & QStyle::State_MouseOver) {
            // 悬停状态:浅色背景
            painter->setBrush(QColor(240, 248, 255));
        } else {
            // 正常状态:白色背景
            painter->setBrush(Qt::white);
        }
        
        // 绘制圆角矩形
        painter->setPen(QPen(QColor(200, 200, 200), 1));
        painter->drawRoundedRect(rect, 5, 5);
        
        // 绘制内容
        QString text = index.data(Qt::DisplayRole).toString();
        QRect textRect = rect.adjusted(10, 5, -10, -5);
        
        if (option.state & QStyle::State_Selected) {
            painter->setPen(Qt::white);
        } else {
            painter->setPen(Qt::black);
        }
        
        painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, text);
        
        painter->restore();
    }
    
    QSize sizeHint(const QStyleOptionViewItem &option,
                   const QModelIndex &index) const override {
        return QSize(200, 40);
    }
};

使用示例

cpp 复制代码
#include <QApplication>
#include <QTableView>
#include <QStandardItemModel>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    // 创建模型
    QStandardItemModel model(5, 3);
    model.setHorizontalHeaderLabels({"名称", "评分", "进度"});
    
    for (int row = 0; row < 5; ++row) {
        model.setItem(row, 0, new QStandardItem(QString("项目%1").arg(row + 1)));
        model.setItem(row, 1, new QStandardItem(QString::number(3.5 + row * 0.3)));
        model.setItem(row, 2, new QStandardItem(QString::number((row + 1) * 20)));
    }
    
    // 创建视图
    QTableView view;
    view.setModel(&model);
    
    // 设置委托
    view.setItemDelegateForColumn(1, new StarRatingDelegate(&view));
    view.setItemDelegateForColumn(2, new ProgressBarDelegate(&view));
    
    // 调整列宽
    view.setColumnWidth(0, 150);
    view.setColumnWidth(1, 150);
    view.setColumnWidth(2, 200);
    
    view.setWindowTitle("自定义渲染委托示例");
    view.resize(600, 300);
    view.show();
    
    return app.exec();
}

本节小结

paint()方法 - 自定义绘制的核心

QPainter使用 - 绘制各种图形和文本

星级评分 - 复杂的自定义渲染示例

进度条 - 使用样式和自定义两种方式

美化效果 - 圆角、阴影、渐变等视觉效果

关键要点

  1. 使用painter->save()和restore()保护状态
  2. 启用抗锯齿以获得更好的视觉效果
  3. 使用QPainterPath绘制复杂形状
  4. 合理使用QStyle绘制系统原生控件
  5. 根据状态(选中、悬停)改变显示效果
  • 重写paint()方法
  • QPainter的使用
  • 绘制文本、图标、进度条等
  • 实战:星级评分显示委托
  • 实战:进度条委托
  • 实战:自定义背景和边框

8.4 自定义编辑委托

自定义编辑委托允许为不同的数据类型提供合适的编辑器,提升用户体验。

8.4.1 为不同列提供不同编辑器

多列编辑器示例

cpp 复制代码
class MultiColumnDelegate : public QStyledItemDelegate {
public:
    explicit MultiColumnDelegate(QObject *parent = nullptr)
        : QStyledItemDelegate(parent) {}
    
protected:
    QWidget* createEditor(QWidget *parent,
                         const QStyleOptionViewItem &option,
                         const QModelIndex &index) const override {
        int column = index.column();
        
        switch (column) {
        case 0:  // 名称 - 文本输入
            return createTextEditor(parent);
        case 1:  // 状态 - 下拉选择
            return createStatusEditor(parent);
        case 2:  // 优先级 - 数字调节
            return createPriorityEditor(parent);
        case 3:  // 日期 - 日期选择
            return createDateEditor(parent);
        case 4:  // 进度 - 滑块
            return createProgressEditor(parent);
        default:
            return QStyledItemDelegate::createEditor(parent, option, index);
        }
    }
    
private:
    QWidget* createTextEditor(QWidget *parent) const {
        QLineEdit *editor = new QLineEdit(parent);
        editor->setPlaceholderText("输入名称...");
        return editor;
    }
    
    QWidget* createStatusEditor(QWidget *parent) const {
        QComboBox *comboBox = new QComboBox(parent);
        comboBox->addItems({"待办", "进行中", "已完成", "暂停"});
        return comboBox;
    }
    
    QWidget* createPriorityEditor(QWidget *parent) const {
        QSpinBox *spinBox = new QSpinBox(parent);
        spinBox->setRange(1, 5);
        spinBox->setSuffix(" 级");
        return spinBox;
    }
    
    QWidget* createDateEditor(QWidget *parent) const {
        QDateEdit *dateEdit = new QDateEdit(parent);
        dateEdit->setCalendarPopup(true);
        dateEdit->setDisplayFormat("yyyy-MM-dd");
        dateEdit->setDate(QDate::currentDate());
        return dateEdit;
    }
    
    QWidget* createProgressEditor(QWidget *parent) const {
        QSlider *slider = new QSlider(Qt::Horizontal, parent);
        slider->setRange(0, 100);
        slider->setTickPosition(QSlider::TicksBelow);
        slider->setTickInterval(10);
        return slider;
    }
};

8.4.2 常用编辑器:QLineEdit、QComboBox、QSpinBox、QDateEdit

完整的编辑器实现模板

cpp 复制代码
class EditorTemplateDelegate : public QStyledItemDelegate {
protected:
    // 1. QLineEdit编辑器
    QWidget* createLineEditEditor(QWidget *parent) const {
        QLineEdit *editor = new QLineEdit(parent);
        
        // 设置输入验证
        QRegularExpressionValidator *validator = 
            new QRegularExpressionValidator(QRegularExpression("[A-Za-z0-9]+"), editor);
        editor->setValidator(validator);
        
        // 设置最大长度
        editor->setMaxLength(50);
        
        return editor;
    }
    
    void setLineEditData(QLineEdit *editor, const QModelIndex &index) const {
        QString text = index.data(Qt::EditRole).toString();
        editor->setText(text);
        editor->selectAll();  // 选中所有文本,方便编辑
    }
    
    void getLineEditData(QLineEdit *editor, QAbstractItemModel *model,
                        const QModelIndex &index) const {
        model->setData(index, editor->text());
    }
    
    // 2. QComboBox编辑器
    QWidget* createComboBoxEditor(QWidget *parent, 
                                  const QStringList &items) const {
        QComboBox *comboBox = new QComboBox(parent);
        comboBox->addItems(items);
        
        // 设置为可编辑(可选)
        // comboBox->setEditable(true);
        
        return comboBox;
    }
    
    void setComboBoxData(QComboBox *comboBox, const QModelIndex &index) const {
        QString value = index.data(Qt::EditRole).toString();
        comboBox->setCurrentText(value);
    }
    
    void getComboBoxData(QComboBox *comboBox, QAbstractItemModel *model,
                        const QModelIndex &index) const {
        model->setData(index, comboBox->currentText());
    }
    
    // 3. QSpinBox编辑器
    QWidget* createSpinBoxEditor(QWidget *parent, int min, int max) const {
        QSpinBox *spinBox = new QSpinBox(parent);
        spinBox->setRange(min, max);
        spinBox->setSingleStep(1);
        
        // 设置前缀和后缀
        // spinBox->setPrefix("$ ");
        // spinBox->setSuffix(" 元");
        
        return spinBox;
    }
    
    void setSpinBoxData(QSpinBox *spinBox, const QModelIndex &index) const {
        int value = index.data(Qt::EditRole).toInt();
        spinBox->setValue(value);
    }
    
    void getSpinBoxData(QSpinBox *spinBox, QAbstractItemModel *model,
                       const QModelIndex &index) const {
        spinBox->interpretText();  // 确保获取最新值
        model->setData(index, spinBox->value());
    }
    
    // 4. QDateEdit编辑器
    QWidget* createDateEditEditor(QWidget *parent) const {
        QDateEdit *dateEdit = new QDateEdit(parent);
        dateEdit->setCalendarPopup(true);
        dateEdit->setDisplayFormat("yyyy-MM-dd");
        
        // 设置日期范围
        dateEdit->setDateRange(QDate(2000, 1, 1), QDate(2099, 12, 31));
        
        return dateEdit;
    }
    
    void setDateEditData(QDateEdit *dateEdit, const QModelIndex &index) const {
        QDate date = index.data(Qt::EditRole).toDate();
        if (date.isValid()) {
            dateEdit->setDate(date);
        } else {
            dateEdit->setDate(QDate::currentDate());
        }
    }
    
    void getDateEditData(QDateEdit *dateEdit, QAbstractItemModel *model,
                        const QModelIndex &index) const {
        model->setData(index, dateEdit->date());
    }
};

8.4.3 实战:下拉选择框委托

完整实现一个状态选择委托。

cpp 复制代码
class StatusComboBoxDelegate : public QStyledItemDelegate {
    Q_OBJECT
    
public:
    explicit StatusComboBoxDelegate(QObject *parent = nullptr)
        : QStyledItemDelegate(parent) {
        // 定义状态选项
        m_statusList = {"待办", "进行中", "已完成", "暂停", "取消"};
    }
    
protected:
    QWidget* createEditor(QWidget *parent,
                         const QStyleOptionViewItem &option,
                         const QModelIndex &index) const override {
        QComboBox *comboBox = new QComboBox(parent);
        comboBox->addItems(m_statusList);
        
        // 连接信号,实现即时提交
        connect(comboBox, QOverload<int>::of(&QComboBox::currentIndexChanged),
                this, [=]() {
            // 提交数据并关闭编辑器
            const_cast<StatusComboBoxDelegate*>(this)->commitData(comboBox);
            const_cast<StatusComboBoxDelegate*>(this)->closeEditor(comboBox);
        });
        
        return comboBox;
    }
    
    void setEditorData(QWidget *editor, const QModelIndex &index) const override {
        QComboBox *comboBox = static_cast<QComboBox*>(editor);
        QString value = index.data(Qt::EditRole).toString();
        
        int idx = comboBox->findText(value);
        if (idx >= 0) {
            comboBox->setCurrentIndex(idx);
        }
    }
    
    void setModelData(QWidget *editor,
                     QAbstractItemModel *model,
                     const QModelIndex &index) const override {
        QComboBox *comboBox = static_cast<QComboBox*>(editor);
        model->setData(index, comboBox->currentText());
    }
    
    void updateEditorGeometry(QWidget *editor,
                             const QStyleOptionViewItem &option,
                             const QModelIndex &index) const override {
        editor->setGeometry(option.rect);
    }
    
    // 自定义渲染,显示不同颜色
    void paint(QPainter *painter,
               const QStyleOptionViewItem &option,
               const QModelIndex &index) const override {
        QString status = index.data(Qt::DisplayRole).toString();
        
        painter->save();
        
        // 绘制背景
        if (option.state & QStyle::State_Selected) {
            painter->fillRect(option.rect, option.palette.highlight());
        } else {
            painter->fillRect(option.rect, option.palette.base());
        }
        
        // 根据状态设置颜色
        QColor statusColor = getStatusColor(status);
        
        // 绘制状态指示器
        QRect indicatorRect(option.rect.left() + 5,
                           option.rect.top() + (option.rect.height() - 10) / 2,
                           10, 10);
        painter->setBrush(statusColor);
        painter->setPen(Qt::NoPen);
        painter->setRenderHint(QPainter::Antialiasing);
        painter->drawEllipse(indicatorRect);
        
        // 绘制文本
        QRect textRect = option.rect.adjusted(25, 0, -5, 0);
        if (option.state & QStyle::State_Selected) {
            painter->setPen(option.palette.highlightedText().color());
        } else {
            painter->setPen(option.palette.text().color());
        }
        painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, status);
        
        painter->restore();
    }
    
private:
    QStringList m_statusList;
    
    QColor getStatusColor(const QString &status) const {
        if (status == "待办") return QColor(150, 150, 150);
        if (status == "进行中") return QColor(100, 150, 255);
        if (status == "已完成") return QColor(100, 200, 100);
        if (status == "暂停") return QColor(255, 180, 100);
        if (status == "取消") return QColor(255, 100, 100);
        return Qt::gray;
    }
};

8.4.4 实战:日期选择器委托
cpp 复制代码
class DatePickerDelegate : public QStyledItemDelegate {
    Q_OBJECT
    
public:
    explicit DatePickerDelegate(QObject *parent = nullptr)
        : QStyledItemDelegate(parent) {}
    
protected:
    QWidget* createEditor(QWidget *parent,
                         const QStyleOptionViewItem &option,
                         const QModelIndex &index) const override {
        QDateEdit *dateEdit = new QDateEdit(parent);
        dateEdit->setCalendarPopup(true);
        dateEdit->setDisplayFormat("yyyy-MM-dd");
        
        // 设置日期范围(最近10年)
        dateEdit->setDateRange(QDate::currentDate().addYears(-10),
                              QDate::currentDate().addYears(10));
        
        return dateEdit;
    }
    
    void setEditorData(QWidget *editor, const QModelIndex &index) const override {
        QDateEdit *dateEdit = static_cast<QDateEdit*>(editor);
        QDate date = index.data(Qt::EditRole).toDate();
        
        if (date.isValid()) {
            dateEdit->setDate(date);
        } else {
            dateEdit->setDate(QDate::currentDate());
        }
    }
    
    void setModelData(QWidget *editor,
                     QAbstractItemModel *model,
                     const QModelIndex &index) const override {
        QDateEdit *dateEdit = static_cast<QDateEdit*>(editor);
        model->setData(index, dateEdit->date());
    }
    
    void updateEditorGeometry(QWidget *editor,
                             const QStyleOptionViewItem &option,
                             const QModelIndex &index) const override {
        editor->setGeometry(option.rect);
    }
    
    // 自定义显示格式
    void paint(QPainter *painter,
               const QStyleOptionViewItem &option,
               const QModelIndex &index) const override {
        QDate date = index.data(Qt::DisplayRole).toDate();
        
        painter->save();
        
        // 绘制背景
        if (option.state & QStyle::State_Selected) {
            painter->fillRect(option.rect, option.palette.highlight());
        }
        
        QString dateText;
        QColor textColor;
        
        if (!date.isValid()) {
            dateText = "未设置";
            textColor = Qt::gray;
        } else {
            dateText = date.toString("yyyy-MM-dd");
            
            // 根据日期远近设置颜色
            int daysTo = QDate::currentDate().daysTo(date);
            if (daysTo < 0) {
                textColor = Qt::red;  // 已过期
            } else if (daysTo <= 7) {
                textColor = QColor(255, 140, 0);  // 即将到期
            } else {
                textColor = option.palette.text().color();
            }
        }
        
        if (option.state & QStyle::State_Selected) {
            painter->setPen(option.palette.highlightedText().color());
        } else {
            painter->setPen(textColor);
        }
        
        QRect textRect = option.rect.adjusted(5, 0, -5, 0);
        painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, dateText);
        
        painter->restore();
    }
};

8.4.5 实战:滑块编辑器委托
cpp 复制代码
class SliderDelegate : public QStyledItemDelegate {
    Q_OBJECT
    
public:
    explicit SliderDelegate(int min = 0, int max = 100, 
                           QObject *parent = nullptr)
        : QStyledItemDelegate(parent), m_min(min), m_max(max) {}
    
protected:
    QWidget* createEditor(QWidget *parent,
                         const QStyleOptionViewItem &option,
                         const QModelIndex &index) const override {
        QSlider *slider = new QSlider(Qt::Horizontal, parent);
        slider->setRange(m_min, m_max);
        slider->setTickPosition(QSlider::TicksBelow);
        slider->setTickInterval((m_max - m_min) / 10);
        
        return slider;
    }
    
    void setEditorData(QWidget *editor, const QModelIndex &index) const override {
        QSlider *slider = static_cast<QSlider*>(editor);
        int value = index.data(Qt::EditRole).toInt();
        slider->setValue(value);
    }
    
    void setModelData(QWidget *editor,
                     QAbstractItemModel *model,
                     const QModelIndex &index) const override {
        QSlider *slider = static_cast<QSlider*>(editor);
        model->setData(index, slider->value());
    }
    
    void updateEditorGeometry(QWidget *editor,
                             const QStyleOptionViewItem &option,
                             const QModelIndex &index) const override {
        editor->setGeometry(option.rect);
    }
    
private:
    int m_min;
    int m_max;
};

本节小结

多列编辑器 - 为不同列提供不同的编辑控件

常用编辑器 - QLineEdit、QComboBox、QSpinBox、QDateEdit的完整实现

状态选择框 - 带颜色指示的下拉选择

日期选择器 - 带日期提醒的日期编辑

滑块编辑器 - 直观的数值调节

关键要点

  1. 根据列号选择合适的编辑器类型
  2. 为编辑器设置合理的范围和格式
  3. 可以在paint()中自定义显示效果
  4. 使用信号实现即时提交和关闭编辑器
  5. 添加数据验证确保输入合法
  • 为不同列提供不同编辑器
  • 常用编辑器:QLineEdit、QComboBox、QSpinBox、QDateEdit
  • 实战:下拉选择框委托
  • 实战:日期选择器委托
  • 实战:滑块编辑器委托

8.5 复杂委托实战

本节通过两个复杂的实战案例展示委托的高级应用。

8.5.1 实战项目:带图片的联系人委托

需求:显示头像、姓名、电话、邮箱的复合布局。

cpp 复制代码
class ContactDelegate : public QStyledItemDelegate {
    Q_OBJECT
    
public:
    explicit ContactDelegate(QObject *parent = nullptr)
        : QStyledItemDelegate(parent) {}
    
protected:
    void paint(QPainter *painter,
               const QStyleOptionViewItem &option,
               const QModelIndex &index) const override {
        painter->save();
        painter->setRenderHint(QPainter::Antialiasing);
        
        // 获取数据(使用自定义角色)
        QString name = index.data(Qt::UserRole).toString();
        QString phone = index.data(Qt::UserRole + 1).toString();
        QString email = index.data(Qt::UserRole + 2).toString();
        QPixmap avatar = index.data(Qt::UserRole + 3).value<QPixmap>();
        
        // 绘制背景
        if (option.state & QStyle::State_Selected) {
            painter->fillRect(option.rect, option.palette.highlight());
        } else {
            painter->fillRect(option.rect, option.palette.base());
        }
        
        // 绘制分隔线
        painter->setPen(QPen(QColor(220, 220, 220), 1));
        painter->drawLine(option.rect.bottomLeft(), option.rect.bottomRight());
        
        // 绘制头像(圆形)
        int avatarSize = 50;
        QRect avatarRect(option.rect.left() + 10,
                        option.rect.top() + (option.rect.height() - avatarSize) / 2,
                        avatarSize, avatarSize);
        
        // 创建圆形裁剪路径
        QPainterPath clipPath;
        clipPath.addEllipse(avatarRect);
        painter->setClipPath(clipPath);
        
        if (!avatar.isNull()) {
            painter->drawPixmap(avatarRect, avatar.scaled(avatarSize, avatarSize,
                                                         Qt::KeepAspectRatioByExpanding,
                                                         Qt::SmoothTransformation));
        } else {
            // 默认头像
            painter->fillRect(avatarRect, QColor(200, 200, 200));
            painter->setPen(Qt::white);
            QFont iconFont = option.font;
            iconFont.setPointSize(20);
            painter->setFont(iconFont);
            painter->drawText(avatarRect, Qt::AlignCenter, "👤");
        }
        
        painter->setClipping(false);
        
        // 绘制文本信息
        int textLeft = avatarRect.right() + 15;
        int textTop = option.rect.top() + 10;
        
        if (option.state & QStyle::State_Selected) {
            painter->setPen(option.palette.highlightedText().color());
        } else {
            painter->setPen(option.palette.text().color());
        }
        
        // 姓名(大号、粗体)
        QFont nameFont = option.font;
        nameFont.setPointSize(12);
        nameFont.setBold(true);
        painter->setFont(nameFont);
        painter->drawText(textLeft, textTop + 15, name);
        
        // 电话(小号)
        QFont detailFont = option.font;
        detailFont.setPointSize(9);
        painter->setFont(detailFont);
        
        if (!(option.state & QStyle::State_Selected)) {
            painter->setPen(Qt::gray);
        }
        
        painter->drawText(textLeft, textTop + 35, "📞 " + phone);
        painter->drawText(textLeft, textTop + 52, "✉ " + email);
        
        painter->restore();
    }
    
    QSize sizeHint(const QStyleOptionViewItem &option,
                   const QModelIndex &index) const override {
        return QSize(400, 70);
    }
};

完整使用示例

cpp 复制代码
#include <QApplication>
#include <QListView>
#include <QStandardItemModel>

int main(int argc, char *argv[]) {
    QApplication app(argc, argv);
    
    // 创建模型
    QStandardItemModel model;
    
    // 添加联系人数据
    for (int i = 0; i < 5; ++i) {
        QStandardItem *item = new QStandardItem;
        
        // 使用自定义角色存储数据
        item->setData(QString("张%1").arg(i + 1), Qt::UserRole);  // 姓名
        item->setData(QString("138%1%2%3%4").arg(i).arg(i).arg(i).arg(i + 1000, 4, 10, QChar('0')), 
                     Qt::UserRole + 1);  // 电话
        item->setData(QString("zhang%1@example.com").arg(i + 1), Qt::UserRole + 2);  // 邮箱
        
        // 头像(这里用颜色代替)
        QPixmap avatar(50, 50);
        avatar.fill(QColor(100 + i * 30, 150, 200));
        item->setData(avatar, Qt::UserRole + 3);
        
        model.appendRow(item);
    }
    
    // 创建视图
    QListView view;
    view.setModel(&model);
    view.setItemDelegate(new ContactDelegate(&view));
    view.setSpacing(0);
    
    view.setWindowTitle("联系人列表");
    view.resize(450, 400);
    view.show();
    
    return app.exec();
}

8.5.2 实战项目:多组件编辑委托

需求:一个单元格内有多个编辑控件(例如:颜色选择器+透明度滑块)。

cpp 复制代码
class ColorAlphaDelegate : public QStyledItemDelegate {
    Q_OBJECT
    
public:
    explicit ColorAlphaDelegate(QObject *parent = nullptr)
        : QStyledItemDelegate(parent) {}
    
protected:
    QWidget* createEditor(QWidget *parent,
                         const QStyleOptionViewItem &option,
                         const QModelIndex &index) const override {
        // 创建复合编辑器容器
        QWidget *container = new QWidget(parent);
        QHBoxLayout *layout = new QHBoxLayout(container);
        layout->setContentsMargins(2, 2, 2, 2);
        layout->setSpacing(5);
        
        // 颜色选择按钮
        QPushButton *colorButton = new QPushButton("选择颜色", container);
        colorButton->setObjectName("colorButton");
        colorButton->setMinimumWidth(80);
        
        // 透明度滑块
        QSlider *alphaSlider = new QSlider(Qt::Horizontal, container);
        alphaSlider->setObjectName("alphaSlider");
        alphaSlider->setRange(0, 255);
        alphaSlider->setValue(255);
        
        // 透明度标签
        QLabel *alphaLabel = new QLabel("255", container);
        alphaLabel->setObjectName("alphaLabel");
        alphaLabel->setMinimumWidth(30);
        
        layout->addWidget(colorButton);
        layout->addWidget(new QLabel("透明度:", container));
        layout->addWidget(alphaSlider);
        layout->addWidget(alphaLabel);
        
        // 连接信号
        connect(colorButton, &QPushButton::clicked, [=]() {
            QColor currentColor = container->property("color").value<QColor>();
            QColor color = QColorDialog::getColor(currentColor, container, "选择颜色");
            if (color.isValid()) {
                container->setProperty("color", color);
                updateButtonColor(colorButton, color);
            }
        });
        
        connect(alphaSlider, &QSlider::valueChanged, [=](int value) {
            alphaLabel->setText(QString::number(value));
            QColor color = container->property("color").value<QColor>();
            color.setAlpha(value);
            container->setProperty("color", color);
        });
        
        return container;
    }
    
    void setEditorData(QWidget *editor, const QModelIndex &index) const override {
        QColor color = index.data(Qt::EditRole).value<QColor>();
        
        editor->setProperty("color", color);
        
        QPushButton *colorButton = editor->findChild<QPushButton*>("colorButton");
        if (colorButton) {
            updateButtonColor(colorButton, color);
        }
        
        QSlider *alphaSlider = editor->findChild<QSlider*>("alphaSlider");
        if (alphaSlider) {
            alphaSlider->setValue(color.alpha());
        }
    }
    
    void setModelData(QWidget *editor,
                     QAbstractItemModel *model,
                     const QModelIndex &index) const override {
        QColor color = editor->property("color").value<QColor>();
        model->setData(index, color);
    }
    
    void updateEditorGeometry(QWidget *editor,
                             const QStyleOptionViewItem &option,
                             const QModelIndex &index) const override {
        editor->setGeometry(option.rect);
    }
    
    void paint(QPainter *painter,
               const QStyleOptionViewItem &option,
               const QModelIndex &index) const override {
        QColor color = index.data(Qt::DisplayRole).value<QColor>();
        
        painter->save();
        
        // 绘制背景
        if (option.state & QStyle::State_Selected) {
            painter->fillRect(option.rect, option.palette.highlight());
        }
        
        // 绘制颜色预览
        QRect colorRect = option.rect.adjusted(5, 5, -5, -5);
        colorRect.setWidth(40);
        
        painter->setPen(Qt::black);
        painter->setBrush(color);
        painter->drawRect(colorRect);
        
        // 绘制颜色信息
        QRect textRect = option.rect.adjusted(55, 0, -5, 0);
        
        if (option.state & QStyle::State_Selected) {
            painter->setPen(option.palette.highlightedText().color());
        } else {
            painter->setPen(option.palette.text().color());
        }
        
        QString colorText = QString("RGB(%1, %2, %3) Alpha: %4")
            .arg(color.red()).arg(color.green())
            .arg(color.blue()).arg(color.alpha());
        
        painter->drawText(textRect, Qt::AlignLeft | Qt::AlignVCenter, colorText);
        
        painter->restore();
    }
    
private:
    void updateButtonColor(QPushButton *button, const QColor &color) const {
        button->setStyleSheet(QString(
            "QPushButton { "
            "  background-color: rgb(%1, %2, %3); "
            "  color: %4; "
            "  border: 1px solid #ccc; "
            "}")
            .arg(color.red()).arg(color.green()).arg(color.blue())
            .arg(color.lightness() > 128 ? "black" : "white"));
    }
};

本节小结

联系人委托 - 复杂的复合布局显示

多组件编辑 - 单元格内多个编辑控件协作

圆形头像 - 使用QPainterPath裁剪

复合编辑器 - 多个控件组合成一个编辑器

实用技巧 - 自定义角色、信号连接、动态样式

关键要点

  1. 使用自定义角色(Qt::UserRole + N)存储多个数据字段
  2. QPainterPath可实现复杂的裁剪效果
  3. 复合编辑器需要一个容器Widget和布局管理
  4. 使用findChild()访问编辑器内的子控件
  5. 通过setProperty()在控件间传递数据
  • 实战项目:带图片的联系人委托
    • 需求:头像、姓名、电话、邮箱复合显示
    • paint()实现复杂布局
    • 完整代码示例
  • 实战项目:多组件编辑委托
    • 需求:一个单元格内多个编辑控件
    • createEditor()实现复合编辑器
    • 完整代码示例

第8章总结

🎉 第8章 Delegate(委托)详解 已全部完成!

本章涵盖了:

  • ✅ 8.1 委托的作用(概念、职责、基类选择)
  • ✅ 8.2 委托的核心方法(6个关键方法)
  • ✅ 8.3 自定义渲染委托(星级评分、进度条、美化效果)
  • ✅ 8.4 自定义编辑委托(多种编辑器实现)
  • ✅ 8.5 复杂委托实战(联系人卡片、复合编辑器)

核心知识点

  1. 委托负责数据的显示和编辑方式
  2. paint()方法控制渲染,createEditor()控制编辑
  3. QStyedItemDelegate是推荐的基类
  4. 可以为不同列设置不同的委托
  5. 支持复杂的自定义渲染和编辑逻辑

接下来可以继续学习第9章"排序与过滤"!


相关推荐
野生技术架构师2 小时前
【面试题】为什么 Java 8 移除了永久代(PermGen)并引入了元空间(Metaspace)?
java·开发语言
Leo July2 小时前
【Java】Java设计模式实战指南:从原理到框架应用
java·开发语言·设计模式
冬奇Lab2 小时前
【Kotlin系列13】DSL设计:构建类型安全的领域语言
开发语言·安全·kotlin
2501_944521592 小时前
Flutter for OpenHarmony 微动漫App实战:分享功能实现
android·开发语言·javascript·flutter·ecmascript
老歌老听老掉牙2 小时前
16宫格属性分析系统:打造专业级科学数据可视化工具
c++·qt·可视化
嵌入式小能手2 小时前
飞凌嵌入式ElfBoard-系统信息与资源之休眠
c语言·开发语言·算法
橘子师兄2 小时前
C++AI大模型接入SDK—API接入大模型思路
开发语言·数据结构·c++·人工智能
Object~2 小时前
7.Go语言中的slice
开发语言·后端·golang
L.EscaRC2 小时前
深度解析 Spring 框架核心代理组件 MethodProxy.java
java·开发语言·spring