Qt Model/View/Delegate 架构深度解析
目录
一、 核心设计思想:数据与表现的分离
二、 三大核心组件详解
1. Model (模型)
QModelIndex:模型的“临时指针”
Role (角色):数据的多面性
实现自定义模型
2. View (视图)
3. Delegate (委托)
自定义委托
三、 完整交互流程示例
四、具体使用场景示例:学生成绩管理
1. Model (StudentModel)
2. Delegate (ScoreDelegate)
3. View 及主程序 (main.cpp)
场景演示说明
一、 核心设计思想:数据与表现的分离
在图形用户界面(GUI)编程中,我们经常需要将底层数据以某种形式展示给用户,并允许用户交互和修改这些数据。传统的做法是将数据存储(如在一个 QListWidget
中)和其视觉表现耦合在一起。这种方式在简单应用中尚可,但随着数据量和复杂度的增加,会带来诸多问题:
-
灵活性差: 如果想用不同方式(例如,既用列表又用表格)展示同一份数据,就需要维护多份数据副本,导致数据同步困难和内存浪费。
-
性能瓶颈: 对于海量数据,一次性加载所有数据到视图控件中,会造成巨大的内存开销和启动延迟。
-
扩展性弱: 自定义数据的显示方式和编辑方式非常困难,通常需要重写大量控件代码。
为了解决这些问题,Qt 引入了 模型/视图/委托(Model/View/Delegate) 架构。其核心思想是 将数据与表现彻底分离:
-
Model (模型): 唯一的数据源。它负责存储和管理数据,并提供一个标准接口供外界访问。它对数据如何被展示一无所有。
-
View (视图): 数据的“皮肤”。它负责以各种形式(列表、表格、树等)将模型中的数据可视化地呈现出来。视图本身不存储数据,它只是一个数据的观察口。
-
Delegate (委托): 渲染和编辑的“画笔”与“编辑器”。它负责控制数据项在视图中如何被绘制以及如何被编辑。
这种架构带来了巨大的优势:
-
一份数据,多种展示: 同一个模型可以被多个不同的视图(
QListView
,QTableView
,QTreeView
)同时使用,任何对模型的修改都会自动、实时地反映在所有视图上。 -
高性能: 视图只向模型请求当前可见区域的数据,实现了数据的按需加载,即使处理数百万条记录也能保持流畅。
-
高度可定制: 通过自定义委托,可以完全控制每一个数据项的渲染和编辑方式,例如用进度条显示百分比、用下拉框编辑枚举值等。
二、 三大核心组件详解
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::Checked
或Qt::Unchecked
)。 -
Qt::UserRole
: 用于自定义角色,方便存储和传递业务逻辑相关的数据。
实现自定义模型
Qt 提供了几个便利的子类来简化模型开发:
-
QAbstractListModel
: 用于一维的列表数据结构(如QStringList
或QVector
)。 -
QAbstractTableModel
: 用于二维的表格数据结构(如二维数组或QVector<QVector<T>>
)。 -
QAbstractItemModel
: 用于更复杂的层次化(树形)数据结构。
要创建一个自定义模型,你需要继承这些类并至少实现以下几个核心的纯虚函数:
-
rowCount(const QModelIndex &parent = QModelIndex()) const
: 返回指定父项下的行数。对于列表和表格模型,父项总是无效的根索引。 -
columnCount(const QModelIndex &parent = QModelIndex()) const
: 返回指定父项下的列数。对于列表模型,恒为1。 -
data(const QModelIndex &index, int role = Qt::DisplayRole) const
: 最重要的函数。返回指定索引和角色对应的数据。视图会频繁调用此函数来获取要显示的内容。 -
headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const
: 返回表头或行头的数据。 -
flags(const QModelIndex &index) const
: 返回数据项的标志,如是否可选 (Qt::ItemIsSelectable
)、是否可编辑 (Qt::ItemIsEditable
) 等。 -
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
并重写以下关键函数,可以实现强大的自定义功能:
-
paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
:-
核心渲染函数。所有绘制逻辑都在这里实现。
-
painter
: 绘图工具。 -
option
: 包含了绘制所需的所有信息,如矩形区域 (option.rect
)、状态 (option.state
,如是否被选中、鼠标是否悬停)。 -
index
: 当前要绘制的数据项的模型索引,可以通过index.data(role)
从模型获取数据。 -
示例: 在单元格中绘制一个进度条来显示完成度。
-
-
createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
:-
当用户触发编辑操作时(如双击),视图会调用此函数来创建一个编辑控件(
QWidget
)。 -
示例: 为一个表示优先级的列创建一个
QComboBox
编辑器。
-
-
setEditorData(QWidget *editor, const QModelIndex &index) const
:-
在编辑器创建后,此函数被调用,用于将模型中的当前数据设置到编辑器上。
-
示例: 将模型中的 "高" 字符串,设置为
QComboBox
编辑器的当前选中项。
-
-
setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
:-
当用户完成编辑后,此函数被调用,用于从编辑器中取出修改后的数据,并通过
model->setData()
将其写回模型。 -
示例: 获取
QComboBox
当前选中的文本,并更新到模型中。
-
-
sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
:-
返回该数据项的最佳尺寸,用于视图的布局。
-
将自定义委托应用到视图上:
MyCustomDelegate* delegate = new MyCustomDelegate(this);
tableView->setItemDelegate(delegate); // 应用到所有列
// 或者应用到特定列
// tableView->setItemDelegateForColumn(1, delegate);
三、 完整交互流程示例
让我们通过一个“编辑表格”的完整流程来梳理三者之间的协作:
-
显示阶段
-
QTableView
向MyModel
请求行数和列数 (rowCount
,columnCount
) 来确定整体布局。 -
QTableView
计算出当前可见的单元格范围。 -
对于每一个可见的单元格,
QTableView
向MyModel
请求数据:model->data(index, Qt::DisplayRole)
。 -
QTableView
将获取到的数据和样式信息传递给MyDelegate
。 -
MyDelegate
的paint()
函数被调用,它根据数据和状态(如是否选中)将内容绘制到单元格上。
-
-
编辑阶段
-
用户双击了某个单元格。
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)
。
-
-
数据更新与通知
-
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_OBJECTpublic: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 modelvoid 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 {// 为分数列创建 QSpinBoxauto *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
实例。
如何体现“一份数据,多种展示”?
-
数据同步: 两个视图都准确地反映了模型中的初始数据。
-
实时更新: 在右侧的表格视图中,双击李四的成绩“58”并将其修改为“95”。当您完成编辑后,您会看到表格中的分数更新,并且颜色从红色变为默认的黑色。虽然左侧的列表视图没有显示分数,但底层的模型数据确实已经被修改了。如果我们在模型中添加一个新的学生,两个视图会同时出现新的条目。
-
独立表现: 表格可以有表头、多列和复杂的委托,而列表只是简单地展示一列。它们各自独立地向同一个模型请求数据,并以自己的方式进行渲染,完美地将**数据存储(Model)与数据表现(View)**分离开来。