Qt5 进阶【9】模型-视图框架实战:从 TableView 到自定义模型的一整套落地方案

目标读者:已经看完前 1--8 期,有一定 Qt 开发经验,但在列表、表格、树形数据展示上仍然停留在「往控件里一个个 addItem」阶段,希望真正吃透 Qt 模型-视图框架(Model/View)的 C++/Qt 程序员。

示例环境:Qt 5.12 / 5.15 + Qt Creator + CMake(你习惯 qmake 也没问题,工程体量不大,改起来很快)

目录

[一、问题背景:当 QListWidget/QTableWidget 撑不住项目时](#一、问题背景:当 QListWidget/QTableWidget 撑不住项目时)

[1. 一改数据结构,UI 代码全线跟着改](#1. 一改数据结构,UI 代码全线跟着改)

[2. 数据和显示耦合太死,想复用完全没法下手](#2. 数据和显示耦合太死,想复用完全没法下手)

[3. 数据量一大,刷新 UI 直接卡住](#3. 数据量一大,刷新 UI 直接卡住)

[4. 线程 / 数据源多样化后,无法优雅扩展](#4. 线程 / 数据源多样化后,无法优雅扩展)

二、核心知识点:先把几件绕不过的概念讲清楚

[1. Qt 里的「模型-视图」到底是什么关系?](#1. Qt 里的「模型-视图」到底是什么关系?)

[2. 一个索引(QModelIndex)说清楚「谁在第几行第几列」](#2. 一个索引(QModelIndex)说清楚「谁在第几行第几列」)

[3. 「角色(role)」:同一个单元格的不同「面孔」](#3. 「角色(role)」:同一个单元格的不同「面孔」)

[4. 代理模型 QSortFilterProxyModel:搜索 / 过滤 / 排序的好帮手](#4. 代理模型 QSortFilterProxyModel:搜索 / 过滤 / 排序的好帮手)

[5. 自定义委托:QStyledItemDelegate](#5. 自定义委托:QStyledItemDelegate)

[三、完整 Demo 工程:User 管理列表的模型-视图重构实战](#三、完整 Demo 工程:User 管理列表的模型-视图重构实战)

[1. 工程目录结构](#1. 工程目录结构)

[2. CMakeLists.txt](#2. CMakeLists.txt)

[3. databaseconnection.h / .cpp:数据库初始化与建表](#3. databaseconnection.h / .cpp:数据库初始化与建表)

[4. user.h / .cpp:简单的用户实体类](#4. user.h / .cpp:简单的用户实体类)

[5. usermodel.h / .cpp:核心自定义模型](#5. usermodel.h / .cpp:核心自定义模型)

[6. statusdelegates.h / .cpp:为状态列画彩色标签](#6. statusdelegates.h / .cpp:为状态列画彩色标签)

[7. mainwindow.h / .cpp:主界面整合](#7. mainwindow.h / .cpp:主界面整合)

[8. main.cpp:程序入口](#8. main.cpp:程序入口)

[四、实战中的坑与优化:基于这个 Demo 再聊几点经验](#四、实战中的坑与优化:基于这个 Demo 再聊几点经验)

[1. 为什么要自己写模型,而不直接用 QSqlTableModel?](#1. 为什么要自己写模型,而不直接用 QSqlTableModel?)

[2. model->reload() / beginResetModel() 的使用场景](#2. model->reload() / beginResetModel() 的使用场景)

[3. 代理模型链:排序 + 过滤 + 分组](#3. 代理模型链:排序 + 过滤 + 分组)

[4. 自定义委托里不要做重逻辑](#4. 自定义委托里不要做重逻辑)

[5. 模型与业务解耦,让单元测试更好写](#5. 模型与业务解耦,让单元测试更好写)

[五、小结:一份可以直接落地的 Model/View 使用清单](#五、小结:一份可以直接落地的 Model/View 使用清单)


一、问题背景:当 QListWidget/QTableWidget 撑不住项目时

写 Qt 程序的时候,很多人一上来习惯用的控件是这些:

  • QListWidget:显示一列简单条目;
  • QTableWidget:显示一些「行 × 列」的表格;
  • QTreeWidget:展示树形结构,比如目录。

这些类用起来确实爽:

cpp 复制代码
// 典型写法
ui->tableWidget->setRowCount(users.size());
for (int i = 0; i < users.size(); ++i) {
    ui->tableWidget->setItem(i, 0, new QTableWidgetItem(QString::number(users[i].id)));
    ui->tableWidget->setItem(i, 1, new QTableWidgetItem(users[i].name));
}

小 Demo、校内课程项目,用这种做法完全没问题。但当项目往下走,你会慢慢被以下几件事折磨:

1. 一改数据结构,UI 代码全线跟着改

比如开始时,用户表只有:

  • id
  • name

后来产品说要加:

  • email
  • 注册时间
  • 是否启用

假设你在很多界面里都用 QTableWidgetItem 直接填字段,那你要改的地方可能包括:

  • 初始化表头的地方;
  • 填充数据的地方(几十处);
  • 排序、搜索、导出等逻辑里的一堆列下标。

任何一个地方漏改,都有可能导致「数据对不上列」「排序错乱」等问题,而且很难第一时间发现。

2. 数据和显示耦合太死,想复用完全没法下手

同一份业务数据,往往有好几种展示方式:

  • 在主界面用 QTableView 显示成表格;
  • 在侧边栏用 QListView 列出简略信息;
  • 在详情面板里展示成树形结构或属性面板。

如果每一种视图都各自把数据「搬一份进去」,那你就需要在每个地方都写一套刷新逻辑,任何数据变更都要手动通知所有视图------极其费劲,也极易出错。

3. 数据量一大,刷新 UI 直接卡住

QTableWidget 这种「数据和控件绑死」的类有一个特点:

每一格数据都是一个实际的 QTableWidgetItem 对象,每次刷新都在创建/销毁大量小对象。

当行数上万、列数十几时,即使只是「重新填充」,也会明显感觉到卡顿------这在监控类工具、日志查看器等应用中非常常见。

4. 线程 / 数据源多样化后,无法优雅扩展

随着项目演进,数据源往往不再只有「内存里的 QList」:

  • 有的来自后台线程的算法结果;
  • 有的来自数据库查询;
  • 有的来自网络接口。

如果 UI 层直接面向容器操作,就很难把这些数据源统一管理。代码会渐渐演变成「到处在塞 item」,而不是「数据变了,模型通知视图一次」。


Qt 其实早就给出了解决方案:模型-视图框架(Model/View Framework)​

它的核心思想其实不复杂:

  • Model(模型)​:只管数据,「我有什么行、什么列,每格里是什么」;
  • View(视图)​:只管展示,「用户想看第几行第几列,我去问模型就完了」;
  • Delegate(委托)​:只管怎么画、「怎么编辑」。

这一期,我就用一个可以直接复制运行的 Demo 工程,把完整的一套 Model/View 实战逻辑从头到尾走一遍。


二、核心知识点:先把几件绕不过的概念讲清楚

1. Qt 里的「模型-视图」到底是什么关系?

Qt 中有两套看起来很像的东西:

  • QTableWidget / QListWidget / QTreeWidget ------ 视图 + 内置模型 的封装;
  • QTableView / QListView / QTreeView ------ 纯视图,需要你手动提供模型。

你可以简单理解成:

  • Widget:适合简单一点的场景,适合初学;
  • View + Model:适合复杂、可扩展、数据量大、数据源多样的场景。

模型(QAbstractItemModel 派生类)负责回答这些问题:

  • 你有多少行、多少列?
    rowCount() / columnCount()
  • 某个位置的数据值是多少?
    data(const QModelIndex&, int role)
  • 用户改了一个单元格,你是否接受?
    setData(...)
  • 哪些单元格可以选中、可以编辑?
    flags(...)

视图(QTableView 等)只做两件事:

  • data() 返回的内容画到界面上;
  • 接受用户输入,然后再通过 setData() 反馈给模型。

2. 一个索引(QModelIndex)说清楚「谁在第几行第几列」

QModelIndex 是模型世界里的「定位卡」:

  • 哪个模型:隶属于哪个 model;
  • 第几行:row()
  • 第几列:column()
  • 父节点是谁:parent()(树形结构才用得多)。

在平铺结构(如表格)里,通常 parent() 都是一个无效索引(QModelIndex())。

3. 「角色(role)」:同一个单元格的不同「面孔」

data() 除了 Qt::DisplayRole,还有很多内置角色:

  • Qt::DisplayRole:显示文字;
  • Qt::EditRole:编辑时的值;
  • Qt::DecorationRole:图标;
  • Qt::TextAlignmentRole:对齐方式;
  • 自定义 Qt::UserRole + N:你可以存任意数据。

这让我们可以把一个单元格当做「数据容器」,而不是只显示一串字符串。

4. 代理模型 QSortFilterProxyModel:搜索 / 过滤 / 排序的好帮手

  • 它本身也是一个 QAbstractItemModel
  • 内部持有一个「源模型」;
  • rowCount()data() 等都是在源模型和视图之间做一层映射。

典型用法:

cpp 复制代码
QSortFilterProxyModel *proxy = new QSortFilterProxyModel(this);
proxy->setSourceModel(userModel);
proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
proxy->setFilterKeyColumn(-1);          // -1 表示所有列参与过滤

ui->tableView->setModel(proxy);

connect(ui->searchEdit, &QLineEdit::textChanged,
        proxy, &QSortFilterProxyModel::setFilterFixedString);

以后你实现搜索、过滤,优先考虑 QSortFilterProxyModel,而不是自己在 UI 层重建一堆临时列表。

5. 自定义委托:QStyledItemDelegate

委托的作用是让你「决定一个单元格怎么画、怎么编辑」。比如:

  • 把 0/1 画成「停用/启用」带颜色的标签;
  • 把 int 画成进度条;
  • 把某列做成一个下拉框编辑器。

常见重写的两个函数:

cpp 复制代码
void paint(QPainter *painter,
           const QStyleOptionViewItem &option,
           const QModelIndex &index) const override;

QWidget *createEditor(QWidget *parent,
                      const QStyleOptionViewItem &option,
                      const QModelIndex &index) const override;

这一期我们主要用委托做一个「启用/停用标签」的效果。


三、完整 Demo 工程:User 管理列表的模型-视图重构实战

下面进入实战部分,这一小节会给出一个可以直接编译运行的工程。示例功能是:

  • 使用 SQLite 存一张 users 表;
  • 用自定义 UserModel 通过 QTableView 显示用户列表;
  • 用自定义委托在「状态」列显示彩色标签;
  • QSortFilterProxyModel 做搜索过滤;
  • 支持添加、编辑、删除用户。

1. 工程目录结构

按下面的结构新建:

bash 复制代码
ModelViewDemo/
├── CMakeLists.txt
├── include/
│   ├── databaseconnection.h
│   ├── user.h
│   ├── usermodel.h
│   ├── statusdelegates.h
│   └── mainwindow.h
└── src/
    ├── main.cpp
    ├── databaseconnection.cpp
    ├── user.cpp
    ├── usermodel.cpp
    ├── statusdelegates.cpp
    └── mainwindow.cpp

说明:

user.h/.cpp 只是一个轻量的实体类,方便管理用户字段;委托我拆成了

statusdelegates.*,其中可以放多个列的自定义绘制逻辑。

下面我按文件依次给出代码。


2. CMakeLists.txt

自己写


3. databaseconnection.h / .cpp:数据库初始化与建表

include/databaseconnection.h

src/databaseconnection.cpp


4. user.h / .cpp:简单的用户实体类

include/user.h

src/user.cpp


5. usermodel.h / .cpp:核心自定义模型

include/usermodel.h

src/usermodel.cpp

3、4、5部分的代码可以参照之前几期的内容自己完成,也可以找我要,后期完整代码会上传资源。


6. statusdelegates.h / .cpp:为状态列画彩色标签

include/statusdelegates.h

cpp 复制代码
#ifndef STATUSDELEGATES_H
#define STATUSDELEGATES_H

#include <QStyledItemDelegate>

/**
 * @brief StatusDelegate
 * 在「状态」列绘制彩色标签:绿色=活跃,红色=停用
 */
class StatusDelegate : public QStyledItemDelegate
{
    Q_OBJECT
public:
    explicit StatusDelegate(QObject *parent = nullptr);

    void paint(QPainter *painter,
               const QStyleOptionViewItem &option,
               const QModelIndex &index) const override;
    QSize sizeHint(const QStyleOptionViewItem &option,
                   const QModelIndex &index) const override;
};

#endif // STATUSDELEGATES_H

src/statusdelegates.cpp

cpp 复制代码
#include "statusdelegates.h"
#include "usermodel.h"

#include <QPainter>
#include <QApplication>
#include <QStyle>

StatusDelegate::StatusDelegate(QObject *parent)
    : QStyledItemDelegate(parent)
{
}

void StatusDelegate::paint(QPainter *painter,
                           const QStyleOptionViewItem &option,
                           const QModelIndex &index) const
{
    if (!index.isValid()) {
        QStyledItemDelegate::paint(painter, option, index);
        return;
    }

    bool active = index.data(UserModel::IsActiveRole).toBool();
    QString text = active ? QStringLiteral("活跃") : QStringLiteral("停用");

    QStyleOptionViewItem opt(option);
    initStyleOption(&opt, index);

    painter->save();
    painter->setRenderHint(QPainter::Antialiasing, true);

    // 背景颜色
    QColor bg = active ? QColor(0, 160, 0) : QColor(180, 0, 0);
    QColor fg = Qt::white;

    QRect rect = opt.rect.adjusted(4, 4, -4, -4);
    painter->setBrush(bg);
    painter->setPen(Qt::NoPen);
    painter->drawRoundedRect(rect, 6, 6);

    painter->setPen(fg);
    painter->drawText(rect, Qt::AlignCenter, text);

    painter->restore();
}

QSize StatusDelegate::sizeHint(const QStyleOptionViewItem &option,
                               const QModelIndex &index) const
{
    QSize base = QStyledItemDelegate::sizeHint(option, index);
    return base + QSize(0, 4);
}

7. mainwindow.h / .cpp:主界面整合

include/mainwindow.h

cpp 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

class QTableView;
class QPushButton;
class QLineEdit;
class QSortFilterProxyModel;
class QStatusBar;

class UserModel;

/**
 * @brief MainWindow
 * 负责搭建 UI,组合模型、视图、代理、搜索等。
 */
class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    explicit MainWindow(QWidget *parent = nullptr);
    ~MainWindow() override;

private slots:
    void onAddUser();
    void onDeleteUser();
    void onEditUser();
    void onSearchTextChanged(const QString &text);
    void onSelectionChanged();

private:
    void setupUi();
    void setupConnections();
    void updateStatusBar();

private:
    UserModel              *m_userModel = nullptr;
    QSortFilterProxyModel  *m_proxyModel = nullptr;
    QTableView             *m_tableView = nullptr;

    QLineEdit   *m_searchEdit = nullptr;
    QPushButton *m_addBtn = nullptr;
    QPushButton *m_editBtn = nullptr;
    QPushButton *m_delBtn = nullptr;

    QStatusBar  *m_statusBar = nullptr;
};

#endif // MAINWINDOW_H

src/mainwindow.cpp

cpp 复制代码
#include "mainwindow.h"
#include "databaseconnection.h"
#include "usermodel.h"
#include "statusdelegates.h"

#include <QTableView>
#include <QSortFilterProxyModel>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLineEdit>
#include <QPushButton>
#include <QStatusBar>
#include <QMessageBox>
#include <QInputDialog>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    setWindowTitle(QStringLiteral("Qt5 模型-视图框架实战 Demo"));
    resize(900, 600);

    setupUi();
    setupConnections();
    updateStatusBar();
}

MainWindow::~MainWindow()
{
}

void MainWindow::setupUi()
{
    // 中心 widget
    auto *central = new QWidget(this);
    auto *mainLayout = new QVBoxLayout(central);

    // 顶部操作区
    auto *topLayout = new QHBoxLayout();
    m_searchEdit = new QLineEdit(this);
    m_searchEdit->setPlaceholderText(QStringLiteral("输入用户名或邮箱搜索"));

    m_addBtn = new QPushButton(QStringLiteral("添加用户"), this);
    m_editBtn = new QPushButton(QStringLiteral("编辑"), this);
    m_delBtn = new QPushButton(QStringLiteral("删除"), this);

    m_editBtn->setEnabled(false);
    m_delBtn->setEnabled(false);

    topLayout->addWidget(m_searchEdit, 1);
    topLayout->addWidget(m_addBtn);
    topLayout->addWidget(m_editBtn);
    topLayout->addWidget(m_delBtn);

    // 表格视图
    m_tableView = new QTableView(this);
    m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
    m_tableView->setSelectionMode(QAbstractItemView::SingleSelection);
    m_tableView->setAlternatingRowColors(true);
    m_tableView->setEditTriggers(QAbstractItemView::DoubleClicked
                                 | QAbstractItemView::SelectedClicked);

    // 模型
    m_userModel = new UserModel(this);

    // 代理模型(搜索过滤 + 排序)
    m_proxyModel = new QSortFilterProxyModel(this);
    m_proxyModel->setSourceModel(m_userModel);
    m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
    m_proxyModel->setFilterKeyColumn(-1);     // 所有列参与过滤
    m_proxyModel->setDynamicSortFilter(true);

    m_tableView->setModel(m_proxyModel);
    m_tableView->setSortingEnabled(true);

    // 状态列自定义委托
    auto *statusDelegate = new StatusDelegate(this);
    m_tableView->setItemDelegateForColumn(UserModel::ColStatus, statusDelegate);

    // 列头设置
    auto *header = m_tableView->horizontalHeader();
    header->setSectionResizeMode(UserModel::ColId,        QHeaderView::ResizeToContents);
    header->setSectionResizeMode(UserModel::ColUsername,  QHeaderView::Stretch);
    header->setSectionResizeMode(UserModel::ColEmail,     QHeaderView::Stretch);
    header->setSectionResizeMode(UserModel::ColStatus,    QHeaderView::ResizeToContents);
    header->setSectionResizeMode(UserModel::ColCreatedAt, QHeaderView::ResizeToContents);

    mainLayout->addLayout(topLayout);
    mainLayout->addWidget(m_tableView, 1);

    setCentralWidget(central);

    // 状态栏
    m_statusBar = new QStatusBar(this);
    setStatusBar(m_statusBar);
}

void MainWindow::setupConnections()
{
    connect(m_addBtn, &QPushButton::clicked,
            this, &MainWindow::onAddUser);
    connect(m_delBtn, &QPushButton::clicked,
            this, &MainWindow::onDeleteUser);
    connect(m_editBtn, &QPushButton::clicked,
            this, &MainWindow::onEditUser);

    connect(m_searchEdit, &QLineEdit::textChanged,
            this, &MainWindow::onSearchTextChanged);

    connect(m_tableView->selectionModel(), &QItemSelectionModel::selectionChanged,
            this, &MainWindow::onSelectionChanged);

    // 双击编辑
    connect(m_tableView, &QTableView::doubleClicked,
            this, &MainWindow::onEditUser);
}

void MainWindow::onAddUser()
{
    bool ok = false;
    QString username = QInputDialog::getText(
        this, QStringLiteral("添加用户"),
        QStringLiteral("用户名:"), QLineEdit::Normal,
        QString(), &ok);
    if (!ok || username.trimmed().isEmpty())
        return;

    QString email = QInputDialog::getText(
        this, QStringLiteral("添加用户"),
        QStringLiteral("邮箱:"), QLineEdit::Normal,
        QString("%1@example.com").arg(username), &ok);
    if (!ok || email.trimmed().isEmpty())
        return;

    User u;
    u.username = username.trimmed();
    u.email    = email.trimmed();
    u.password = "123456";
    u.isActive = true;

    if (!m_userModel->insertUser(u)) {
        QMessageBox::warning(this, QStringLiteral("错误"), QStringLiteral("添加用户失败"));
    }

    updateStatusBar();
}

void MainWindow::onDeleteUser()
{
    QModelIndex proxyIndex =
        m_tableView->currentIndex();
    if (!proxyIndex.isValid())
        return;

    QModelIndex srcIndex =
        m_proxyModel->mapToSource(proxyIndex);
    int row = srcIndex.row();
    if (row < 0)
        return;

    User u = m_userModel->userAt(row);

    auto ret = QMessageBox::question(
        this, QStringLiteral("确认删除"),
        QStringLiteral("确定要删除用户 \"%1\" 吗?").arg(u.username),
        QMessageBox::Yes | QMessageBox::No);
    if (ret != QMessageBox::Yes)
        return;

    if (!m_userModel->removeUser(row)) {
        QMessageBox::warning(this, QStringLiteral("错误"), QStringLiteral("删除失败"));
    }

    updateStatusBar();
}

void MainWindow::onEditUser()
{
    QModelIndex proxyIndex = m_tableView->currentIndex();
    if (!proxyIndex.isValid())
        return;

    QModelIndex srcIndex = m_proxyModel->mapToSource(proxyIndex);
    int row = srcIndex.row();
    if (row < 0)
        return;

    User u = m_userModel->userAt(row);

    bool ok = false;
    QString newName = QInputDialog::getText(
        this, QStringLiteral("编辑用户"),
        QStringLiteral("用户名:"), QLineEdit::Normal,
        u.username, &ok);
    if (!ok || newName.trimmed().isEmpty())
        return;

    QString newEmail = QInputDialog::getText(
        this, QStringLiteral("编辑用户"),
        QStringLiteral("邮箱:"), QLineEdit::Normal,
        u.email, &ok);
    if (!ok || newEmail.trimmed().isEmpty())
        return;

    // 通过 setData 写回模型,触发数据库更新
    m_userModel->setData(
        m_userModel->index(row, UserModel::ColUsername),
        newName.trimmed(),
        Qt::EditRole);

    m_userModel->setData(
        m_userModel->index(row, UserModel::ColEmail),
        newEmail.trimmed(),
        Qt::EditRole);

    updateStatusBar();
}

void MainWindow::onSearchTextChanged(const QString &text)
{
    m_proxyModel->setFilterFixedString(text.trimmed());
    updateStatusBar();
}

void MainWindow::onSelectionChanged()
{
    bool hasSel = m_tableView->selectionModel()->hasSelection();
    m_editBtn->setEnabled(hasSel);
    m_delBtn->setEnabled(hasSel);
}

void MainWindow::updateStatusBar()
{
    int total = m_userModel->rowCount();
    int visible = m_proxyModel->rowCount();

    QString msg = QStringLiteral("总用户数:%1").arg(total);
    if (m_searchEdit->text().trimmed().size() > 0) {
        msg += QStringLiteral("  |  当前过滤结果:%1").arg(visible);
    }
    m_statusBar->showMessage(msg);
}

8. main.cpp:程序入口

src/main.cpp

cpp 复制代码
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    QApplication::setApplicationName("ModelViewDemo");
    QApplication::setOrganizationName("QtAdvancedSeries");
    a.setStyle("Fusion");

    // 准备数据库目录
    QString dbDir = QStandardPaths::writableLocation(
                QStandardPaths::AppDataLocation);
    QDir dir(dbDir);
    if (!dir.exists()) {
        dir.mkpath(".");
    }

    QString dbPath = dir.filePath("users_demo.db");
    qInfo() << "数据库文件路径:" << dbPath;

    if (!DatabaseConnection::instance().initialize(dbPath)) {
        qCritical() << "数据库初始化失败";
        return 1;
    }

    MainWindow w;
    w.show();

    return a.exec();
}

到这里,一个完整的 Model/View Demo 工程就搭好了。


四、实战中的坑与优化:基于这个 Demo 再聊几点经验

1. 为什么要自己写模型,而不直接用 QSqlTableModel?

QSqlTableModel 确实是 Qt 自带的一个「数据库表模型」,简单场景用起来很好,比如:

cpp 复制代码
QSqlTableModel *model = new QSqlTableModel(this, db);
model->setTable("users");
model->select();
ui->tableView->setModel(model);

但问题在于:

  • 它默认把「数据库结构」直接暴露给 UI 层;
  • 一旦你要合并多张表、加虚拟字段(比如用 is_active 画标签),就开始吃力;
  • 对于复杂过滤、多线程加载、分页等需求,定制成本比自己写一个轻量模型还高。

所以我的习惯是:

  • 小工具 / 内部调试面板,可以用 QSqlTableModel 快速搞定;
  • 正式业务界面,一律自定义模型,把数据库细节藏在模型里面。

2. model->reload() / beginResetModel() 的使用场景

在 Demo 里,我在插入新用户后调用了 reload(),内部是:

cpp 复制代码
beginResetModel();
// 重新查询数据库,替换 m_users
endResetModel();

这等价于告诉所有视图:

「我整个模型都变了,你重新全量刷新一遍。」

对中小数据量来说,完全没问题;但如果你未来数据量很大,就要考虑更细粒度的更新:

  • 插入:beginInsertRows() / endInsertRows()
  • 删除:beginRemoveRows() / endRemoveRows()
  • 修改:dataChanged()

3. 代理模型链:排序 + 过滤 + 分组

本 Demo 里只用了一个 QSortFilterProxyModel 做过滤和排序,如果你再想搞一些复杂功能,比如:

  • 按状态分组显示;
  • 多条件组合过滤(启用状态 + 关键字);

可以把多个代理链起来:源模型 -> 过滤代理 1 -> 过滤代理 2 -> 视图

这样每一层只负责一个维度,逻辑会清晰很多。

4. 自定义委托里不要做重逻辑

委托的 paint() 是高频函数------界面滚动、窗口重绘时会被频繁调用。

因此:

  • 不要在里面做数据库访问;
  • 不要做复杂计算;
  • 最好只是从 index.data() 里读一点简单的信息,然后画图。

如果需要复杂数据,可以提前存在模型的自定义 role 里。

5. 模型与业务解耦,让单元测试更好写

把所有数据库访问都放在模型或者专门的 DAO 里,你可以很容易做「假数据模型」,用来写单元测试或离线 UI 调试:

cpp 复制代码
class FakeUserModel : public QAbstractTableModel {
    // 内存里放一些固定 User,模拟行为
};

这样你完全可以在不连数据库的情况下调试 UI 布局、交互逻辑,极大提升开发效率。


五、小结:一份可以直接落地的 Model/View 使用清单

最后,把这一期的内容压缩成几条可以直接写进项目规范里的要点:

  1. UI 层尽量不用 QListWidget/QTableWidget/QTreeWidget

    • 正式业务界面优先使用 QListView/QTableView/QTreeView + 自定义模型
    • 把数据读写、刷新逻辑放在模型里,而不是放在界面类里。
  2. 一份业务数据,一个模型,多种视图

    • 同一模型可以同时挂到多个视图上,例如主表 + 侧边简略列表;
    • 模型更新一次,所有视图自动联动。
  3. 所有搜索、过滤、排序优先考虑 QSortFilterProxyModel

    • 源模型只管「给出所有数据」;
    • 过滤/排序交给代理模型,组合功能时可以链式使用多个代理。
  4. 状态 / 进度 / 等级之类的列,优先用委托绘制小组件

    • 委托不改数据,只负责画、编辑;
    • 充分利用自定义 role 存储绘制所需的信息。
  5. 模型操作的粒度尽量精细

    • 修改一行:dataChanged()
    • 增删一行:beginInsertRows() / endInsertRows()
    • 大范围更新再考虑 beginResetModel()
  6. 数据库访问藏在模型或 DAO 层,UI 不直接写 SQL

    • UI 调用模型的业务方法即可,如 insertUser()/removeUser()
    • 方便未来切换存储方式(比如从 SQLite 换成 HTTP API)。
  7. 复杂页面拆成多个小模型

    • 「用户列表」一个模型,「日志列表」一个模型;
    • 避免一个万能大模型塞满业务逻辑。

按照这套方式整理项目里所有需要「展示列表/表格/树」的地方,你会非常明显地感受到两点变化:

  • UI 代码变得干净很多,更多关注「怎么展示」,而不是「怎么搬数据」;
  • 重构数据库或业务字段时,不再需要满世界找 setItem(),只要改一次模型即可。
相关推荐
人道领域2 小时前
javaWeb从入门到进阶(SpringBoot基础案例2)
java·开发语言·mybatis
Stack Overflow?Tan902 小时前
c++constexpr
开发语言·c++
雨季6662 小时前
Flutter 三端应用实战:OpenHarmony 简易数字累加器开发指南
开发语言·flutter·ui·ecmascript
晚风吹长发2 小时前
初步了解Linux中的信号捕捉
linux·运维·服务器·c++·算法·进程·x信号
码农水水2 小时前
米哈游Java面试被问:Shenandoah GC的Brooks Pointer实现机制
java·开发语言·jvm·spring boot·redis·安全·面试
小程同学>o<2 小时前
嵌入式之C/C++(二)内存
c语言·开发语言·c++·笔记·嵌入式软件·面试题库
程序员清洒2 小时前
Flutter for OpenHarmony:Dialog 与 BottomSheet — 弹出式交互
开发语言·flutter·华为·交互·鸿蒙
cyforkk2 小时前
07、Java 基础硬核复习:面向对象编程(进阶)的核心逻辑与面试考点
java·开发语言·面试
wjhx2 小时前
在Qt Design Studio中进行页面切换
前端·javascript·qt