1. 先搞清楚 QItemSelectionModel 和 QStyledItemDelegate 到底解决什么问题
在 Qt 的 Model/View 框架里,新手最容易卡住的两个点,一个是“怎么让用户选中多行数据”,另一个是“怎么让表格或列表里的单元格显示成我想要的样子”。这两个问题,单靠 QTableView 或 QListView 自己解决不了,必须引入两个关键角色: QItemSelectionModel 和 QStyledItemDelegate 。
简单来说:
- QItemSelectionModel 是“选区的管理者”。它负责记录用户在视图(View)中选择了哪些项(Item),管理单选、多选、区域选择等所有与“选中状态”相关的逻辑。你不用自己去计算哪些行、哪些列被点了,它帮你管着。
- QStyledItemDelegate 是“单元格的化妆师和交互代理”。它决定了每个单元格如何绘制(比如把数字显示为进度条),以及如何编辑(比如点击后弹出一个日期选择器)。默认的 Delegate 只能显示文本和简单的编辑框,想自定义外观和交互,就得靠它。
如果你正在做一个数据管理工具、配置面板或者任何需要展示列表/表格数据的界面,并且遇到了“选择功能不好用”或者“单元格显示太丑/难用”的问题,那这篇文章就是为你写的。我会把这两个看似独立,但在实际项目中经常需要配合使用的类,拆解成可落地的步骤和代码。
最关键的实践价值在于: 理解并正确使用这两个类,能让你从“仅仅是把数据显示出来”进阶到“构建一个交互友好、表现力强的专业级数据界面” 。下面,我们就从环境准备开始,一步步实现。
2. 环境准备与项目基础框架搭建
在开始写具体的 Selection 和 Delegate 代码之前,得先把舞台搭好。这里假设你已经有基本的 Qt C++ 开发环境(Qt 5.15 或 Qt 6.x, MSVC/MinGW/GCC 编译器均可)。我们创建一个最基础的 Model-View 结构作为实验沙盒。
2.1 创建标准 Qt Widgets 项目
使用 Qt Creator 新建一个 “Qt Widgets Application” 项目。在创建过程中,确保勾选了 QMainWindow 作为主窗口基类。项目生成后,你会得到 main.cpp , mainwindow.h , mainwindow.cpp 等文件。
2.2 定义数据模型 (Model)
Model 是数据的源头。我们从最简单的开始,继承 QAbstractTableModel 来创建一个自定义模型。在项目中新建一个头文件 mytablemodel.h 。
// mytablemodel.h
#ifndef MYTABLEMODEL_H
#define MYTABLEMODEL_H
#include <QAbstractTableModel>
#include <QVector>
// 定义一个简单的数据项结构体
struct MyDataItem {
QString name;
int score;
bool passed;
QDateTime timestamp;
};
class MyTableModel : public QAbstractTableModel
{
Q_OBJECT
public:
explicit MyTableModel(QObject *parent = nullptr);
// 必须重写的基类虚函数
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
// 为了使数据可编辑,还需要重写 setData 和 flags
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
// 自定义方法:用于初始化测试数据
void initTestData();
private:
QVector<MyDataItem> m_data; // 存储数据的容器
};
#endif // MYTABLEMODEL_H
接着实现这个模型 mytablemodel.cpp :
// mytablemodel.cpp
#include "mytablemodel.h"
#include <QDateTime>
MyTableModel::MyTableModel(QObject *parent)
: QAbstractTableModel(parent)
{
initTestData();
}
int MyTableModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return m_data.size();
}
int MyTableModel::columnCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return 4; // 对应 MyDataItem 的四个字段:name, score, passed, timestamp
}
QVariant MyTableModel::data(const QModelIndex &index, int role) const
{
if (!index.isValid() || index.row() >= m_data.size() || index.column() >= 4)
return QVariant();
const MyDataItem &item = m_data.at(index.row());
switch (role) {
case Qt::DisplayRole:
case Qt::EditRole: // 编辑时也返回原始数据
switch (index.column()) {
case 0: return item.name;
case 1: return item.score;
case 2: return item.passed ? tr("通过") : tr("未通过"); // 显示角色用中文
case 3: return item.timestamp.toString("yyyy-MM-dd hh:mm:ss");
}
break;
case Qt::TextAlignmentRole:
if (index.column() == 1) { // 分数列居中
return Qt::AlignCenter;
}
break;
case Qt::CheckStateRole:
if (index.column() == 2) { // 布尔值列用 CheckStateRole 来支持复选框
return item.passed ? Qt::Checked : Qt::Unchecked;
}
break;
}
return QVariant();
}
QVariant MyTableModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (role != Qt::DisplayRole)
return QVariant();
if (orientation == Qt::Horizontal) {
switch (section) {
case 0: return tr("姓名");
case 1: return tr("分数");
case 2: return tr("是否通过");
case 3: return tr("时间戳");
}
}
return QVariant();
}
bool MyTableModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if (!index.isValid() || index.row() >= m_data.size())
return false;
MyDataItem &item = m_data[index.row()];
bool changed = false;
switch (role) {
case Qt::EditRole:
switch (index.column()) {
case 0:
if (value.canConvert<QString>()) {
item.name = value.toString();
changed = true;
}
break;
case 1:
if (value.canConvert<int>()) {
item.score = value.toInt();
changed = true;
}
break;
// 第2列(布尔值)我们通过 CheckStateRole 来编辑,见下文
case 3:
// 时间戳编辑略复杂,通常用 Delegate,这里先不实现
break;
}
break;
case Qt::CheckStateRole:
if (index.column() == 2) {
Qt::CheckState state = static_cast<Qt::CheckState>(value.toInt());
item.passed = (state == Qt::Checked);
changed = true;
}
break;
}
if (changed) {
// 发出数据改变信号,这是 Model/View 框架更新的关键
emit dataChanged(index, index, {role});
return true;
}
return false;
}
Qt::ItemFlags MyTableModel::flags(const QModelIndex &index) const
{
Qt::ItemFlags defaultFlags = QAbstractTableModel::flags(index);
if (!index.isValid())
return defaultFlags;
// 所有单元格都可选择
defaultFlags |= Qt::ItemIsSelectable;
// 前三列可编辑
if (index.column() < 3) {
defaultFlags |= Qt::ItemIsEditable;
}
// 第二列(是否通过)额外支持用户可点击的复选框
if (index.column() == 2) {
defaultFlags |= Qt::ItemIsUserCheckable;
}
// 第一列(分数)我们稍后会通过 Delegate 限制输入范围
return defaultFlags;
}
void MyTableModel::initTestData()
{
m_data.clear();
m_data.append({“张三”, 85, true, QDateTime::currentDateTime()});
m_data.append({“李四”, 42, false, QDateTime::currentDateTime().addSecs(-3600)});
m_data.append({“王五”, 93, true, QDateTime::currentDateTime().addSecs(-7200)});
m_data.append({“赵六”, 60, true, QDateTime::currentDateTime().addSecs(-10800)});
}
这个模型提供了4列数据,其中“是否通过”列使用了 Qt::CheckStateRole ,这为后面使用 Delegate 显示复选框打下了基础。 setData 和 flags 的重写使得模型可编辑。
2.3 设置主窗口视图 (View)
现在,在 MainWindow 中设置一个 QTableView 并使用我们的模型。修改 mainwindow.h 和 mainwindow.cpp 。
// mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
class QTableView;
class MyTableModel;
class QItemSelectionModel; // 前向声明
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
private slots:
// 用于响应选择变化的槽函数
void onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected);
private:
void setupUI();
void setupModelAndView();
QTableView *m_tableView;
MyTableModel *m_model;
QItemSelectionModel *m_selectionModel; // 我们将显式持有它
};
#endif // MAINWINDOW_H
// mainwindow.cpp
#include "mainwindow.h"
#include "mytablemodel.h"
#include <QTableView>
#include <QVBoxLayout>
#include <QWidget>
#include <QDebug>
#include <QItemSelectionModel>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, m_tableView(new QTableView(this))
, m_model(new MyTableModel(this))
{
setupUI();
setupModelAndView();
resize(600, 400);
}
MainWindow::~MainWindow()
{
}
void MainWindow::setupUI()
{
QWidget *centralWidget = new QWidget(this);
QVBoxLayout *layout = new QVBoxLayout(centralWidget);
layout->addWidget(m_tableView);
setCentralWidget(centralWidget);
}
void MainWindow::setupModelAndView()
{
// 1. 设置模型
m_tableView->setModel(m_model);
// 2. 获取视图默认的 SelectionModel 并连接信号
// 每个视图在设置模型后,会自动创建一个 QItemSelectionModel。
// 我们可以直接使用它。
m_selectionModel = m_tableView->selectionModel();
if (m_selectionModel) {
connect(m_selectionModel, &QItemSelectionModel::selectionChanged,
this, &MainWindow::onSelectionChanged);
}
// 3. 设置选择行为
m_tableView->setSelectionMode(QAbstractItemView::ExtendedSelection); // 支持多选(Ctrl+点击,Shift+区域)
m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows); // 按行选择
// 4. 调整列宽
m_tableView->horizontalHeader()->setStretchLastSection(true);
}
void MainWindow::onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
{
Q_UNUSED(deselected);
// 打印当前选中的行号
QModelIndexList selectedIndexes = m_selectionModel->selectedRows();
QStringList rows;
for (const QModelIndex &index : selectedIndexes) {
rows.append(QString::number(index.row()));
}
qDebug() << “当前选中的行:” << rows.join(“, “);
}
现在运行程序,你应该能看到一个包含4行数据的表格。你可以用鼠标点击、Ctrl+点击、Shift+点击来选择多行,并且在 Qt Creator 的“应用程序输出”面板中,会看到打印出的选中行号。 这就是 QItemSelectionModel 在幕后工作的结果 。视图 ( QTableView ) 内部已经关联了一个 QItemSelectionModel ,它自动处理了鼠标和键盘的交互,并发出 selectionChanged 信号。
基础框架已经就绪。接下来,我们深入 QItemSelectionModel ,看看如何更主动、更精细地控制选择行为。
3. 深入 QItemSelectionModel:不只是被动接收选择
在上一节,我们通过 selectionModel() 获取了视图内置的选择模型并监听其信号。但 QItemSelectionModel 的能力远不止于此。你经常需要 以编程方式控制选择 ,或者 理解复杂的选择状态 。
3.1 核心概念:QModelIndex 与 QItemSelection
- QModelIndex :代表模型中的一个数据项的“坐标”(行、列、父索引)。它是访问模型中具体数据的句柄。
- QItemSelection :代表一个或多个
QModelIndex的集合,通常是一个连续的矩形区域(对于表格)或一个范围(对于列表)。它由QItemSelectionRange组成。
QItemSelectionModel 管理的就是一个或多个 QItemSelection (当前选择),并允许你在它们之上进行操作(选择、反选、切换、清除)。
3.2 编程式选择操作
假设我们想在点击一个按钮时,选中所有“分数”大于 60 的行。我们在 MainWindow 中添加一个按钮和对应的槽函数。
首先在 mainwindow.h 的 private slots 区域添加:
void selectPassedRows();
在 mainwindow.cpp 的 setupUI 函数中添加按钮:
// 在 setupUI 函数中,layout 添加按钮
QPushButton *btnSelectPassed = new QPushButton(“选中及格行”, this);
layout->addWidget(btnSelectPassed);
connect(btnSelectPassed, &QPushButton::clicked, this, &MainWindow::selectPassedRows);
然后实现这个函数:
void MainWindow::selectPassedRows()
{
if (!m_selectionModel || !m_model) return;
// 1. 先清除当前所有选择
m_selectionModel->clearSelection();
// 2. 遍历模型,找出分数>=60的行
QItemSelection selection;
for (int row = 0; row < m_model->rowCount(); ++row) {
QModelIndex scoreIndex = m_model->index(row, 1); // 第1列是分数
int score = m_model->data(scoreIndex, Qt::DisplayRole).toInt();
if (score >= 60) {
// 3. 对于每一行,我们选择整行。需要创建一个包含该行所有列的范围。
QModelIndex leftTop = m_model->index(row, 0);
QModelIndex rightBottom = m_model->index(row, m_model->columnCount() - 1);
QItemSelectionRange range(leftTop, rightBottom);
selection.merge(range, QItemSelectionModel::Select);
}
}
// 4. 应用这个选择。使用 Select 命令,它会用新的选择替换当前选择。
// 因为我们之前 clear 了,所以效果是“选中所有及格行”。
m_selectionModel->select(selection, QItemSelectionModel::Select);
}
这里的关键是 m_selectionModel->select() 函数。它的第二个参数是 QItemSelectionModel::SelectionFlags ,这是一个枚举组合,决定了操作方式:
-
QItemSelectionModel::Select:将指定的项添加到当前选择中(如果已存在则不变)。 -
QItemSelectionModel::Deselect:从当前选择中移除指定的项。 -
QItemSelectionModel::Toggle:切换指定项的选择状态。 -
QItemSelectionModel::ClearAndSelect:先清除所有选择,然后选择指定的项(这是我们上面分两步做的)。 -
QItemSelectionModel::Current:与Select等结合使用,设置当前焦点项(影响键盘导航)。
3.3 处理复杂选择状态与交互
有时你需要根据选择状态来更新界面其他部分。例如,在状态栏显示选中行的统计信息。我们修改 onSelectionChanged 槽函数:
void MainWindow::onSelectionChanged(const QItemSelection &selected, const QItemSelection &deselected)
{
Q_UNUSED(deselected);
int totalScore = 0;
int count = 0;
// selected.indexes() 返回所有被选中的单元格的索引。
// 因为我们设置的是 SelectRows,所以选中的是整个行的所有单元格。
// 我们只取第一列(分数列)的索引来计算总分。
QModelIndexList selectedIndexes = m_selectionModel->selectedIndexes();
for (const QModelIndex &index : selectedIndexes) {
if (index.column() == 1) { // 只处理分数列
bool ok;
int score = m_model->data(index, Qt::DisplayRole).toInt(&ok);
if (ok) {
totalScore += score;
count++;
}
}
}
QString statusText;
if (count > 0) {
double average = static_cast<double>(totalScore) / count;
statusText = tr(“选中 %1 行,平均分:%2”).arg(count).arg(average, 0, ‘f’, 1);
} else {
statusText = tr(“未选中任何行”);
}
statusBar()->showMessage(statusText);
}
现在,当你选择不同行时,状态栏会实时显示选中行的平均分。这展示了如何利用 QItemSelectionModel 提供的信息来驱动 UI 更新。
注意 :
selected和deselected参数分别代表 本次操作中新选择的 和 本次操作中取消选择的 范围。在处理大量数据时,直接使用这两个参数进行增量更新比遍历所有选中项 (selectedIndexes()) 效率更高。
3.4 选择模式的进阶设置
我们之前用 setSelectionMode 设置了 ExtendedSelection 。其他常用模式包括:
-
SingleSelection:只能单选。 -
MultiSelection:简单的多选(点击即切换选中状态,无需 Ctrl 键)。交互逻辑与ExtendedSelection不同,根据需求选择。 -
ContiguousSelection:只能选择连续的区域(通过 Shift 键)。
setSelectionBehavior 决定了选择的最小单位:
-
SelectItems:以单元格为单位。 -
SelectRows:以行为单位(我们用的这个)。 -
SelectColumns:以列为单位。
一个常见的坑 :如果你设置了 SelectRows ,但通过 selectedIndexes() 获取索引,得到的仍然是所有被选中行的 每一个单元格 的索引。在处理数据时,需要自己过滤(例如只取第0列的索引来代表行)。
至此,我们已经能主动控制选择、响应选择变化。接下来,解决另一个痛点:如何让单元格的显示和编辑更符合业务需求?这就需要请出 QStyledItemDelegate 。
4. 掌握 QStyledItemDelegate:自定义单元格的绘制与编辑
默认的 Delegate 只能显示文本和提供一个简单的 QLineEdit 进行编辑。对于“分数”列,我们可能想显示一个进度条;对于“是否通过”列,我们想显示一个可点击的复选框;对于“时间戳”列,我们想在编辑时弹出一个日历。这些都需要自定义 Delegate。
4.1 创建自定义代理类
我们创建一个代理类,首先处理“分数”列的进度条显示。新建文件 scoredelegate.h 和 scoredelegate.cpp 。
// scoredelegate.h
#ifndef SCOREDELEGATE_H
#define SCOREDELEGATE_H
#include <QStyledItemDelegate>
class ScoreDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
explicit ScoreDelegate(QObject *parent = nullptr);
// 重写:自定义绘制
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
// 重写:返回编辑控件(如果需要自定义编辑控件,例如滑块)
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
// 重写:将模型数据设置到编辑器
void setEditorData(QWidget *editor, const QModelIndex &index) const override;
// 重写:将编辑器数据保存回模型
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override;
// 重写:更新编辑器几何位置
void updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
};
#endif // SCOREDELEGATE_H
// scoredelegate.cpp
#include “scoredelegate.h”
#include <QPainter>
#include <QStyleOptionProgressBar>
#include <QProgressBar>
#include <QSpinBox> // 我们使用 SpinBox 作为编辑器,方便限制范围
#include <QApplication>
ScoreDelegate::ScoreDelegate(QObject *parent)
: QStyledItemDelegate(parent)
{
}
void ScoreDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
// 1. 只处理第1列(分数列)
if (index.column() == 1) {
// 2. 获取数据
int score = index.data(Qt::DisplayRole).toInt();
// 3. 准备进度条样式选项
QStyleOptionProgressBar progressBarOption;
progressBarOption.rect = option.rect.adjusted(2, 2, -2, -2); // 内边距
progressBarOption.minimum = 0;
progressBarOption.maximum = 100;
progressBarOption.progress = score;
progressBarOption.text = QString(“%1%”).arg(score);
progressBarOption.textVisible = true;
progressBarOption.textAlignment = Qt::AlignCenter;
// 4. 绘制进度条背景和进度(由样式表控制)
QApplication::style()->drawControl(QStyle::CE_ProgressBar, &progressBarOption, painter);
// 5. 绘制文本(样式已经绘制了,这里可以省略)
// QStyledItemDelegate::paint(painter, option, index); // 不要调用基类,否则会覆盖
} else {
// 其他列,使用基类的默认绘制(文本)
QStyledItemDelegate::paint(painter, option, index);
}
}
QWidget *ScoreDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
Q_UNUSED(option);
// 只对分数列提供自定义编辑器
if (index.column() == 1) {
QSpinBox *editor = new QSpinBox(parent);
editor->setFrame(false); // 无边框,更美观
editor->setMinimum(0);
editor->setMaximum(100);
editor->setSuffix(“分”);
return editor;
}
// 其他列返回 nullptr,视图会使用默认的 QLineEdit
return QStyledItemDelegate::createEditor(parent, option, index);
}
void ScoreDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const
{
if (index.column() == 1) {
int score = index.data(Qt::EditRole).toInt();
QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
spinBox->setValue(score);
} else {
QStyledItemDelegate::setEditorData(editor, index);
}
}
void ScoreDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
if (index.column() == 1) {
QSpinBox *spinBox = static_cast<QSpinBox*>(editor);
spinBox->interpretText(); // 确保获取当前显示的值
int value = spinBox->value();
model->setData(index, value, Qt::EditRole);
} else {
QStyledItemDelegate::setModelData(editor, model, index);
}
}
void ScoreDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
Q_UNUSED(index);
editor->setGeometry(option.rect);
}
这个代理做了以下几件事:
- paint : 在“分数”列,不画文本,而是画一个
QProgressBar。注意,我们使用了QStyle来绘制,这能保证外观与当前系统主题一致。 - createEditor : 当用户双击“分数”列准备编辑时,我们提供一个
QSpinBox(带上下箭头的数字输入框),并限制输入范围在 0-100。 - setEditorData / setModelData : 负责在编辑器打开时从模型加载数据,以及在编辑完成后将数据保存回模型。
- updateEditorGeometry : 确保编辑器出现在正确的位置。
4.2 将代理设置到视图
回到 MainWindow 的 setupModelAndView 函数,在设置模型后添加:
// 5. 为特定列设置自定义代理
ScoreDelegate *scoreDelegate = new ScoreDelegate(this);
m_tableView->setItemDelegateForColumn(1, scoreDelegate); // 为第1列(分数)设置代理
现在运行程序,“分数”列将以进度条形式显示,双击编辑时会弹出 QSpinBox 。
4.3 处理复选框列(使用内置功能)
对于“是否通过”列,我们想要一个可点击的复选框。其实 Qt 已经为布尔型数据提供了内置支持,我们之前在模型的 data() 函数中为第2列返回了 Qt::CheckStateRole ,并且在 flags() 中为该项添加了 Qt::ItemIsUserCheckable 标志。这已经足够了!视图会自动为这一列显示复选框,并且用户点击复选框会触发模型的 setData() (使用 Qt::CheckStateRole )。
验证一下 :运行程序,点击“是否通过”列的复选框,你会发现状态可以切换,并且我们模型中的 m_data 也会被更新(因为 setData 被调用)。这就是 Qt Model/View 框架的优雅之处: 模型负责存储数据和逻辑,视图负责显示,代理负责定制显示和编辑,三者通过角色(Role)和索引(Index)通信,职责清晰 。
4.4 创建更复杂的代理(例如日期时间编辑器)
假设我们想为“时间戳”列提供一个 QDateTimeEdit 编辑器。创建 datetimeedelegate.h/cpp ,其结构与 ScoreDelegate 类似,主要区别在 createEditor 和数据处理部分。
// 在 datetimeedelegate.cpp 的 createEditor 中
if (index.column() == 3) {
QDateTimeEdit *editor = new QDateTimeEdit(parent);
editor->setDisplayFormat(“yyyy-MM-dd HH:mm:ss”);
editor->setCalendarPopup(true); // 弹出日历
return editor;
}
在 setEditorData 和 setModelData 中,使用 QDateTime 类型进行转换。然后在主窗口中为第3列设置这个代理。
DateTimeDelegate *dateTimeDelegate = new DateTimeDelegate(this);
m_tableView->setItemDelegateForColumn(3, dateTimeDelegate);
4.5 代理使用的边界与性能
- 作用域 :
setItemDelegateForColumn和setItemDelegateForRow可以为特定行/列设置代理。setItemDelegate则为整个视图设置一个全局代理(需要你在代理内部根据行列号判断如何绘制/编辑)。 - 性能 :
paint函数会被频繁调用(滚动、重绘时)。确保其中的计算轻量。避免在paint中进行复杂查询或对象创建。 - 编辑器生命周期 :编辑器控件 (
QWidget) 在编辑开始时创建,在编辑完成或取消时销毁。不要在代理中缓存编辑器实例。 - 样式 :自定义绘制时,尽量使用
QStyle绘制原生控件,这样能保持跨平台外观一致。直接使用painter->drawXXX绘制原始图形虽然灵活,但可能不遵循系统主题。
现在,你的表格已经具备了高度自定义的显示和编辑能力。最后,我们把 Selection 和 Delegate 结合起来,实现一个常见的联动功能。
5. 综合实战:选择项高亮与条件格式渲染
一个常见的需求是:根据数据状态(例如分数不及格)高亮整行,并且当用户选中某行时,用另一种高亮色显示。这需要同时用到模型数据(判断条件)和选择状态。
5.1 在模型中提供判断数据
首先,在 MyTableModel 的 data() 函数中,我们可以根据 Qt::BackgroundRole 来设置背景色。
// 在 MyTableModel::data 函数中,添加一个 case
case Qt::BackgroundRole:
if (index.column() == 0) { // 只在第一列设置行背景色,避免每列都设置
int score = m_data.at(index.row()).score;
if (score < 60) {
return QBrush(QColor(255, 200, 200)); // 浅红色背景表示不及格
}
}
break;
这样,所有分数小于60的行,其第一列背景会变成浅红色。这是一种简单的条件格式。
5.2 在代理中响应选择状态进行绘制
但是,上面的方法有一个问题:当用户选中一行时,系统默认的选择高亮色(通常是蓝色)会覆盖我们设置的条件格式背景色。为了同时体现“选中”和“条件高亮”,我们需要在自定义代理的 paint 函数中做更精细的控制。
修改 ScoreDelegate::paint 函数,或者创建一个新的、应用于所有列的全局代理。这里我们修改 ScoreDelegate ,让它也处理背景绘制。
void ScoreDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
// 复制一份 option,因为我们要修改它
QStyleOptionViewItem opt = option;
// 1. 条件格式:分数不及格,设置背景色
if (index.column() == 0) { // 为姓名列设置行背景
int score = index.sibling(index.row(), 1).data(Qt::DisplayRole).toInt(); // 获取同行的分数
if (score < 60) {
opt.backgroundBrush = QBrush(QColor(255, 230, 230)); // 更浅的红色,避免太刺眼
}
}
// 2. 处理分数列的进度条绘制
if (index.column() == 1) {
// ... 之前的进度条绘制代码 ...
// 在绘制进度条前,先绘制背景(包括条件格式背景)
if (opt.state & QStyle::State_Selected) {
// 如果被选中,使用选中的背景色(覆盖条件格式背景)
painter->fillRect(opt.rect, opt.palette.highlight());
} else if (opt.backgroundBrush.style() != Qt::NoBrush) {
// 否则,如果设置了条件格式背景,就绘制它
painter->fillRect(opt.rect, opt.backgroundBrush);
}
// 然后绘制进度条(进度条控件本身是透明的)
QApplication::style()->drawControl(QStyle::CE_ProgressBar, &progressBarOption, painter);
return; // 我们自己完成了绘制,直接返回
}
// 3. 对于其他列,交给基类绘制,它会处理文本、选中状态等。
// 基类的绘制会自动处理 opt 中的背景和选中状态。
QStyledItemDelegate::paint(painter, opt, index);
}
这个逻辑更清晰:
- 我们根据业务逻辑(分数<60)准备了一个背景画刷 (
opt.backgroundBrush)。 - 在绘制“分数”列时,我们手动判断:如果单元格被选中 (
opt.state & QStyle::State_Selected),就绘制系统高亮色;否则,如果设置了条件背景,就绘制条件背景。然后在其上绘制进度条。 - 对于其他列(如姓名列),我们将包含条件背景信息的
opt传递给基类的paint方法。基类方法会正确处理选中状态(绘制高亮)和未选中状态(绘制我们传入的条件背景)。
关键点 :
QStyleOptionViewItem的state成员包含了QStyle::State_Selected标志,这个标志是由视图的QItemSelectionModel维护并设置的。代理在绘制每个单元格时,视图会告诉它这个单元格是否被选中。这就是 SelectionModel 和 Delegate 的协作 :SelectionModel 管理“哪些被选中”的状态,Delegate 根据这个状态决定如何绘制。
5.3 实现行交替背景色与选择色的协调
Qt 视图本身支持行交替背景色 ( setAlternatingRowColors )。如果你同时开启了交替色、条件格式和选择高亮,绘制顺序和颜色叠加会变得复杂。一个稳妥的做法是:
- 让 Qt 处理交替背景色和默认选择色 :调用基类的
paint方法。 - 在基类绘制之后,再叠加条件格式 :但这需要更复杂的绘制逻辑,可能要用到
painter->fillRect并设置一定的透明度。
更常见的实践是: 如果使用了复杂的条件格式,就关闭系统的交替行颜色 ( setAlternatingRowColors(false) ),并在自定义代理中统一管理所有视觉表现 。
5.4 最终整合与测试
将更新后的代理设置到视图。现在运行程序,你应该能看到:
- 分数不及格的行,背景有浅红色提示。
- 选中某行时,该行显示系统高亮色(覆盖条件背景)。
- 分数列显示为进度条,并可点击编辑。
- 是否通过列有可点击的复选框。
- 在状态栏可以看到选中行的平均分。
- 通过按钮可以编程选中所有及格行。
6. 排查清单与进阶建议
当你按照上述步骤实现,但效果不对时,可以按以下顺序排查:
6.1 选择模型 (QItemSelectionModel) 相关问题
- 信号没触发 :检查
connect语句是否正确,特别是m_selectionModel是否在设置模型后获取(setModel之后)。 - 选择模式不对 :确认
setSelectionMode和setSelectionBehavior是否设置正确。ExtendedSelection需要 Ctrl/Shift 键配合。 - 编程选择无效 :
- 检查
QModelIndex是否有效 (index.isValid())。 - 检查
selection.merge或m_selectionModel->select的参数是否正确。 - 确保操作的是正确的模型 (
m_model) 和选择模型 (m_selectionModel)。
- 检查
- 获取选中数据为空 :如果使用
selectedRows(),确保选择行为是SelectRows。注意selectedRows()返回的是每行 第一列 的索引。使用selectedIndexes()会返回所有选中单元格的索引。
6.2 代理 (QStyledItemDelegate) 相关问题
- 代理没生效 :
- 检查
setItemDelegateForColumn或setItemDelegateForRow的列号/行号是否正确。 - 确保自定义代理类正确继承了
QStyledItemDelegate并重写了相关方法。 - 在代理的
paint方法中,对于不想自定义的列,一定要调用QStyledItemDelegate::paint(painter, option, index);,否则单元格会是空白。
- 检查
- 编辑器不弹出或数据不保存 :
- 检查模型的
flags()方法是否对目标列返回了Qt::ItemIsEditable。 - 在代理的
createEditor中,确保返回了正确的编辑器控件。 - 检查
setEditorData和setModelData中的类型转换是否正确。 - 确保模型的
setData()方法被正确调用并返回true。
- 检查模型的
- 绘制错乱或性能差 :
- 在
paint方法中不要创建QBrush,QPen,QFont等对象,应在构造函数中创建并复用。 - 复杂的绘制计算,考虑在模型的数据角色中预先计算好。
- 使用
QStyle绘制标准控件以确保性能和质量。
- 在
6.3 进阶使用建议
- 自定义视图 :如果
QTableView/QListView/QTreeView的现有表现力仍不能满足需求(如脑图、甘特图),可以考虑继承QAbstractItemView实现完全自定义的视图。这时,你需要自己处理所有的绘制、布局、鼠标键盘事件,并与一个QItemSelectionModel交互。 - 大数据量优化 :对于海量数据(数万行),需要实现自定义模型,重写
canFetchMore/fetchMore进行懒加载。同时,确保代理的paint函数极其高效。可以考虑关闭动画、平滑滚动等特效。 - 上下文菜单 (Context Menu) :通常在主窗口或视图的
contextMenuEvent中处理。通过indexAt(event->pos())获取鼠标下的索引,再通过selectionModel()->selectedIndexes()获取当前选中项,从而决定菜单内容。 - 拖放支持 :需要在模型和视图上分别设置支持拖放的标志,并重写模型的
mimeData、dropMimeData等方法。QItemSelectionModel可以帮助你获取被拖动的选中项。
最后的核心建议 : QItemSelectionModel 和 QStyledItemDelegate 是 Qt Model/View 框架中用于增强交互和表现层的两大利器。理解它们最好的方式,就是像本文这样,从一个简单但完整的例子开始,先让基础功能跑通,然后逐步添加自定义的选择逻辑和绘制逻辑。在实际项目中,先把数据模型 ( QAbstractItemModel ) 设计扎实,再考虑视图和代理的定制,会让整个架构更清晰、更易维护。



1211

被折叠的 条评论
为什么被折叠?



