尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
Qt模型类实战指南:从现成模型到自定义实现与数据同步
很多 Qt 学习者看《Qt Creator快速入门第三版》前面十几章都挺顺一到第16章 Model/View 就有点卡壳尤其是第16-2节讲到“模型类”的时候概念一下子抽象起来。我当年这节看了三遍才真正缓过来主要原因不是模型难而是书里同时讲 Model、View、Delegate一时反应不过来这三者到底怎么分。这篇文章就把“模型类”单独拿出来深挖一遍讲清它是干什么的、现有几个子类怎么选、什么时候必须自己写以及写完以后怎么让视图保持同步。适合刚看到这里开始发懵的读者也适合已经用过 QTableWidget 但没写过自定义模型的开发者。1. 模型类到底解决了什么问题先把思维转过来1.1 没有模型类之前的界面代码有多别扭在接触 Model/View 之前多数人的 Qt 界面写法是new 一个 QTableWidget然后 setItem 往里塞 QTableWidgetItem。这样做对原型非常友好但一遇到“同一条业务数据要在多个界面里展示”就开始难受。比如某个项目里有一批任务数据表格要展示所有任务侧边面板要显示未完成数量、过期数量底部还要有最近的几条记录。如果全用 QTableWidget 硬塞同一个任务会在表格、侧边面板、底部记录里各生成一份 item任务状态一变你要在少则两三个、多则四五个地方同步修改 item——漏一个就是展示不一致而且难查。这种别扭的本质是数据本身是“一份”但每个界面都在维护自己的“副本”。传统做法把数据摊平成了界面里的 item并没有一个统一的“数据源”。刚开始数据量小没感觉等任务数量过万、要加排序加过滤的时候item 的重复维护成本会直接把人拖垮。1.2 模型、视图、委托为什么非要拆开Qt 的答案是 Model/View 架构。三者的分工其实很朴素Model 只回答“数据在哪里、数据长什么样”负责把内部的数据结构翻译成统一接口View 只负责把模型提供的内容画出来处理选中、滚动、拖拽Delegate 负责具体单元格的绘制和编辑控件。翻译成生活场景模型是厨房后厨的食材台账视图是餐厅的菜单委托是具体的摆盘和传菜方式。菜单样式随便换不需要去改台账菜单上写的菜名变了那是台账那边变了才导致的。这里最关键的一句话是模型类可以同时挂多个视图。一份任务数据挂一个 QTableView 做详细列表再挂一个 QTreeView 做分类树模型里任意一条数据通过信号把变化广播出去所有视图自动刷新。这就是模型类存在的最直接理由。刚接触这一节时不需要把 Model/View/Delegate 全部理解透只要先记住一件事界面代码不应该直接抱着业务数据而要隔着一层模型类去访问。2. 四种现成模型类怎么挑能拿来用就别急着自己写2.1 最轻量的 QStringListModel如果只是要显示一份字符串列表比如历史搜索记录、日志等级列表QStringListModel 是最快选择。它的内部数据结构就是 QStringList用法只有几行构造一个 QStringListModel 把列表传进去再 view-setModel(model)。之后想整体替换内容直接 setStringList(另一个列表)视图会自动刷新。要注意的是它是“整体替换式”更新适合中小规模的静态列表不适合频繁插入单条数据的场景。2.2 QStandardItemModel 这个万金油QStandardItemModel 是初学者最先遇到的模型类因为它既能当表格又能当树行列、父子、表头、图标、对齐都支持。用它对中小数据量界面开发来说非常省事代码无非是 setHorizontalHeaderLabels 设置表头然后拿 QStandardItem 填数据再 setItem。这里有个经常被忽略的规则item 一旦交给 model所有权就归 model 管理你在外部不要再去 delete 它否则模型内部还保留着一个已删除对象的指针轻则显示异常重则崩溃。另外同一个 QStandardItem 不要同时塞给两个 model那样两端状态会互相打架。2.3 绑数据库的两个模型QSqlTableModel 适合单表的增删改查界面它 setTable 指定表名、select 执行查询后QTableView 就能直接展示用户改了单元格它还会帮你拼出 UPDATE 语句。QSqlQueryModel 则是只读的查询结果集适合做报表、统计这类不需要回写数据的场景。把它们放在一起记很省力一个是“能改库的”一个是“只能看的”。实际项目里管理后台多数用 QSqlTableModel统计报表用 QSqlQueryModel两者互补。2.4 四选一怎么判断选择模型类时我是按这四个问题来的数据结构是数组、表格还是树数据量多大需不需要用户编辑现有数据是已经在内存容器里还是在数据库或文件里我整理了一张对照表方便卡在这一节的读者直接套用模型类数据结构默认可编辑适合数据量自定义成本QStringListModel一维字符串列表否小无QStandardItemModel表格/树节点是(item 支持)中低QSqlTableModel数据库单表是较大低QSqlQueryModelSQL 查询结果否较大低自定义 QAbstractItemModel任意结构取决于实现任意高不要一上来就自定义模型。能用 QStandardItemModel 撑住的项目先用它跑通界面等出现“数据量太大”“需要实时联动”“结构特殊”这些问题时再考虑写自己的模型。这也是看书时最容易走偏的地方把自定义模型当成目的其实它只是手段。3. 自定义模型的五个核心函数实现顺序和常见遗漏3.1 什么时候非自定义不可当内存里已经维护着一份数据结构比如 QVector 或自定义类而你又希望界面直接读这份结构、不额外复制一份的时候就该自定义模型了。常见场景包括需要展示几十万行日志、要按业务规则做动态角色显示、数据源是内存容器且实时变化。自定义模型的价值在于“零拷贝映射”界面不复制你的数据而是通过接口按需读取。《Qt Creator快速入门 第三版》第16-2节把模型类的抽象结构讲得比较规范但如果只照着书上的接口顺序看容易忽略一个实操问题到底先实现哪个函数。我的建议是先实现 rowCount 和 columnCount让视图知道有多少行列再实现 data让视图能取到每个格子的内容接着处理 headerData 让表头能显示文字需要编辑再补 flags 和 setData。按这个顺序调试每一步都能看到明确进度。3.2 索引的语义一个格子靠 row/column/parent 定位模型类对外暴露的每个数据位置都是一个 QModelIndex。这个概念是最容易绕晕的地方但拆开只有一条规则row、column、parent 三个值唯一定位一个格子其中 parent 用于区分“父级下的位置”。顶层数据项的 parent 是默认构造的空索引 QModelIndex()。视图在显示时并不知道你的数据到底是数组还是链表它只拿着索引来问模型模型内部把索引翻译成真实数据的访问路径。createIndex 用来把模型的内部位置打包成索引发给视图一般写作 createIndex(row, column) 就够。注意索引在使用时可能因为模型结构变化而失效所以不要在视图里长期保存一个过期的 QModelIndex要用的时候现取。3.3 一个可直接套用的列表模型代码我用一个任务表格模型举例内部结构是 QVector 。定义 TaskItem 包含名称、是否完成、截止时间三个字段然后让 TaskTableModel 继承 QAbstractTableModel重写相应函数struct TaskItem { QString name; bool done false; QDateTime deadline; }; class TaskTableModel final : public QAbstractTableModel { public: explicit TaskTableModel(QObject* parent nullptr) : QAbstractTableModel(parent) {} int rowCount(const QModelIndex parent QModelIndex()) const override { return parent.isValid() ? 0 : m_tasks.size(); } int columnCount(const QModelIndex parent QModelIndex()) const override { return parent.isValid() ? 0 : 3; } QVariant data(const QModelIndex index, int role) const override { if (!index.isValid() || index.row() 0 || index.row() m_tasks.size()) return {}; const TaskItem t m_tasks.at(index.row()); if (role Qt::DisplayRole || role Qt::EditRole) { if (index.column() 0) return t.name; if (index.column() 1) return t.deadline.toString(yyyy-MM-dd hh:mm); if (index.column() 2) return t.done ? QStringLiteral(已完成) : QStringLiteral(未完成); } if (role Qt::TextAlignmentRole) { return index.column() 1 ? int(Qt::AlignCenter) : int(Qt::AlignLeft | Qt::AlignVCenter); } return {}; } QVariant headerData(int section, Qt::Orientation orientation, int role) const override { if (role ! Qt::DisplayRole) return {}; if (orientation Qt::Horizontal) { if (section 0) return QStringLiteral(任务名); if (section 1) return QStringLiteral(截止时间); if (section 2) return QStringLiteral(状态); } return section 1; } bool setData(const QModelIndex index, const QVariant value, int role) override { if (!index.isValid() || index.row() m_tasks.size()) return false; TaskItem t m_tasks[index.row()]; if (role Qt::EditRole) { if (index.column() 0) t.name value.toString(); if (index.column() 1) t.deadline value.toDateTime(); if (index.column() 2) t.done (value.toString() QStringLiteral(已完成)); } emit dataChanged(index, index, {Qt::DisplayRole, Qt::EditRole}); return true; } Qt::ItemFlags flags(const QModelIndex index) const override { if (!index.isValid()) return Qt::NoItemFlags; return Qt::ItemIsEnabled | Qt::ItemIsSelectable; } QVectorTaskItem m_tasks; };这段代码演示了常规模型类的完整骨架。要特别说明两点data() 里的 DisplayRole 和 EditRole 通常返回同一份数据这样界面显示和编辑框都能拿到正确内容如果你只想让某列可编辑flags() 里按列做判断即可不用全部开放编辑。3.4 新手最容易漏的三个点第一个是只实现了 DisplayRole结果程序里 QToolTip、字体、勾选框全都无效因为不同角色对应不同界面表现。第二个是忘记实现 headerData表格出现 1、2、3 的默认列名看起来非常业余。第三个是 flags() 不返回 Qt::ItemIsEnabled导致格子虽然显示了但不可选不可点在调试期容易被当成“模型没生效”。这三个点都是我看书时踩过、后来在实际模型代码里反复确认过的细节。4. 界面不刷新、插入就崩溃模型通知信号的成对规矩4.1 数据改了视图不知道根因是没有发信号自定义模型最常见的困惑是我在代码里改了 m_tasks[i].name界面上却纹丝不动。原因很简单视图不会主动轮询模型它依赖模型发出的数据变更信号。如果你绕过 setData 直接改内部容器视图根本不知情自然不刷新。正确的做法是改完数据后调用 emit dataChanged(起始索引, 结束索引)让视图更新对应区间。不要小看这个细节。我在写第一个自定义模型时曾经在一个业务回调里直接改结构体数组然后用 update() 重绘界面结果只有被重绘的格子偶尔变一下悬停、选中都乱套。后来把改动收敛进 setData 并统一 emit dataChanged才彻底正常。4.2 行数变化必须 begin/end 成对出现如果数据行数发生了增删光发 dataChanged 是不够的必须调用 beginInsertRows 和 endInsertRows或者对应的 beginRemoveRows / endRemoveRows。这两个函数的作用是通知视图“行结构即将变化”让视图在插入前后维持一致的索引状态。中间不能有其他事件循环打断也不能乱顺序beginInsertRows 之前先改内部数据会导致视图在通知前就看到了不一致的行数endInsertRows 之后才改又会让视图访问到越界行。这两个函数的配对原则是全部 Qt 模型编程里最重要的规矩。代码示例bool TaskTableModel::insertRow(int row, const QModelIndex parent) { if (row 0 || row m_tasks.size()) return false; beginInsertRows(parent, row, row); m_tasks.insert(row, TaskItem()); endInsertRows(); return true; }4.3 整体重置与批量更新的取舍当数据整体变化时用 beginResetModel / endResetModel 直接通知视图“全部重来”简单粗暴但开销大。逐条 dataChanged 虽然精细但逐条发信号在几千行数据量下也够让 UI 卡一下。我的经验做法是数据量小就整体重置数据量大且只是部分字段变化时尽可能把能合并成连续区间的合并发出如果是排序、过滤这种结构性变化就用 QSortFilterProxyModel而不是手动挨个发信号。这样写出来的模型界面响应速度会顺滑很多。5. 树形模型与父子索引parent() 的实现思路5.1 什么时候需要真正的树模型QStandardItemModel 已经能表达简单的树结构但如果你的数据本身就是一棵动态变化的树比如目录树、组织架构、分类层级而且节点数量很大自己写模型是更合适的方案。第16章讲到模型类时把树模型也归入范畴但很多读者会卡在“树模型到底比表模型多实现什么”这一点上。其实就多了一个关键成员parent() 函数以及 index() 函数里对 parent 的处理。5.2 parent() 与 index() 的配合逻辑对视图来说它拿到任意一个索引后会调用 parent(index) 去追问“这个节点的上一级是谁”也会调用 index(row, column, parent) 去展开“parent 下面的第 row 行第 column 列是谁”。所以这两个函数必须协同工作index(parent) 里要能根据 parent 返回对应子项parent(index) 要能根据 index 返回它的上级。根节点统一返回空索引 QModelIndex()。如果数据结构是一棵用节点指针串起来的树实现时可以借助 createIndex 的第三个参数把节点指针或节点 ID 存进索引这样后面定位子节点、找父节点都很快。5.3 树模型里最常见的内存陷阱createIndex 支持把内部指针存进去方便索引携带额外信息。这带来一个风险如果数据节点的生命周期不由模型管理节点被释放后索引还指向旧内存视图一展开就崩溃。我实际遇到过一次在一个模拟的磁盘目录树项目里刷新目录时先释放了旧节点模型却还持有旧节点指针用户一展开某个分支程序就崩。后来改成在刷新前先调用 beginResetModel让所有外部索引全部失效再重建数据节点endResetModel 之后重新获取索引必要时还要在 beginResetModel 之前清掉视图的选中状态避免视图拿着过期索引做选区计算。5.4 树模型性能的另一个关键树模型里常见性能瓶颈是行数为 0 的判断。有些实现会在 rowCount 里遍历整棵子树统计节点数量几万节点时每次展开都卡一下。比较好的做法是节点结构里直接缓存子节点数量增删子节点时同步更新计数。这样 rowCount 就是 O(1) 级别的查询界面展开会明显跟手。这一条属于常规文档不会写的优化细节但实际项目里非常值钱。6. 角色数据、排序与过滤模型类的延伸玩法6.1 用角色给视图传递更多信息data() 里除了 DisplayRole 和 EditRole还常搭配 Qt::ToolTipRole、Qt::TextColorRole、Qt::FontRole、Qt::CheckStateRole。比如任务列表里把过期未完成的行用红色显示用 CheckStateRole 让用户直接勾选完成。这些表现逻辑全部收在模型里视图不需要额外分支换一个视图仍然保持同样的表现这是模型类带来的典型收益。还是用前面的 TaskTableModel 举例data() 里再加一段代码即可实现“未完成且已过期显示成红色”if (role Qt::TextColorRole) { if (!t.done t.deadline QDateTime::currentDateTime() t.deadline.isValid()) return QColor(Qt::red); return {}; }6.2 用 QSortFilterProxyModel 实现排序过滤排序过滤不该直接写进数据模型否则模型既得负责存取又得负责排序职责一多就容易乱。Qt 给的做法是在模型和视图之间插一个 QSortFilterProxyModel视图挂这个中间模型中间模型底层再挂真实模型。代码大致是QSortFilterProxyModel proxy; proxy.setSourceModel(model); view-setModel(proxy); view-sortByColumn(1, Qt::AscendingOrder);注意两点排序要生效data() 里对应列要返回 Qt::EditRole或者给 proxy 设置 setSortRole过滤规则通过 setFilterFixedString 或重写 filterAcceptsRow 实现。更重要的是从视图中拿到的行号是中间模型的行号要真正访问源模型数据必须用 mapToSource 把索引转换回去否则你在视图里看到的行号和源模型的行号对不上选中数据容易串。6.3 更大数据量的两个实用建议一个是尽量让 data() 只做数据读取不在里面做耗时计算。比如字符串格式化、日期转换这类操作如果放在 data() 里视图每次重绘都要重算一次几百行还没事上万行时滚动就肉眼可见地卡。另一个是在需要高频局部刷新时把“最近变更的索引区间”缓存下来集中发送一次 dataChanged而不是每次改动就立刻发一条。这两个建议是我在两个项目里用实际交互体验换回来的教训写在这里给看到这一节的读者省点调试时间。我自己回头看模型类最让人怕的其实是它把“界面”和“数据”这两件原本绑在一起的事拆开了这个拆开的瞬间思维要转个弯。只要转过来了后面 rowCount、data、信号配对这些反而都是能一步步查到头的东西。刚开始写模型时别追求一步到位先用现成模型把界面跑起来再针对一个痛点换成自定义模型最后把树形和中间筛选层加进来这一章就算是真正吃透了。
RELATED

