Qt Model/View/Delegate 架构深度解析

目录

[一、 核心设计思想:数据与表现的分离](#一、 核心设计思想:数据与表现的分离)

[二、 三大核心组件详解](#二、 三大核心组件详解)

[1. Model (模型)](#1. Model (模型))

QModelIndex:模型的"临时指针"

[Role (角色):数据的多面性](#Role (角色):数据的多面性)

实现自定义模型

[2. View (视图)](#2. View (视图))

[3. Delegate (委托)](#3. Delegate (委托))

自定义委托

[三、 完整交互流程示例](#三、 完整交互流程示例)

四、具体使用场景示例:学生成绩管理

[1. Model (StudentModel)](#1. Model (StudentModel))

[2. Delegate (ScoreDelegate)](#2. Delegate (ScoreDelegate))

[3. View 及主程序 (main.cpp)](#3. View 及主程序 (main.cpp))

场景演示说明


一、 核心设计思想:数据与表现的分离

在图形用户界面(GUI)编程中,我们经常需要将底层数据以某种形式展示给用户,并允许用户交互和修改这些数据。传统的做法是将数据存储(如在一个 QListWidget 中)和其视觉表现耦合在一起。这种方式在简单应用中尚可,但随着数据量和复杂度的增加,会带来诸多问题:

  • 灵活性差: 如果想用不同方式(例如,既用列表又用表格)展示同一份数据,就需要维护多份数据副本,导致数据同步困难和内存浪费。

  • 性能瓶颈: 对于海量数据,一次性加载所有数据到视图控件中,会造成巨大的内存开销和启动延迟。

  • 扩展性弱: 自定义数据的显示方式和编辑方式非常困难,通常需要重写大量控件代码。

为了解决这些问题,Qt 引入了 模型/视图/委托(Model/View/Delegate) 架构。其核心思想是 将数据与表现彻底分离

  • Model (模型): 唯一的数据源。它负责存储和管理数据,并提供一个标准接口供外界访问。它对数据如何被展示一无所有。

  • View (视图): 数据的"皮肤"。它负责以各种形式(列表、表格、树等)将模型中的数据可视化地呈现出来。视图本身不存储数据,它只是一个数据的观察口。

  • Delegate (委托): 渲染和编辑的"画笔"与"编辑器"。它负责控制数据项在视图中如何被绘制以及如何被编辑。

这种架构带来了巨大的优势:

  1. 一份数据,多种展示: 同一个模型可以被多个不同的视图(QListView, QTableView, QTreeView)同时使用,任何对模型的修改都会自动、实时地反映在所有视图上。

  2. 高性能: 视图只向模型请求当前可见区域的数据,实现了数据的按需加载,即使处理数百万条记录也能保持流畅。

  3. 高度可定制: 通过自定义委托,可以完全控制每一个数据项的渲染和编辑方式,例如用进度条显示百分比、用下拉框编辑枚举值等。

二、 三大核心组件详解

1. Model (模型)

模型是整个架构的核心,是所有数据的来源。Qt 提供了 QAbstractItemModel 作为所有模型的基类接口。

QModelIndex:模型的"临时指针"

在与模型交互时,我们不直接操作数据的指针,而是通过 QModelIndex。可以将其理解为一个临时的、轻量级的"坐标",用于定位模型中的某一个数据项。它包含三个关键信息:行(row)列(column)父项的 QModelIndex(用于树形结构)。

关键点: QModelIndex 是由模型按需创建的,并且是暂时的。不应该存储 QModelIndex 并在之后使用,因为模型结构可能已经改变。

Role (角色):数据的多面性

同一个数据项可能有多种呈现方式。例如,"一个文件"这个数据项,可以有文件名(文本)、文件图标(图像)、文件大小(工具提示)、是否可写(可编辑状态)等多种信息。模型通过 角色(Role) 来区分这些信息。

当视图向模型请求数据时,它会同时提供一个 QModelIndex 和一个 role。常见的角色有:

  • Qt::DisplayRole: 显示的文本(如 QString)。

  • Qt::EditRole: 在编辑器中显示的文本,通常与 DisplayRole 相同。

  • Qt::DecorationRole: 以图标形式显示的装饰(如 QIcon, QPixmap)。

  • Qt::ToolTipRole: 鼠标悬停时显示的工具提示文本。

  • Qt::CheckStateRole: 复选框的状态(Qt::CheckedQt::Unchecked)。

  • Qt::UserRole: 用于自定义角色,方便存储和传递业务逻辑相关的数据。

实现自定义模型

Qt 提供了几个便利的子类来简化模型开发:

  • QAbstractListModel : 用于一维的列表数据结构(如 QStringListQVector)。

  • QAbstractTableModel : 用于二维的表格数据结构(如二维数组或 QVector<QVector<T>>)。

  • QAbstractItemModel: 用于更复杂的层次化(树形)数据结构。

要创建一个自定义模型,你需要继承这些类并至少实现以下几个核心的纯虚函数:

  1. rowCount(const QModelIndex &parent = QModelIndex()) const: 返回指定父项下的行数。对于列表和表格模型,父项总是无效的根索引。

  2. columnCount(const QModelIndex &parent = QModelIndex()) const: 返回指定父项下的列数。对于列表模型,恒为1。

  3. data(const QModelIndex &index, int role = Qt::DisplayRole) const : 最重要的函数。返回指定索引和角色对应的数据。视图会频繁调用此函数来获取要显示的内容。

  4. headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const: 返回表头或行头的数据。

  5. flags(const QModelIndex &index) const : 返回数据项的标志,如是否可选 (Qt::ItemIsSelectable)、是否可编辑 (Qt::ItemIsEditable) 等。

  6. setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) : 当用户通过视图修改数据时被调用。在此函数中,你需要更新底层数据结构,并在成功后发射 dataChanged() 信号通知所有视图更新。

信号与槽: 当底层数据发生改变时(增、删、改),模型 必须 发射相应的信号(如 dataChanged(), rowsInserted(), rowsRemoved()),以便所有关联的视图能够及时刷新。

2. View (视图)

视图负责将模型中的数据呈现给用户。Qt 提供了三种主要的视图类:

  • QListView: 以单列列表的形式展示数据。

  • QTableView: 以多行多列的表格形式展示数据。

  • QTreeView: 以可展开和折叠的树形结构展示层次化数据。

将视图与模型关联起来非常简单,只需调用 setModel() 函数:

复制代码
MyCustomModel* model = new MyCustomModel(this);
QTableView* tableView = new QTableView(this);
tableView->setModel(model);

从此,tableView 就会自动从 model 中拉取数据并展示出来。视图还负责处理用户的选择操作,通过 QItemSelectionModel 来管理。

3. Delegate (委托)

如果说模型是"骨骼",视图是"皮肤",那么委托就是"化妆师"。它负责精细地控制每个数据项的 外观(如何绘制)行为(如何编辑)

默认情况下,视图会使用一个 QStyledItemDelegate 的实例,它可以处理常见的数据类型(QString, int, bool 等)。但当你需要更复杂的渲染或编辑时,就需要自定义委托。

自定义委托

通过继承 QStyledItemDelegate 并重写以下关键函数,可以实现强大的自定义功能:

  1. paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const:

    • 核心渲染函数。所有绘制逻辑都在这里实现。

    • painter: 绘图工具。

    • option: 包含了绘制所需的所有信息,如矩形区域 (option.rect)、状态 (option.state,如是否被选中、鼠标是否悬停)。

    • index: 当前要绘制的数据项的模型索引,可以通过 index.data(role) 从模型获取数据。

    • 示例: 在单元格中绘制一个进度条来显示完成度。

  2. createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const:

    • 当用户触发编辑操作时(如双击),视图会调用此函数来创建一个编辑控件(QWidget)。

    • 示例: 为一个表示优先级的列创建一个 QComboBox 编辑器。

  3. setEditorData(QWidget *editor, const QModelIndex &index) const:

    • 在编辑器创建后,此函数被调用,用于将模型中的当前数据设置到编辑器上。

    • 示例: 将模型中的 "高" 字符串,设置为 QComboBox 编辑器的当前选中项。

  4. setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const:

    • 当用户完成编辑后,此函数被调用,用于从编辑器中取出修改后的数据,并通过 model->setData() 将其写回模型。

    • 示例: 获取 QComboBox 当前选中的文本,并更新到模型中。

  5. sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const:

    • 返回该数据项的最佳尺寸,用于视图的布局。

将自定义委托应用到视图上:

复制代码
MyCustomDelegate* delegate = new MyCustomDelegate(this);
tableView->setItemDelegate(delegate); // 应用到所有列
// 或者应用到特定列
// tableView->setItemDelegateForColumn(1, delegate);

三、 完整交互流程示例

让我们通过一个"编辑表格"的完整流程来梳理三者之间的协作:

  1. 显示阶段

    • QTableViewMyModel 请求行数和列数 (rowCount, columnCount) 来确定整体布局。

    • QTableView 计算出当前可见的单元格范围。

    • 对于每一个可见的单元格,QTableViewMyModel 请求数据:model->data(index, Qt::DisplayRole)

    • QTableView 将获取到的数据和样式信息传递给 MyDelegate

    • MyDelegatepaint() 函数被调用,它根据数据和状态(如是否选中)将内容绘制到单元格上。

  2. 编辑阶段

    • 用户双击了某个单元格。QTableView 检测到这个操作。

    • QTableView 首先向 MyModel 查询该单元格的标志位:model->flags(index),确认 Qt::ItemIsEditable 标志已设置。

    • QTableView 请求 MyDelegate 创建一个编辑器:delegate->createEditor(...)。假设它返回了一个 QLineEdit

    • QTableView 请求 MyDelegate 将模型数据同步到编辑器:delegate->setEditorData(...)。这会调用 lineEdit->setText(model->data(index, Qt::EditRole))

    • QLineEdit 编辑器显示在单元格上,用户输入新内容。

    • 用户完成编辑(如按下回车键)。

    • QTableView 请求 MyDelegate 将编辑器数据写回模型:delegate->setModelData(...)

    • setModelData 内部,调用 model->setData(index, lineEdit->text(), Qt::EditRole)

  3. 数据更新与通知

    • MyModel 在其 setData() 函数中更新内部存储的数据。

    • 数据更新成功后,MyModel 必须 发射信号:emit dataChanged(index, index)

    • 所有连接到此模型的视图(包括当前的 QTableView)都会收到 dataChanged 信号。

    • QTableView 知道该 index 对应的数据已变更,于是它会重新请求该 index 的数据并让委托重绘它,完成界面刷新。

通过这个流程,数据、显示和编辑逻辑被完美地解耦,每一个组件都只关心自己的职责,共同协作完成复杂的任务。掌握模型/视图/委托编程是精通 Qt GUI 开发的关键一步。

四、具体使用场景示例:学生成绩管理

我们将通过一个具体的例子来演示如何使用 Model/View/Delegate 架构。

场景需求:

  • 创建一个应用程序,用表格显示学生名单。

  • 表格包含三列:姓名(只读)、科目(只读)、分数(可编辑)。

  • 分数列只能接受 0-100 之间的整数。

  • 分数低于 60 分的单元格,文本颜色显示为红色。

这个需求完美地对应了 M/V/D 的三个组件:

  • Model: 负责存储和管理学生数据(姓名、科目、分数)。

  • View : 使用 QTableView 来展示数据。

  • Delegate : 自定义一个委托来处理分数列的特殊渲染(红色文本)和编辑方式(QSpinBox)。

1. Model (StudentModel)

首先,定义一个简单的数据结构来存储学生信息,然后创建继承自 QAbstractTableModel 的模型。

student.h (数据结构)

复制代码
#pragma once
#include <QString>

struct Student {
    QString name;
    QString subject;
    int score;
};

studentmodel.h (模型头文件)

复制代码
#pragma once
#include <QAbstractTableModel>
#include <QVector>
#include "student.h"

class StudentModel : public QAbstractTableModel {
    Q_OBJECT

public:
    explicit StudentModel(QObject *parent = nullptr);

    // Header:
    QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;

    // Basic functionality:
    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;

    // Editable:
    bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
    Qt::ItemFlags flags(const QModelIndex &index) const override;

    // Add data to model
    void addStudent(const Student& student);

private:
    QVector<Student> m_students;
};

studentmodel.cpp (模型实现)

复制代码
#include "studentmodel.h"
#include <QColor>

StudentModel::StudentModel(QObject *parent)
    : QAbstractTableModel(parent) {}

QVariant StudentModel::headerData(int section, Qt::Orientation orientation, int role) const {
    if (role == Qt::DisplayRole && orientation == Qt::Horizontal) {
        switch (section) {
            case 0: return QString("姓名");
            case 1: return QString("科目");
            case 2: return QString("分数");
        }
    }
    return QVariant();
}

int StudentModel::rowCount(const QModelIndex &parent) const {
    if (parent.isValid()) return 0;
    return m_students.count();
}

int StudentModel::columnCount(const QModelIndex &parent) const {
    if (parent.isValid()) return 0;
    return 3;
}

QVariant StudentModel::data(const QModelIndex &index, int role) const {
    if (!index.isValid()) return QVariant();

    const Student &student = m_students.at(index.row());
    
    // 用于显示和编辑的数据
    if (role == Qt::DisplayRole || role == Qt::EditRole) {
        switch (index.column()) {
            case 0: return student.name;
            case 1: return student.subject;
            case 2: return student.score;
        }
    }
    
    // 用于分数列 < 60 时设置前景色为红色
    if (role == Qt::ForegroundRole && index.column() == 2) {
        if (student.score < 60) {
            return QColor(Qt::red);
        }
    }

    return QVariant();
}

bool StudentModel::setData(const QModelIndex &index, const QVariant &value, int role) {
    if (role == Qt::EditRole) {
        if (!checkIndex(index)) return false;

        // 只允许修改分数
        if (index.column() == 2) {
            m_students[index.row()].score = value.toInt();
            emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole});
            return true;
        }
    }
    return false;
}

Qt::ItemFlags StudentModel::flags(const QModelIndex &index) const {
    if (!index.isValid()) return Qt::NoItemFlags;
    
    // 默认标志
    Qt::ItemFlags defaultFlags = QAbstractTableModel::flags(index);
    
    // 只有分数列是可编辑的
    if (index.column() == 2) {
        return defaultFlags | Qt::ItemIsEditable;
    }
    
    return defaultFlags;
}

void StudentModel::addStudent(const Student &student)
{
    beginInsertRows(QModelIndex(), rowCount(), rowCount());
    m_students.append(student);
    endInsertRows();
}

2. Delegate (ScoreDelegate)

现在,为分数列创建一个委托,使用 QSpinBox 作为编辑器,并限制范围为 0-100。

注意:对于本例中简单的颜色变化,可以直接在 Model 的 data() 函数中处理 Qt::ForegroundRole 来实现,这更简单。但为了演示委托的 paint 功能,我们也可以在委托中绘制。此处采用更简单的 Model 方式。如果需要更复杂的绘制(如绘制进度条),则必须使用委托。

scoredelegate.h (委托头文件)

复制代码
#pragma once
#include <QStyledItemDelegate>

class ScoreDelegate : public QStyledItemDelegate {
    Q_OBJECT
public:
    explicit ScoreDelegate(QObject *parent = nullptr);

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

scoredelegate.cpp (委托实现)

复制代码
#include "scoredelegate.h"
#include <QSpinBox>

ScoreDelegate::ScoreDelegate(QObject *parent)
    : QStyledItemDelegate(parent) {}

QWidget *ScoreDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const {
    // 为分数列创建 QSpinBox
    auto *editor = new QSpinBox(parent);
    editor->setFrame(false);
    editor->setMinimum(0);
    editor->setMaximum(100);
    return editor;
}

void ScoreDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const {
    // 将模型数据设置到编辑器
    int value = index.model()->data(index, Qt::EditRole).toInt();
    auto *spinBox = static_cast<QSpinBox*>(editor);
    spinBox->setValue(value);
}

void ScoreDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const {
    // 将编辑器数据写回模型
    auto *spinBox = static_cast<QSpinBox*>(editor);
    spinBox->interpretText();
    int value = spinBox->value();
    model->setData(index, value, Qt::EditRole);
}

void ScoreDelegate::updateEditorGeometry(QWidget *editor, const QStyleOptionViewItem &option, const QModelIndex &index) const {
    // 设置编辑器的位置和大小
    editor->setGeometry(option.rect);
}

3. View 及主程序 (main.cpp)

最后,在主函数中把所有组件组装起来。

复制代码
#include <QApplication>
#include <QTableView>
#include <QListView>
#include <QWidget>
#include <QHBoxLayout>
#include "studentmodel.h"
#include "scoredelegate.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    
    // 创建一个主窗口和布局
    QWidget window;
    QHBoxLayout *layout = new QHBoxLayout(&window);

    // 1. 创建模型 (Model) - 这是唯一的数据源
    StudentModel studentModel;
    studentModel.addStudent({"张三", "数学", 92});
    studentModel.addStudent({"李四", "数学", 58});
    studentModel.addStudent({"王五", "英语", 85});
    studentModel.addStudent({"赵六", "英语", 45});

    // 2. 创建第一个视图 (View 1: QTableView)
    QTableView *tableView = new QTableView;
    tableView->setModel(&studentModel); // 将模型设置到表格视图

    // 3. 创建第二个视图 (View 2: QListView)
    QListView *listView = new QListView;
    listView->setModel(&studentModel); // 将同一个模型设置到列表视图

    // 将视图添加到布局
    layout->addWidget(listView);
    layout->addWidget(tableView);
    
    // 4. 创建并设置委托 (只对 TableView 的特定列)
    ScoreDelegate *scoreDelegate = new ScoreDelegate(&window);
    tableView->setItemDelegateForColumn(2, scoreDelegate);
    
    window.setWindowTitle("一份数据,多种展示");
    window.resize(600, 300);
    window.show();

    return a.exec();
}
场景演示说明

上述代码运行后,您会看到一个窗口,其中包含两个并排的视图:

  • 左侧是一个列表 (QListView): 它默认只显示模型中第一列的数据,也就是所有学生的姓名。

  • 右侧是一个表格 (QTableView): 它完整地显示了模型中的所有数据(姓名、科目、分数)。

这两个视图虽然外观和功能完全不同,但它们共享着同一个 StudentModel 实例

如何体现"一份数据,多种展示"?

  1. 数据同步: 两个视图都准确地反映了模型中的初始数据。

  2. 实时更新 : 在右侧的表格视图 中,双击李四的成绩"58"并将其修改为"95"。当您完成编辑后,您会看到表格中的分数更新,并且颜色从红色变为默认的黑色。虽然左侧的列表视图 没有显示分数,但底层的模型数据确实已经被修改了。如果我们在模型中添加一个新的学生,两个视图会同时出现新的条目。

  3. 独立表现 : 表格可以有表头、多列和复杂的委托,而列表只是简单地展示一列。它们各自独立地向同一个模型请求数据,并以自己的方式进行渲染,完美地将**数据存储(Model)数据表现(View)**分离开来。

相关推荐
xiaoxiao无脸男2 小时前
three.js
开发语言·前端·javascript
Coovally AI模型快速验证2 小时前
华为发布开源超节点架构,以开放战略叩响AI算力生态变局
人工智能·深度学习·神经网络·计算机视觉·华为·架构·开源
hnlgzb3 小时前
安卓中,kotlin如何写app界面?
android·开发语言·kotlin
Z_z在努力3 小时前
【MySQL 高阶】MySQL 架构与存储引擎全面详解
数据库·mysql·架构
一叶飘零_sweeeet3 小时前
极简 Go 语言教程:从 Java 开发者视角 3 小时入门实战
java·开发语言·golang
BUTCHER53 小时前
Go语言环境安装
linux·开发语言·golang
失散133 小时前
分布式专题——21 Kafka客户端消息流转流程
java·分布式·云原生·架构·kafka
花心蝴蝶.3 小时前
JVM 类加载
开发语言·jvm·后端
_OP_CHEN4 小时前
C++:(四)类和对象(中)—— 构造、析构与重载
开发语言·c++·类和对象·构造函数·析构函数·运算符重载·日期类