第十七章:QT模型/视图架构——MVC模式、QAbstractItemModel、QListView、QTableView、QTreeView、自定义模型
各位同学,今天我们来聊聊QT里一个非常核心的设计模式——模型/视图架构。说实话,我刚接触QT那会儿,看到MVC这三个字母就头大。总觉得这是Java或者Web开发才用的东西,跟桌面开发有什么关系?直到我在一个项目中,需要同时用表格、列表、树形三种方式展示同一份数据,才真正体会到这个架构的威力。
17.1 什么是MVC模式?
MVC,全称Model-View-Controller。说白了,就是把数据、显示、交互这三件事拆开干。
- Model(模型):负责管理数据。它不关心数据怎么显示,只关心数据怎么存、怎么取。
- View(视图):负责展示数据。它不关心数据从哪来,只关心怎么画出来。
- Controller(控制器):负责处理用户交互。在QT里,这部分通常被View自己消化了,或者通过Delegate(委托)来实现。
你想想看,如果数据、显示、交互全揉在一起,改一个地方就得动整个代码。我早期写QT程序时,就犯过这个错——一个QTableWidget里塞了上千行数据,每次刷新都卡得要命。后来改成模型/视图架构,性能直接起飞。
核心思想:模型只存数据,视图只负责画,两者通过信号/槽机制通信。数据变了,模型发信号通知视图刷新;用户操作了,视图发信号通知模型更新。
17.2 QT的模型/视图架构
QT把MVC稍微改了一下,变成了模型/视图/委托(Model/View/Delegate)架构。委托负责编辑和渲染细节,相当于把Controller的一部分职责拆了出来。
我画了一张图,帮你理清它们之间的关系:
17.3 QAbstractItemModel——一切模型的基类
QT里所有模型都继承自QAbstractItemModel。这个类定义了一套纯虚接口,你必须实现它们才能让模型正常工作。
核心接口就这几个:
| 接口 | 作用 | 必须实现? |
|---|---|---|
index() |
根据行、列、父索引创建模型索引 | 是 |
parent() |
返回给定索引的父索引 | 是 |
rowCount() |
返回给定父索引下的行数 | 是 |
columnCount() |
返回给定父索引下的列数 | 是 |
data() |
返回指定索引处指定角色的数据 | 是 |
setData() |
设置指定索引处指定角色的数据 | 否(可编辑时需要) |
flags() |
返回指定索引的项标志(是否可编辑、可选等) | 否 |
headerData() |
返回表头数据 | 否 |
我的经验:刚开始写自定义模型时,最容易搞错的是index()和parent()的对应关系。尤其是树形结构,父索引搞错了,视图里显示的数据就全乱了。我建议你先从简单的列表模型开始练手,再逐步过渡到表格和树形。
17.4 三种常用视图
QT提供了三个现成的视图类,覆盖了最常见的展示需求:
- QListView:列表视图。适合展示一列数据,比如文件列表、联系人列表。
- QTableView:表格视图。适合展示二维数据,比如Excel表格、数据库查询结果。
- QTreeView:树形视图。适合展示层级数据,比如文件系统、组织架构。
这三个视图的使用方式几乎一模一样:
// 创建模型
QStandardItemModel *model = new QStandardItemModel(4, 2, this);
model->setHeaderData(0, Qt::Horizontal, "姓名");
model->setHeaderData(1, Qt::Horizontal, "年龄");
// 填充数据
model->setItem(0, 0, new QStandardItem("张三"));
model->setItem(0, 1, new QStandardItem("28"));
model->setItem(1, 0, new QStandardItem("李四"));
model->setItem(1, 1, new QStandardItem("32"));
// 创建视图并设置模型
QTableView *tableView = new QTableView;
tableView->setModel(model);
tableView->show();
你看,代码就这么几行。视图和模型通过setModel()绑定,之后所有数据展示和交互都由框架自动处理。
注意:千万不要把QTableWidget和QTableView搞混了。QTableWidget是视图+模型的合体版,适合小数据量快速开发。而QTableView配合自定义模型,才是处理大数据量的正确姿势。我曾经在一个项目中用QTableWidget加载了10万行数据,界面直接卡死。换成QTableView+自定义模型后,丝滑流畅。
17.5 自定义模型——从零开始写一个
光用现成的QStandardItemModel肯定不够。真正的大项目,你需要自己写模型。下面我带你手写一个简单的只读列表模型。
class MyListModel : public QAbstractListModel
{
Q_OBJECT
public:
explicit MyListModel(const QStringList &data, QObject *parent = nullptr)
: QAbstractListModel(parent), m_data(data) {}
int rowCount(const QModelIndex &parent = QModelIndex()) const override
{
// 顶层节点才返回数据行数
if (parent.isValid())
return 0;
return m_data.size();
}
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
{
if (!index.isValid())
return QVariant();
if (index.row() >= m_data.size() || index.row() < 0)
return QVariant();
if (role == Qt::DisplayRole)
return m_data.at(index.row());
return QVariant();
}
private:
QStringList m_data;
};
使用起来也很简单:
QStringList list;
list << "苹果" << "香蕉" << "橘子" << "西瓜";
MyListModel *model = new MyListModel(list, this);
QListView *view = new QListView;
view->setModel(model);
view->show();
嗯,这里要注意:QAbstractListModel是QAbstractItemModel的简化版,专门为一维列表准备的。如果你需要表格或树形,直接继承QAbstractItemModel会更灵活。
17.6 模型索引——理解QModelIndex
模型索引是连接模型和视图的桥梁。它本质上是一个轻量级对象,包含了数据的位置信息:行、列、父索引、内部指针。
为什么不用行号列号直接访问数据?因为树形结构里,每个节点的位置是相对的。比如第二行第一列,在根节点下和在某子节点下,含义完全不同。模型索引通过内部指针和父索引,唯一确定了一个数据项。
我刚开始学的时候,总搞不清QModelIndex()和无效索引的区别。记住一句话:无效索引代表根节点。在树形模型里,根节点是不可见的,它的子节点就是第一层节点。
17.7 角色(Role)——数据的不同面孔
同一个数据,在不同场景下需要不同的表现形式。比如一个名字,在列表里显示文本,在编辑框里显示可编辑文本,在工具提示里显示完整信息。QT通过角色(Role)来区分这些需求。
常用的角色有:
| 角色 | 用途 |
|---|---|
Qt::DisplayRole |
显示的文本 |
Qt::EditRole |
编辑时的数据 |
Qt::ToolTipRole |
工具提示文本 |
Qt::DecorationRole |
图标 |
Qt::ForegroundRole |
前景色(文字颜色) |
Qt::BackgroundRole |
背景色 |
Qt::FontRole |
字体 |
Qt::TextAlignmentRole |
文字对齐方式 |
Qt::CheckStateRole |
复选框状态 |
在data()函数里,根据不同的角色返回不同的数据:
QVariant data(const QModelIndex &index, int role) const override
{
if (!index.isValid())
return QVariant();
if (role == Qt::DisplayRole)
return m_data.at(index.row()).name;
else if (role == Qt::DecorationRole)
return m_data.at(index.row()).icon;
else if (role == Qt::ToolTipRole)
return m_data.at(index.row()).description;
else if (role == Qt::ForegroundRole)
return m_data.at(index.row()).isValid ? QColor(Qt::black) : QColor(Qt::red);
return QVariant();
}
避坑指南:我曾经在data()里忘了处理Qt::EditRole,结果视图里双击编辑时,编辑框里一片空白。记住:如果模型是可编辑的,DisplayRole和EditRole最好都返回数据,或者让EditRole返回原始数据,DisplayRole返回格式化后的数据。
17.8 实战建议
说了这么多,最后给你几条实战建议:
- 从小做起:先写一个只读的列表模型,跑通了再扩展成表格或树形。
- 善用调试:模型/视图架构的bug很难肉眼发现。我习惯在
data()和rowCount()里加qDebug,看看视图到底在请求什么数据。 - 注意性能:
data()会被频繁调用,千万别在里面做耗时操作。如果数据需要计算,提前算好缓存起来。 - 信号/槽别忘:数据变化时,记得发射
dataChanged()、rowsInserted()等信号,否则视图不会刷新。
模型/视图架构是QT里最优雅的设计之一。刚开始可能觉得绕,但一旦用顺手了,你会发现它让代码结构清晰了不止一个档次。好了,这一章就到这里,去写代码吧。
公众号:蓝海资料掘金营,微信deep3321