Qt Model/View 架构实战:从 Simple DOM Model 示例深入 QAbstractItemModel 与 QDomDocument

发布时间:2026/10/14 20:11:45
Qt Model/View 架构实战:从 Simple DOM Model 示例深入 QAbstractItemModel 与 QDomDocument 1. 从一个被低估的官方示例说起如果你装完 Qt 5.14.2 之后打开 Qt Creator 的欢迎界面点进 Examples 搜索框敲下 “Simple DOM Model”大概率会看到这么一个不起眼的小工程目录里躺着几个 .cpp、一个 .h外加一份 XML 文件编译运行后弹出一个左右分栏的窗口左边是树形结构右边是表格。很多人第一次跑完就关掉了觉得“哦就是个读 XML 的 demo”。但我要说这个示例是整个 Qt Model/View 体系里最值得反复咀嚼的入门样本之一。它用不到 300 行核心代码把QAbstractItemModel 的自定义实现、QDomDocument 的解析、QTreeView 与 QTableView 共享同一份数据模型这三件事串成了一条完整的链路。你把它吃透后面再去看 QFileSystemModel、QSqlTableModel 甚至自己写一个支持拖拽排序的复杂模型都会觉得有迹可循。这篇文章面向的是已经能写基本 Qt 界面、但对 Model/View 架构还停留在“会用 QTableWidget”阶段的开发者。我会把这个示例拆到骨头缝里它为什么这么设计、每个虚函数在什么时候被调用、QModelIndex 的 internalPointer 到底存了什么、两个视图共享模型时有哪些坑。全程基于 Qt 5.14.2 的实际代码你可以边看边对照源码验证。先说结论这个示例解决的核心问题是——如何把一份树状的 XML 数据用标准的 Model/View 接口暴露给 Qt 的任意视图控件。它不依赖任何第三方库纯 Qt 自带模块QtCore QtXml QtWidgets是理解“数据与视图分离”这件事最干净的教材。2. 整体设计与思路拆解2.1 为什么不用 QTreeWidget 而要自己写 Model刚接触 Qt 的人容易有个疑问QTreeWidget 三行代码就能显示一棵树为什么官方示例非要绕一大圈去继承 QAbstractItemModel这个问题不搞清楚后面看代码会一直别扭。QTreeWidget 是“便捷类”它把数据和视图揉在一起每个 QTreeWidgetItem 既存数据又负责显示。数据量小、结构固定的时候确实爽。但一旦遇到这几种情况就崩了同一份数据要同时用树和图两种方式展示数据来自数据库或网络需要懒加载数据变动频繁需要精确控制刷新范围。这时候便捷类要么做不到要么做起来极其丑陋。Simple DOM Model 演示的正是“一份数据、多个视图”的经典场景。它内部只维护一个 QDomDocument左树右表两个视图共用同一个 QDomModel 实例。你在左边点中一个节点右边表格立刻显示该节点的属性列表——这不是靠两个控件互相通信实现的而是两个视图各自向同一个模型要数据。这就是 Model/View 架构的威力视图之间零耦合全靠模型做单一数据源。从工程角度看自己写模型还有个隐性好处数据的生命周期完全可控。QDomDocument 归模型所有视图只是“借看”模型析构时数据自然释放不会出现控件销毁了数据还在内存里飘着的情况。2.2 QDomDocument 作为数据后端的取舍示例选择 QDomDocument 而不是 QXmlStreamReader这个决定值得说道说道。QXmlStreamReader 是流式解析内存占用低、速度快适合处理超大 XML。但它是“一次性”的——读过去就没了你想回头再访问某个节点得自己把数据存下来。而 QDomDocument 把整个 XML 加载成一棵常驻内存的 DOM 树每个节点都是 QDomNode 对象可以随时按需遍历、随机访问。对于这个示例的场景——数据量不大、需要反复按索引取节点——QDomDocument 是更合适的选择。模型里那个QModelIndex.internalPointer()存的就是 QDomNode 的指针准确说是 QDomNode 的副本地址视图每次要数据模型就顺着指针拿到节点读它的标签名、属性、子节点数。如果换成流式解析这套“按索引随机访问”的机制根本没法实现。代价也很明显整个 XML 常驻内存。示例里那份 XML 才几十行无所谓但你要是拿它去解析几百 MB 的配置文件内存直接爆掉。所以这个示例的定位很明确——教学样本不是生产级 XML 浏览器。真要做大文件浏览得换成懒加载模型节点展开时才去解析对应片段。2.3 树和表共享模型的架构逻辑这是整个示例最精妙的地方也是初学者最容易看晕的地方。同一个 QDomModel左边 QTreeView 拿它当树用右边 QTableView 拿它当表用凭什么答案在于 QAbstractItemModel 的接口设计本身就是“维度无关”的。模型只回答三个问题某个索引下有几个子项rowCount、有几列columnCount、每个格子显示什么data。至于这些子项该排成树还是排成表那是视图的事。具体到这个示例模型的顶层节点是 XML 的根元素每个节点有若干属性。当 QTreeView 访问时它把“子节点”理解为 XML 的子元素于是显示成一棵树当 QTableView 访问时它把当前选中节点的“子项”理解为属性列表每个属性一行于是显示成一张表。模型代码里其实做了区分——rowCount和data会根据 parent 是不是有效索引、以及当前是树模式还是表模式返回不同的内容。我当初看这段代码时卡了很久后来画了张图才理清模型内部其实维护了两套“视角”通过QModelIndex的层级关系来切换。根索引无效索引对应 XML 文档本身它的子项是根元素根元素的子项是它的属性表格视角或子元素树视角。这个切换逻辑藏在nodeFromIndex和index这两个辅助函数里是理解全篇的钥匙。3. 核心细节解析与实操要点3.1 QModelIndex 与 internalPointer 的配合机制QModelIndex 是 Model/View 架构里最核心也最抽象的概念。你可以把它理解成“数据的地图坐标”——它不直接持有数据只记录三样东西行号、列号、以及一个 void* 指针internalPointer。前两个好懂关键是这个 internalPointer。在 Simple DOM Model 里internalPointer 存的是 QDomNode 的地址。每次视图要访问某个格子Qt 会把对应的 QModelIndex 传回给模型的data()函数模型通过index.internalPointer()拿到 QDomNode再读它的内容。这样模型就不需要自己维护一个“行号到节点”的映射表索引本身就携带了定位信息。这里有个必须注意的细节internalPointer 指向的对象生命周期必须长于 QModelIndex。示例里 QDomNode 是 QDomDocument 的一部分只要模型不析构节点就一直有效所以安全。但如果你自己写模型时 internalPointer 指向的是临时对象视图一刷新就会访问野指针程序直接崩。我踩过这个坑当时用一个局部 QList 存节点函数返回后列表销毁界面一滚动就段错误查了大半天。另一个细节是createIndex的调用方式。示例里是createIndex(row, column, node)第三个参数就是 internalPointer。注意 row 和 column 必须和这个节点在父节点下的真实位置一致否则视图的选中、展开行为会错乱。我见过有人图省事全传 0结果树能显示但点不中就是因为索引坐标和实际位置对不上。3.2 rowCount、columnCount、data 三个虚函数的实现要点这三个函数是模型的“心脏”视图每次刷新都会疯狂调用它们所以实现时既要正确又要高效。rowCount的逻辑分两种情况。如果 parent 是无效索引代表根返回 1因为 XML 只有一个根元素如果 parent 有效返回该节点的子元素数量。示例里用node.childNodes().count()但做了过滤——只统计 Element 类型的节点跳过文本节点和注释。这个过滤很关键因为 XML 里标签之间的换行和缩进在 DOM 里都是文本节点不过滤的话树里会多出一堆空白项。columnCount简单树模式下返回 1每行一列表模式下返回 2属性名一列、属性值一列。示例通过判断 parent 是否有效来切换。data函数是重头戏它要根据 role 返回不同内容。DisplayRole 返回显示文本树模式下返回节点标签名表模式下第一列返回属性名、第二列返回属性值。这里有个容易忽略的点表模式下 parent 是当前选中的节点而 index 的行号对应的是属性序号所以取数据时要用parent.internalPointer()拿到节点再用index.row()去取第几个属性。这个“父节点定主体、行号定属性”的取值逻辑是表视图能正确显示的关键。提示调试模型时在 data() 里加一句 qDebug() 打印 role 和返回值能快速定位显示异常。但记得发布前删掉否则视图滚动时日志会刷屏。3.3 headerData 与节点属性展示的细节headerData负责表头和树头的文字。示例里树头返回 “Node”表头第一列返回 “Attribute”、第二列返回 “Value”。这个函数看着简单但有个坑它也会被频繁调用而且 role 可能是 DisplayRole 之外的多种类型必须判断 role 再返回否则会出现奇怪的显示。节点属性的展示还有个细节值得说XML 属性在 DOM 里是通过QDomElement::attribute()按名字取的但示例要列出所有属性就得用QDomNamedNodeMap遍历。element.attributes()返回的就是这个 mapcount()拿数量item(i)按序号取。注意属性在 map 里的顺序不保证和 XML 原文一致这是 DOM 规范决定的不是 bug。如果你需要保持属性顺序得自己在解析时额外记录。我在实际项目里遇到过属性顺序敏感的场景比如配置文件要求按特定顺序输出最后是在 QDomNode 上挂了个自定义的 QList 来记录顺序。这个示例没涉及但你要拿它改造成生产工具这点得提前想清楚。4. 实操过程与核心环节实现4.1 从零编译运行示例的完整步骤虽然示例是现成的但我建议你手动走一遍编译流程这样能顺便检查环境配置。第一步确认 Qt 5.14.2 的 QtXml 模块已安装。这个模块默认包含在完整安装包里但如果你当初装的时候精简了组件可能缺失。检查方法是在 Qt Creator 里新建工程时看能不能 includeQtXml或者直接看安装目录下有没有 QtXml 的库文件。第二步用 Qt Creator 打开示例工程文件.pro。工程里应该有QT xml这一行这是链接 QtXml 模块的关键。如果漏了编译时会报一堆 “undefined reference to QDomDocument” 之类的错。第三步直接构建运行。正常情况下会弹出一个窗口左边树显示 XML 结构右边表显示选中节点的属性。如果窗口空白八成是 XML 文件路径不对——示例用的是相对路径读取工作目录设置错了就加载不到。第四步故意改坏 XML 文件比如删掉一个闭合标签重新运行观察程序行为。示例对解析错误做了基本处理会弹出提示框。这个测试能帮你理解 QDomDocument 的容错边界。4.2 模型初始化与 XML 加载的关键代码模型构造函数里做了两件事加载 XML、建立根索引。加载部分用 QFile 打开文件QDomDocument 的setContent()解析。这里有个参数值得注意setContent的第二个参数是 namespaceProcessing示例传的是 false。如果你的 XML 用了命名空间得传 true否则标签名会带上前缀显示出来很难看。解析成功后模型把根元素存下来作为整个树的起点。注意setContent返回 bool失败时应该给出明确提示示例里用 QMessageBox 弹窗这是好习惯。我见过不少项目直接忽略返回值XML 坏了程序照跑只是界面空白排查起来很费劲。根索引的建立是通过createIndex完成的行号 0、列号 0、internalPointer 指向根元素。这个索引在index()函数里被反复用到是整棵树的锚点。4.3 index 与 parent 函数的递归逻辑这两个函数是模型里最绕的部分因为它们互为逆运算index()根据父索引和行列号生成子索引parent()根据子索引反推父索引。index()的逻辑是先通过nodeFromIndex(parent)拿到父节点然后取该父节点的第 row 个子元素用createIndex包装成新索引。注意这里要判断 row 和 column 是否越界越界返回空索引否则视图会访问到不存在的节点。parent()的逻辑反过来通过index.internalPointer()拿到当前节点再调parentNode()拿到父节点。如果父节点就是根元素返回空索引因为根没有父否则要计算父节点在它自己父节点下的行号用这个行号创建父索引。这个“计算行号”的步骤容易写错示例里用了一个循环去数虽然效率不高但逻辑清晰。注意parent()里计算行号时必须用和rowCount()一致的过滤规则只数 Element 节点否则行号对不上树会显示错乱。这个一致性要求是自定义模型最容易翻车的地方。4.4 两个视图的联动配置主窗口里创建 QTreeView 和 QTableView分别setModel()同一个模型实例。然后关键一步把树的 selectionModel 的 currentChanged 信号连到表的槽上槽里调用table-setRootIndex()把当前选中节点设为表的根。这一步是“联动”的实现方式。注意它不是让表去监听树而是通过 selectionModel 这个中间层。selectionModel 是 Qt 提供的选中状态管理器树和表各自有独立的 selectionModel但你可以把树的选中变化转发给表。示例里就是这么做的代码量很少但效果很直观。还有个细节表视图的setRootIndex传入的是树当前选中的索引。这意味着表显示的是“以选中节点为根”的属性列表。如果你选中根元素表显示根元素的属性选中某个子元素表切换到该子元素的属性。这个交互设计很符合直觉也是这个示例的亮点之一。5. 常见问题与排查技巧实录5.1 树显示空白或只有根节点这是最常见的现象原因通常有三个。第一XML 加载失败但没报错模型里根元素是空的。排查方法是检查 QFile 的打开路径用绝对路径试一次。第二rowCount里过滤逻辑写错把 Element 节点也过滤掉了。检查nodeType() QDomNode::ElementNode这个判断是不是写反了。第三index()函数里createIndex的 internalPointer 传了空指针视图拿不到数据。我遇到过一次特别隐蔽的XML 文件编码是 UTF-8 带 BOMQDomDocument 解析时把 BOM 当成了文本节点导致根元素前面多了个空白项树显示异常。解决办法是读取时用 QTextStream 设置编码或者干脆把文件存成无 BOM 的 UTF-8。5.2 表视图显示不出属性如果树正常但表空白先确认 selectionModel 的信号连接有没有生效。可以在槽函数里打个断点看setRootIndex有没有被调用。如果调用了还是空白检查columnCount在表模式下是不是返回了 2以及data函数里表模式的分支有没有正确取到属性值。另一个常见原因是headerData返回了空字符串表头看不见但数据其实在。把表头高度调大或者检查 headerData 的 role 判断。5.3 节点展开后子节点重复或错位这通常是parent()函数计算行号时过滤规则不一致导致的。比如rowCount只数 Element 节点但parent()里数了所有节点行号就偏了。解决办法是把过滤逻辑抽成一个独立函数两处都调用它保证一致。还有一种情况是index()里没有做越界检查视图请求了不存在的行返回了一个 internalPointer 为空的索引展开时就会出问题。加上if (row 0 || row rowCount(parent)) return QModelIndex();这行防御代码能避免大部分诡异现象。5.4 内存泄漏与析构顺序QDomDocument 是值语义的模型析构时它会自动释放一般不用担心。但如果你在模型里 new 了额外的对象挂到 internalPointer 上就必须在析构函数里手动 delete。我见过有人在 internalPointer 里存 new 出来的自定义结构体结果模型销毁后内存泄漏跑久了程序越来越卡。排查内存问题可以用 Qt 自带的分析工具或者简单粗暴地在析构函数里加日志看每个节点有没有被正确释放。问题现象最可能原因快速排查方法树完全空白XML 路径错误或解析失败打印 QFile 路径和 setContent 返回值只有根节点rowCount 过滤逻辑错误检查 nodeType 判断条件表无数据selectionModel 信号未连接在槽函数打断点节点错位parent 行号计算不一致统一过滤函数滚动卡顿data 函数里有耗时操作用 qDebug 计时5.5 从示例到生产几个必须改造的点示例代码为了教学清晰牺牲了不少生产环境需要的东西。第一它没有做懒加载大 XML 会卡死改造方向是节点展开时才解析子节点。第二它没有排序和过滤实际使用中经常需要按属性名排序得实现sort()函数。第三它不支持编辑如果要做 XML 编辑器得实现setData()和flags()返回可编辑标志。我个人的经验是拿这个示例做原型验证很快但直接上生产至少要补这三块。尤其是setData()写的时候要注意同步更新 QDomDocument 和视图刷新两者不一致会导致界面和数据对不上。6. 几个让我印象深刻的实操心得第一个心得关于调试。自定义模型出问题时视图往往只给你一个空白界面没有任何错误信息。我的做法是在data()、rowCount()、index()这三个函数入口加条件断点只在特定行号时中断这样能精准定位是哪个索引出了问题。比无脑加日志高效得多。第二个心得关于 internalPointer 的类型安全。示例里直接存 QDomNode 的地址但 QDomNode 是隐式共享的拷贝一份地址和拷贝对象本身行为不同。我建议在存之前先确认节点的生命周期必要时用QDomNode::cloneNode()做深拷贝虽然费点内存但省心。第三个心得关于视图性能。QTreeView 默认会对每个可见项调用data()多次不同 role如果data()里有字符串拼接或 DOM 遍历滚动会明显卡顿。优化方法是在模型里缓存常用数据或者用QAbstractItemModel::data()的 role 参数提前返回避免不必要的计算。第四个心得关于代码组织。示例把所有逻辑塞在一个类里几十行还能看上百行就乱了。我后来重写时把 XML 解析、索引管理、数据读取拆成三个辅助类模型只做接口转发维护起来轻松很多。这个重构思路对任何自定义模型都适用。最后说个扩展方向这个示例的 DOM 后端可以换成 QJsonDocument把 JSON 也用同样的模型展示出来。接口几乎不用改只换数据源。我试过用这套模型显示一份嵌套的 JSON 配置效果很好树显示层级、表显示键值对比专门的 JSON 查看器还顺手。你要是手头有类似需求不妨拿这个示例改改比从头写省事得多。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询