Qt Model/View框架实战:QItemSelectionModel与QStyledItemDelegate详解

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);
}

这个代理做了以下几件事:

  1. paint : 在“分数”列,不画文本,而是画一个 QProgressBar 。注意,我们使用了 QStyle 来绘制,这能保证外观与当前系统主题一致。
  2. createEditor : 当用户双击“分数”列准备编辑时,我们提供一个 QSpinBox (带上下箭头的数字输入框),并限制输入范围在 0-100。
  3. setEditorData / setModelData : 负责在编辑器打开时从模型加载数据,以及在编辑完成后将数据保存回模型。
  4. 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);
}

这个逻辑更清晰:

  1. 我们根据业务逻辑(分数<60)准备了一个背景画刷 ( opt.backgroundBrush )。
  2. 在绘制“分数”列时,我们手动判断:如果单元格被选中 ( opt.state & QStyle::State_Selected ),就绘制系统高亮色;否则,如果设置了条件背景,就绘制条件背景。然后在其上绘制进度条。
  3. 对于其他列(如姓名列),我们将包含条件背景信息的 opt 传递给基类的 paint 方法。基类方法会正确处理选中状态(绘制高亮)和未选中状态(绘制我们传入的条件背景)。

关键点 QStyleOptionViewItem state 成员包含了 QStyle::State_Selected 标志,这个标志是由视图的 QItemSelectionModel 维护并设置的。代理在绘制每个单元格时,视图会告诉它这个单元格是否被选中。这就是 SelectionModel 和 Delegate 的协作 :SelectionModel 管理“哪些被选中”的状态,Delegate 根据这个状态决定如何绘制。

5.3 实现行交替背景色与选择色的协调

Qt 视图本身支持行交替背景色 ( setAlternatingRowColors )。如果你同时开启了交替色、条件格式和选择高亮,绘制顺序和颜色叠加会变得复杂。一个稳妥的做法是:

  1. 让 Qt 处理交替背景色和默认选择色 :调用基类的 paint 方法。
  2. 在基类绘制之后,再叠加条件格式 :但这需要更复杂的绘制逻辑,可能要用到 painter->fillRect 并设置一定的透明度。

更常见的实践是: 如果使用了复杂的条件格式,就关闭系统的交替行颜色 ( setAlternatingRowColors(false) ),并在自定义代理中统一管理所有视觉表现

5.4 最终整合与测试

将更新后的代理设置到视图。现在运行程序,你应该能看到:

  1. 分数不及格的行,背景有浅红色提示。
  2. 选中某行时,该行显示系统高亮色(覆盖条件背景)。
  3. 分数列显示为进度条,并可点击编辑。
  4. 是否通过列有可点击的复选框。
  5. 在状态栏可以看到选中行的平均分。
  6. 通过按钮可以编程选中所有及格行。

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 进阶使用建议

  1. 自定义视图 :如果 QTableView / QListView / QTreeView 的现有表现力仍不能满足需求(如脑图、甘特图),可以考虑继承 QAbstractItemView 实现完全自定义的视图。这时,你需要自己处理所有的绘制、布局、鼠标键盘事件,并与一个 QItemSelectionModel 交互。
  2. 大数据量优化 :对于海量数据(数万行),需要实现自定义模型,重写 canFetchMore / fetchMore 进行懒加载。同时,确保代理的 paint 函数极其高效。可以考虑关闭动画、平滑滚动等特效。
  3. 上下文菜单 (Context Menu) :通常在主窗口或视图的 contextMenuEvent 中处理。通过 indexAt(event->pos()) 获取鼠标下的索引,再通过 selectionModel()->selectedIndexes() 获取当前选中项,从而决定菜单内容。
  4. 拖放支持 :需要在模型和视图上分别设置支持拖放的标志,并重写模型的 mimeData dropMimeData 等方法。 QItemSelectionModel 可以帮助你获取被拖动的选中项。

最后的核心建议 QItemSelectionModel QStyledItemDelegate 是 Qt Model/View 框架中用于增强交互和表现层的两大利器。理解它们最好的方式,就是像本文这样,从一个简单但完整的例子开始,先让基础功能跑通,然后逐步添加自定义的选择逻辑和绘制逻辑。在实际项目中,先把数据模型 ( QAbstractItemModel ) 设计扎实,再考虑视图和代理的定制,会让整个架构更清晰、更易维护。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值