第十七章: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的一部分职责拆了出来。

我画了一张图,帮你理清它们之间的关系:

Model QAbstractItemModel 存储数据、提供接口 View QListView / QTableView 展示数据、处理交互 Delegate 自定义渲染与编辑 数据索引 用户操作 委托渲染 编辑数据

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()绑定,之后所有数据展示和交互都由框架自动处理。

注意:千万不要把QTableWidgetQTableView搞混了。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();

嗯,这里要注意:QAbstractListModelQAbstractItemModel的简化版,专门为一维列表准备的。如果你需要表格或树形,直接继承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,结果视图里双击编辑时,编辑框里一片空白。记住:如果模型是可编辑的,DisplayRoleEditRole最好都返回数据,或者让EditRole返回原始数据,DisplayRole返回格式化后的数据。

17.8 实战建议

说了这么多,最后给你几条实战建议:

  • 从小做起:先写一个只读的列表模型,跑通了再扩展成表格或树形。
  • 善用调试:模型/视图架构的bug很难肉眼发现。我习惯在data()rowCount()里加qDebug,看看视图到底在请求什么数据。
  • 注意性能data()会被频繁调用,千万别在里面做耗时操作。如果数据需要计算,提前算好缓存起来。
  • 信号/槽别忘:数据变化时,记得发射dataChanged()rowsInserted()等信号,否则视图不会刷新。

模型/视图架构是QT里最优雅的设计之一。刚开始可能觉得绕,但一旦用顺手了,你会发现它让代码结构清晰了不止一个档次。好了,这一章就到这里,去写代码吧。


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