相关推荐

VCSA服务全Invalid?从vmon到systemd的排障实践与修复全攻略

VCSA服务全Invalid?从vmon到systemd的排障实践与修复全攻略

1. 故障背景:VCSA服务集体“invalid”的真相搞虚拟化的同行看到这个报错应该都不陌生:某天vCenter Server Appliance(VCSA)管理界面打不开,浏览器刷不出登录页,外部PSC/VC通信断掉,SSH上去执行s…

📅 2026/10/17 23:06:22
PS流深度解析:从RTP打包到H.264帧提取的完整实践

PS流深度解析:从RTP打包到H.264帧提取的完整实践

简介:面向音视频流媒体开发者,这份压缩包提供了海康、国标PS流解析的两种实现方案——基于ffmpeg的解析版本与直接解析PS流版本,适合需要对接海康设备或国标PS流做二次开发、播放与转码的技术人员。压缩包共27个文件,包括6个C源文…

📅 2026/10/17 23:06:22
Intouch与SQL Server报表系统配置:从数据库连接到VBA刷新的完整指南

Intouch与SQL Server报表系统配置:从数据库连接到VBA刷新的完整指南

简介:这是一份面向SCADA系统工程师与HMI开发者的Word操作文档,专门解决WonderWare Intouch 2014 R2(IDE)环境下的数据库对接与Excel报表生成问题。文档从基础配置讲起,包括通过SQL Server Management Studio修改身份验…

