目标读者:已经看完前 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
后来产品说要加:
- 注册时间
- 是否启用
假设你在很多界面里都用 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 使用清单
最后,把这一期的内容压缩成几条可以直接写进项目规范里的要点:
-
UI 层尽量不用 QListWidget/QTableWidget/QTreeWidget
- 正式业务界面优先使用
QListView/QTableView/QTreeView + 自定义模型; - 把数据读写、刷新逻辑放在模型里,而不是放在界面类里。
- 正式业务界面优先使用
-
一份业务数据,一个模型,多种视图
- 同一模型可以同时挂到多个视图上,例如主表 + 侧边简略列表;
- 模型更新一次,所有视图自动联动。
-
所有搜索、过滤、排序优先考虑 QSortFilterProxyModel
- 源模型只管「给出所有数据」;
- 过滤/排序交给代理模型,组合功能时可以链式使用多个代理。
-
状态 / 进度 / 等级之类的列,优先用委托绘制小组件
- 委托不改数据,只负责画、编辑;
- 充分利用自定义
role存储绘制所需的信息。
-
模型操作的粒度尽量精细
- 修改一行:
dataChanged(); - 增删一行:
beginInsertRows()/endInsertRows(); - 大范围更新再考虑
beginResetModel()。
- 修改一行:
-
数据库访问藏在模型或 DAO 层,UI 不直接写 SQL
- UI 调用模型的业务方法即可,如
insertUser()/removeUser(); - 方便未来切换存储方式(比如从 SQLite 换成 HTTP API)。
- UI 调用模型的业务方法即可,如
-
复杂页面拆成多个小模型
- 「用户列表」一个模型,「日志列表」一个模型;
- 避免一个万能大模型塞满业务逻辑。
按照这套方式整理项目里所有需要「展示列表/表格/树」的地方,你会非常明显地感受到两点变化:
- UI 代码变得干净很多,更多关注「怎么展示」,而不是「怎么搬数据」;
- 重构数据库或业务字段时,不再需要满世界找
setItem(),只要改一次模型即可。