22、Qt模型/视图编程:MVC模式简介、QAbstractListModel、QStandardItemModel、QSortFilterProxyModel

模型/视图编程,说白了就是Qt里处理数据和界面显示的一套经典套路。我刚开始学Qt那会儿,总觉得这东西绕来绕去,不如直接往控件里塞数据来得痛快。后来项目越做越大,才发现没有模型/视图,代码根本没法维护。

今天我们就来聊聊MVC模式在Qt里的落地,以及三个最常用的模型类:QAbstractListModelQStandardItemModelQSortFilterProxyModel

MVC模式:数据与显示的“中间人”

MVC全称Model-View-Controller,在Qt里稍微变了一下,变成了Model-View-Delegate。不过核心思想没变:数据(Model)和显示(View)彻底分离

你想想看,如果没有MVC,你写一个表格,数据直接塞进表格控件里。哪天想换数据源了,得把整个表格重写一遍。有了MVC,数据只管存,视图只管画,中间通过模型接口沟通。改数据源?换个模型就行,视图不用动。

核心关系:

  • Model(模型):负责存储和管理数据,提供访问接口
  • View(视图):负责数据的可视化呈现,如列表、表格、树
  • Delegate(委托):负责数据的编辑和绘制细节

我个人习惯把Model想象成一个“数据仓库”,View是“展示窗口”,Delegate是“装修师傅”。仓库里有什么,窗口就展示什么,装修师傅决定怎么摆好看。

Model(模型) 数据存储与接口 QAbstractItemModel View(视图) 数据可视化 QListView/QTableView Delegate(委托) 绘制与编辑 QStyledItemDelegate 数据通过模型索引(QModelIndex)在层间传递 View不直接操作数据,Model不关心显示方式 信号-槽机制:数据变化时Model发出信号,View自动刷新

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,效率最高。但正式项目如果数据量超过几千条,或者数据结构复杂,还是老老实实继承QAbstractListModelQAbstractTableModel吧。性能差距很明显。

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 需要排序功能 需要过滤功能 多视图共享数据 代理模型可以嵌套:QSortFilterProxyModel 可以 包装 QStandardItemModel 或 QAbstractListModel 视图始终绑定代理模型,不直接绑定源模型 这样切换数据源时,视图代码完全不用改

说白了,这三者的关系就像搭积木:QAbstractListModel是地基,给你最大的自由度;QStandardItemModel是预制板,拿来就能用;QSortFilterProxyModel是装修层,给房子加上排序和过滤的功能。

实际项目中,我经常这样组合:用QAbstractListModel封装业务数据,外面套一层QSortFilterProxyModel实现搜索和排序,视图绑定代理模型。这样数据层、逻辑层、显示层完全解耦,后期维护起来特别舒服。

嗯,模型/视图编程的内容远不止这些,但掌握了这三个类,你已经能应对Qt开发中80%的数据展示场景了。剩下的20%,等你遇到具体需求时,再深入去研究QAbstractTableModel、自定义委托、拖拽支持这些高级特性吧。


公众号:蓝海资料掘金营,微信deep3321