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等
✅ 测试与调试 - 基本功能、边界条件、信号、性能测试
设计模型的黄金法则:
- 先设计后实现 - 明确数据结构和需求
- 最小化实现 - 只实现需要的功能
- 信号必须准确 - 每次数据变化都要发送正确的信号
- 充分测试 - 测试所有边界条件和异常情况
- 确定数据结构
- 选择合适的基类
- 实现必要的接口
- 发送正确的信号
- 测试与调试
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 ¤t, 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的完美配合
关键要点:
- 使用自定义角色存储额外数据
- 通过不同的Qt::ItemDataRole实现丰富的视觉效果
- 正确使用begin/endInsertRows和dataChanged信号
- 提供便利的操作方法(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 ¬e = "")
: 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的标准实现
✅ 统计功能 - 数据汇总和分析
关键技术点:
- 使用结构体存储完整的行数据
- 在data()中根据角色返回不同类型的数据
- 在setData()中进行数据验证
- 正确使用begin/endInsertRows和begin/endRemoveRows
- 根据数据值动态设置显示样式
- 需求分析:姓名、学号、成绩、备注
- 数据结构:使用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的作用
核心职责:
- 跟踪选择状态 - 记录哪些项被选中
- 跟踪当前项 - 记录当前焦点项
- 发送选择信号 - 通知视图选择变化
- 提供选择接口 - 允许程序化地控制选择
为什么需要独立的选择模型:
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 ¤t,
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()时自动创建选择模型
✅ 可共享 - 多个视图可共享同一选择模型
✅ 丰富接口 - 提供多种获取选择信息的方法
关键要点:
- 选择模型是视图的一部分,但独立于数据模型
- 每个视图默认有自己的选择模型
- 可以通过setSelectionModel()共享选择模型
- 使用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 ¤t, 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三种行为
关键要点:
- 使用QItemSelectionModel::SelectionFlags控制选择行为
- 当前项(焦点)和选中项是两个不同的概念
- 选择模式决定用户如何进行选择操作
- 选择行为决定选择的粒度(单元格/行/列)
- 选择项、行、列
- 清除选择
- 当前项 vs 选中项
- 选择模式:单选、多选、扩展选择、连续选择
7.3 选择相关信号
QItemSelectionModel提供了多个信号来通知选择状态的变化,这些信号对于构建交互式应用非常重要。
7.3.1 currentChanged() - 当前项变化
信号定义:
cpp
void currentChanged(const QModelIndex ¤t, const QModelIndex &previous);
使用场景:
- 跟踪用户的焦点变化
- 更新详情面板
- 响应键盘导航
示例:
cpp
QItemSelectionModel *sm = view->selectionModel();
connect(sm, &QItemSelectionModel::currentChanged,
[=](const QModelIndex ¤t, 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 ¤t, 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 - 跟踪选择集合变化
✅ 实战应用 - 主从视图的完整实现
✅ 性能优化 - 防抖动、批量处理
关键要点:
- currentChanged用于单个焦点项的跟踪
- selectionChanged用于多选场景
- 可以组合使用两个信号以实现复杂交互
- 注意性能优化,避免频繁的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 - 表示矩形选择区域
✅ 范围操作 - 合并、遍历、检查包含关系
✅ 批量操作 - 全选、反选、批量编辑、导出
关键要点:
- QItemSelection用于表示多个不连续的选择范围
- 可以通过merge()合并多个选择
- indexes()方法获取所有选中的索引
- 适合实现批量操作功能
- QItemSelection的使用
- 选择范围的操作
- 实战:批量操作选中项
第7章总结:
🎉 第7章 选择模型(QItemSelectionModel) 已全部完成!
本章涵盖了:
- ✅ 7.1 选择模型基础(作用、关系、获取)
- ✅ 7.2 选择操作(选择、清除、模式)
- ✅ 7.3 选择相关信号(currentChanged、selectionChanged)
- ✅ 7.4 QItemSelection(范围操作、批量处理)
核心知识点:
- 选择模型独立于数据模型和视图
- 当前项和选中项是两个不同的概念
- 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 委托的职责:渲染和编辑
双重职责:
- 渲染(Rendering) - 如何显示数据
- 编辑(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 - 现代推荐的委托基类
✅ 自定义场景 - 特殊显示和编辑需求
关键要点:
- 委托负责数据的显示和编辑方式
- 默认委托已能满足大部分简单需求
- QStyledItemDelegate优于QItemDelegate
- 可以为视图、列、行分别设置不同的委托
- 自定义委托通过重写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() - 设置编辑器位置和大小
关键要点:
- paint()负责渲染,sizeHint()控制大小
- 编辑流程:createEditor → setEditorData → 用户编辑 → setModelData
- 使用qobject_cast安全地转换编辑器类型
- 可以在setModelData()中进行数据验证
- 为不同列设置不同编辑器可实现丰富的编辑体验
- 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使用 - 绘制各种图形和文本
✅ 星级评分 - 复杂的自定义渲染示例
✅ 进度条 - 使用样式和自定义两种方式
✅ 美化效果 - 圆角、阴影、渐变等视觉效果
关键要点:
- 使用painter->save()和restore()保护状态
- 启用抗锯齿以获得更好的视觉效果
- 使用QPainterPath绘制复杂形状
- 合理使用QStyle绘制系统原生控件
- 根据状态(选中、悬停)改变显示效果
- 重写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的完整实现
✅ 状态选择框 - 带颜色指示的下拉选择
✅ 日期选择器 - 带日期提醒的日期编辑
✅ 滑块编辑器 - 直观的数值调节
关键要点:
- 根据列号选择合适的编辑器类型
- 为编辑器设置合理的范围和格式
- 可以在paint()中自定义显示效果
- 使用信号实现即时提交和关闭编辑器
- 添加数据验证确保输入合法
- 为不同列提供不同编辑器
- 常用编辑器: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裁剪
✅ 复合编辑器 - 多个控件组合成一个编辑器
✅ 实用技巧 - 自定义角色、信号连接、动态样式
关键要点:
- 使用自定义角色(Qt::UserRole + N)存储多个数据字段
- QPainterPath可实现复杂的裁剪效果
- 复合编辑器需要一个容器Widget和布局管理
- 使用findChild()访问编辑器内的子控件
- 通过setProperty()在控件间传递数据
- 实战项目:带图片的联系人委托
- 需求:头像、姓名、电话、邮箱复合显示
- paint()实现复杂布局
- 完整代码示例
- 实战项目:多组件编辑委托
- 需求:一个单元格内多个编辑控件
- createEditor()实现复合编辑器
- 完整代码示例
第8章总结:
🎉 第8章 Delegate(委托)详解 已全部完成!
本章涵盖了:
- ✅ 8.1 委托的作用(概念、职责、基类选择)
- ✅ 8.2 委托的核心方法(6个关键方法)
- ✅ 8.3 自定义渲染委托(星级评分、进度条、美化效果)
- ✅ 8.4 自定义编辑委托(多种编辑器实现)
- ✅ 8.5 复杂委托实战(联系人卡片、复合编辑器)
核心知识点:
- 委托负责数据的显示和编辑方式
- paint()方法控制渲染,createEditor()控制编辑
- QStyedItemDelegate是推荐的基类
- 可以为不同列设置不同的委托
- 支持复杂的自定义渲染和编辑逻辑
接下来可以继续学习第9章"排序与过滤"!