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章"排序与过滤"!


相关推荐
用户805533698032 天前
不止三件套:QObject 属性系统全关键字与运行时反射!
c++·qt
xcyxiner2 天前
DicomViewer (vcpkg Windows和ubuntu编译)7
qt
Quz7 天前
QML Hello World 入门示例
qt
xcyxiner10 天前
DicomViewer (dcmtk读取dcm文件)5
qt
xcyxiner10 天前
DicomViewer (后台线程处理文件)4
qt
xcyxiner11 天前
DicomViewer (添加模型类)3
qt
xcyxiner11 天前
DicomViewer (目录调整) 2
qt
xcyxiner11 天前
dcmtk vtk vtk-dicom(gdcm) 编译(debug) v2
qt
LDR00613 天前
Type-C 快充全面升级!LDR6601 赋能个人护理便携电机,重塑剃须刀 / 理发器新体验
c语言·开发语言
雪碧聊技术13 天前
Tree.js是什么?一文讲透
开发语言·javascript·ecmascript