22、Qt模型/视图编程:MVC模式简介、QAbstractListModel、QStandardItemModel、QSortFilterProxyModel
模型/视图编程,说白了就是Qt里处理数据和界面显示的一套经典套路。我刚开始学Qt那会儿,总觉得这东西绕来绕去,不如直接往控件里塞数据来得痛快。后来项目越做越大,才发现没有模型/视图,代码根本没法维护。
今天我们就来聊聊MVC模式在Qt里的落地,以及三个最常用的模型类:QAbstractListModel、QStandardItemModel和QSortFilterProxyModel。
MVC模式:数据与显示的“中间人”
MVC全称Model-View-Controller,在Qt里稍微变了一下,变成了Model-View-Delegate。不过核心思想没变:数据(Model)和显示(View)彻底分离。
你想想看,如果没有MVC,你写一个表格,数据直接塞进表格控件里。哪天想换数据源了,得把整个表格重写一遍。有了MVC,数据只管存,视图只管画,中间通过模型接口沟通。改数据源?换个模型就行,视图不用动。
核心关系:
- Model(模型):负责存储和管理数据,提供访问接口
- View(视图):负责数据的可视化呈现,如列表、表格、树
- Delegate(委托):负责数据的编辑和绘制细节
我个人习惯把Model想象成一个“数据仓库”,View是“展示窗口”,Delegate是“装修师傅”。仓库里有什么,窗口就展示什么,装修师傅决定怎么摆好看。
QAbstractListModel:自己动手,丰衣足食
QAbstractListModel是Qt提供的一个抽象基类,专门用来实现一维列表模型。说白了,它给你搭好了架子,你只需要填几个关键函数就行。
为什么要用这个?因为有时候你的数据不是简单的字符串列表,可能是自定义的结构体、对象,甚至是从数据库查出来的记录。这时候QAbstractListModel就派上用场了。
我的经验:我在项目中遇到过需要显示设备列表的场景,每个设备有ID、名称、状态、IP地址四个字段。用QAbstractListModel封装后,视图层只需要绑定模型,数据源从文件换到数据库,视图代码一行没改。
实现一个自定义模型,最少需要重写三个函数:
class DeviceListModel : public QAbstractListModel
{
Q_OBJECT
public:
// 必须重写:返回行数
int rowCount(const QModelIndex &parent = QModelIndex()) const override
{
Q_UNUSED(parent);
return m_devices.size();
}
// 必须重写:返回指定位置的数据
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
{
if (!index.isValid())
return QVariant();
const Device &dev = m_devices.at(index.row());
if (role == Qt::DisplayRole) {
return dev.name; // 显示名称
} else if (role == Qt::ToolTipRole) {
return dev.ipAddress; // 悬停显示IP
} else if (role == Qt::UserRole) {
return dev.id; // 自定义角色存ID
}
return QVariant();
}
// 可选但常用:返回表头数据
QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override
{
if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
return QStringLiteral("设备列表");
}
return QVariant();
}
private:
QList<Device> m_devices;
};
嗯,这里要注意:data()函数里的role参数很关键。Qt用不同的角色来区分数据的用途——显示用DisplayRole,图标用DecorationRole,字体颜色用ForegroundRole。你可以在同一个位置返回不同角色的数据。
避坑指南:我曾经在data()里忘记检查index.isValid(),结果程序时不时崩溃。后来养成习惯:所有模型接口第一行先判断索引有效性。另外,rowCount()里如果parent是有效索引,应该返回0(因为列表没有子节点)。
QStandardItemModel:开箱即用的通用模型
如果你不想自己写模型,或者数据比较简单,QStandardItemModel就是你的好朋友。它就像一个“万能数据容器”,表格、列表、树都能用。
我刚开始用Qt时,最喜欢的就是这个类——不用继承,不用重写,直接new出来就能用。
// 创建一个5行3列的表格模型
QStandardItemModel *model = new QStandardItemModel(5, 3, this);
// 设置表头
model->setHorizontalHeaderLabels({QStringLiteral("姓名"),
QStringLiteral("年龄"),
QStringLiteral("城市")});
// 填充数据
model->setItem(0, 0, new QStandardItem(QStringLiteral("张三")));
model->setItem(0, 1, new QStandardItem(QStringLiteral("28")));
model->setItem(0, 2, new QStandardItem(QStringLiteral("北京")));
// 还可以设置图标、颜色等
QStandardItem *item = new QStandardItem(QStringLiteral("李四"));
item->setIcon(QIcon(":/icons/user.png"));
item->setForeground(QBrush(Qt::blue));
model->setItem(1, 0, item);
// 绑定到视图
QTableView *tableView = new QTableView;
tableView->setModel(model);
你看,就这么几行代码,一个带表头的表格就出来了。每个QStandardItem可以设置文本、图标、字体、颜色、工具提示、自定义数据……几乎你能想到的显示属性它都支持。
| 功能 | QStandardItemModel | QAbstractListModel |
|---|---|---|
| 使用方式 | 直接实例化,无需继承 | 需要继承并重写虚函数 |
| 灵活性 | 中等,适合通用场景 | 高,完全自定义 |
| 性能 | 一般,每个Item都是对象 | 优秀,数据可紧凑存储 |
| 适用场景 | 小型数据、快速原型 | 大量数据、复杂数据结构 |
我的建议:做原型验证时用QStandardItemModel,效率最高。但正式项目如果数据量超过几千条,或者数据结构复杂,还是老老实实继承QAbstractListModel或QAbstractTableModel吧。性能差距很明显。
QSortFilterProxyModel:排序和过滤的“中间层”
这个类很有意思。它本身不存数据,而是作为一个“代理”夹在模型和视图之间。视图看到的是代理模型,代理模型从源模型拿数据,然后进行排序或过滤。
为什么要多这一层?因为排序和过滤不应该污染原始数据。你想想看,用户点一下表头排序,难道要把源数据重新排一遍?那多视图同时显示怎么办?
// 源模型:一个简单的字符串列表模型
QStringListModel *sourceModel = new QStringListModel(this);
sourceModel->setStringList({
QStringLiteral("苹果"),
QStringLiteral("香蕉"),
QStringLiteral("橙子"),
QStringLiteral("西瓜"),
QStringLiteral("葡萄")
});
// 代理模型:支持排序和过滤
QSortFilterProxyModel *proxyModel = new QSortFilterProxyModel(this);
proxyModel->setSourceModel(sourceModel);
proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
proxyModel->setFilterKeyColumn(0);
// 视图绑定代理模型
QListView *listView = new QListView;
listView->setModel(proxyModel);
listView->setSortingEnabled(true); // 启用点击排序
// 过滤:只显示包含"瓜"的项目
proxyModel->setFilterFixedString(QStringLiteral("瓜"));
运行这段代码,列表里只会显示“西瓜”。而且用户点击表头还能排序,源数据纹丝不动。
核心要点:
- 代理模型通过
setSourceModel()绑定源模型 - 视图始终操作代理模型,不直接接触源模型
- 排序和过滤完全在代理层完成,源模型不受影响
- 可以叠加多个代理模型实现复杂效果
我曾经在项目中遇到一个需求:一个表格要同时支持多列过滤、模糊搜索、点击排序。如果用QStandardItemModel自己实现,代码量至少翻三倍。用QSortFilterProxyModel,重写filterAcceptsRow()函数,几十行代码搞定。
// 自定义过滤逻辑:只显示年龄大于20且城市包含"北"的行
bool MyFilterProxy::filterAcceptsRow(int sourceRow,
const QModelIndex &sourceParent) const
{
QModelIndex ageIndex = sourceModel()->index(sourceRow, 1, sourceParent);
QModelIndex cityIndex = sourceModel()->index(sourceRow, 2, sourceParent);
int age = sourceModel()->data(ageIndex).toInt();
QString city = sourceModel()->data(cityIndex).toString();
return (age > 20) && city.contains(QStringLiteral("北"));
}
避坑指南:我曾经犯过一个错误——在filterAcceptsRow()里直接修改了源模型的数据。结果导致过滤和显示不同步,界面乱成一团。记住:代理模型里永远不要修改源模型的数据,你只能决定“显示”或“不显示”。
三者如何选择?一张图说清楚
说白了,这三者的关系就像搭积木:QAbstractListModel是地基,给你最大的自由度;QStandardItemModel是预制板,拿来就能用;QSortFilterProxyModel是装修层,给房子加上排序和过滤的功能。
实际项目中,我经常这样组合:用QAbstractListModel封装业务数据,外面套一层QSortFilterProxyModel实现搜索和排序,视图绑定代理模型。这样数据层、逻辑层、显示层完全解耦,后期维护起来特别舒服。
嗯,模型/视图编程的内容远不止这些,但掌握了这三个类,你已经能应对Qt开发中80%的数据展示场景了。剩下的20%,等你遇到具体需求时,再深入去研究QAbstractTableModel、自定义委托、拖拽支持这些高级特性吧。
公众号:蓝海资料掘金营,微信deep3321