📅 2026/10/17 23:06:22
MORE NEWS

更多资讯

📰

Manus数据手套EMF手势跟踪在遥操作机械手中的优势:TaoToken统一Key打通数据链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

Claude Code 太贵?把 settings 改到 TaoToken 的省钱体验分享

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

交叉熵损失、可调节参数与LoRA:大模型微调核心链路全解析

交叉熵损失&大模型可调节参数&LoRA搞大模型微调的人,一定绕不开这三样东西:损失函数怎么算、超参数怎么调、参数高效微调用什么手法。这三个词看着各自独立,其实是一条完整链路——交叉熵损失告诉你模型学得好不好,可调节…

📰

Agent策略是什么?从岗位说明书到决策机制全解析

“Agent策略”这个词,我第一次听到的时候觉得特别玄乎,总觉得它是某种高深的数学定义或者算法黑话。后来真正上手做智能体应用,踩了半年坑才反应过来:策略就是给AI定的一套“在什么情况下做什么事”的规则,是它决策和行…

📰

数据科学硕士申请全攻略:笔试与面试真题解析、备考策略

去年十一月,我在申请交流群里看到一条新消息:“数据科学硕士笔试通知已发出,请查收邮件。”那一刻我的手确实抖了一下。作为一个统计学背景的申请者,笔试对我而言意味着两件事:一是材料关总算过了,二是真正…

📰

Cocos JSC Endecryptor:从原理到实战的脚本加解密指南

不知道你有没有过这种体验:用 Cocos Creator 打完包,闲得没事把原生包里的 assets 目录翻出来,结果一眼就看到一堆 JS 文件,层级清晰,变量名和注释都还在。那一刻你会发现,自己花几个月写的玩法逻辑&#x…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