恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Qt数据展示控件详解:列表表格树形与联动布局
首页
资讯中心
/
Qt数据展示控件详解:列表表格树形与联动布局
Qt数据展示控件详解:列表表格树形与联动布局
发布时间:2026/10/14 4:07:59
这个系列写到第4篇后台问我“数据怎么显示”的人越来越多了。前几篇把按钮、输入框、单选复选这类交互控件梳理了一遍这回轮到列表QListWidget、表格QTableView/QTableWidget、树形QTreeWidget/QTreeView这几个“数据展示型”控件了。说实话这几个控件才是绝大多数桌面工具里最不好讲清楚的部分因为它们表面上是控件使用问题实际是数据组织问题。这期内容适合两类人看一类是刚把基础控件跑通、准备做正经界面的初学者另一类是已经写了小工具、想在数据处理上少踩几个坑的进阶选手。如果你想看的是“某个控件怎么用”的速查可以直接跳到对应章节如果你愿意花十几分钟从源头理解后面所有操作都会顺很多。1. 为什么“控件指南(4)”要聊数据展示控件1.1 从单控件思维到数据驱动的界面前几篇里那些控件逻辑链条非常短按钮点了就发信号行编辑器读完就返回字符串数值框直接取数字。写起来痛快但真实项目不会一直这么简单。做设备台账、订单管理、文件浏览器、配置面板的时候你手里拿到的是一批结构化的数据不是单个字段这时候只懂“控件怎么用”远远不够你还得回答三个问题数据放在哪、怎么装进界面、用户选中某一条之后怎么回传。很多初学者在这里栽跟头是因为习惯把控件当成数据库把数据一项一项填进去结果界面和业务逻辑搅成一锅粥。QListWidget、QTableWidget、QTreeWidget 这类封装好的控件确实允许你“往里面塞数据”它们用起来顺手但背后藏着一套数据与界面分离的规则。你可以先不用完全理解但至少要意识到列表、表格、树不是简简单单的容器你选择哪一种实际上是在选择一种数据关系的表达方式。打个比方按钮像开关按下去灯就亮而数据展示控件像书架。书架上放什么书、按什么顺序放、用户抽出哪一本、抽出来之后你想不想记录这些都需要你自己定。控件本身只保证“书能放上去、能看见”不保证“放得聪明”。1.2 本篇的控件范围与选型逻辑这一篇我挑了四个主题QListWidget 列表、QTableView 表格带 QStandardItemModel、QTreeWidget 树形控件以及它们组合布局时几乎必然要用的 QSplitter。选这几样的理由很简单它们覆盖了桌面软件里最常见的三类数据展示场景——“一维明细”“二维表格”“树形分层”。QListWidget 适合展示单列或少列、总量在千行以内的数据比如日志列表、搜索结果、事件记录。QTableView 适合展示列固定、行可变、需要排序和编辑的二维数据比如资产清单、库存表、报价单。QTreeWidget 则适合天然带有父子层级的数据比如设备分组、项目目录、地区分类。至于 QSplitter它本身不做数据展示但它能把多个数据控件拼成一个可调宽窄的工作台界面组合实战那一节会看到它有多好用。选择哪一个控件核心看数据结构和用户操作方式。数据本身层次不深用列表数据是行列立体结构用表格数据有父子包含关系用树。不要一上来就追求复杂能用列表解决的事情不要硬套树形。2. QListWidget 列表控件从添加数据到信号处理2.1 核心接口与添加数据QListWidget 是我在实际项目里用得最多的轻量数据控件没有之一。它继承自 QListView封装好了默认模型和列表项 QListWidgetItem所以日常开发不需要关心 Model/View 的细节addItem 就行。// 创建一个列表 QListWidget *list new QListWidget(this); list-addItem(项目一); list-addItem(项目二); // 也可以用 QListWidgetItem 添加 QListWidgetItem *item new QListWidgetItem(项目三); list-addItem(item); // 批量添加适合静态数据 QStringList items; items A类 B类 C类; list-addItems(items);注意一个容易踩坑的点addItem 之后列表项的所有权就归 QListWidget 了你不需要也不应该去 delete 这个 item。反过来如果你调用了takeItem(row)把项从列表里摘出来那这个摘出来的项就需要你自己负责释放否则会泄漏。2.2 在列表项上挂业务数据列表界面显示的是给人看的文本但业务逻辑里你通常需要的是另一个数据比如数据库主键、文件路径、设备编号。这种“显示文本”和“真实标识”分离的做法QListWidget 天然支持用 Qt::UserRole 即可。QListWidgetItem *deviceItem new QListWidgetItem(设备A); deviceItem-setData(Qt::UserRole, DEV-001); deviceItem-setData(Qt::UserRole 1, 192.168.1.10); list-addItem(deviceItem); // 读取 QListWidgetItem *cur list-currentItem(); if (cur) { qDebug() cur-text(); qDebug() cur-data(Qt::UserRole).toString(); }这个做法非常实用。你可以在界面上任意修改显示文案只要不破坏 UserRole后面的业务逻辑照常运行。像我早期写代码经常犯的错是拿 text 去反查数据表一旦界面文案被用户改了整个查询就崩了。用 UserRole 挂真实标识就彻底绕开了这个问题。2.3 批量插入、删除和自定义 Item 的注意事项批量插入几千行的时候如果直接循环 addItem界面每插一行都要做一次布局计算肉眼可见地卡。正确姿势是先关闭更新插完再打开list-setUpdatesEnabled(false); for (int i 0; i 5000; i) { list-addItem(QString(条目 %1).arg(i)); } list-setUpdatesEnabled(true);删除项的时候要注意clear()会清空所有项并释放内部 item但如果你之前用item(row)拿了指针存起来clear 之后那个指针就是悬空指针再用就会出问题。取出来的项仅用于当前逻辑不要长期保存。如果列表项里面不只是一行字还需要放按钮、进度条、缩略图之类的东西可以用setItemWidget把一个普通 QWidget 塞进某一行。但这个方法有代价widget 开销比纯 item 大行数一多就吃内存。行数少没问题行数上了千我建议考虑 QListView 自定义 Delegate等后面有机会单独写一篇。信号处理这里有个很容易混淆的细节currentRowChanged 只在当前行变化时触发而 itemClicked 在用户点击任意一行时都会触发哪怕点击的还是当前行。你如果连接的是 itemClicked又在槽里读 currentItem会出现“点同一行也反复触发”的情况。做选中详情联动时用 currentRowChanged 更精准。3. QTableView 表格核心玩法是 QStandardItemModel3.1 先搞懂 Model/View 框架再谈表格很多教程一上来就贴 QTableWidget 的代码告诉你“表格就这么用”结果读者写完之后一旦想实现从文件加载数据、按列排序、动态更新就开始抓耳挠腮。QTableWidget 确实简单它相当于帮你预置了一个小模型但这个简单是有边界的。真正要驾驭表格必须理解 Qt 的 Model/View 架构。Model/View 架构拆开说就是三个角色Model 负责存数据、提供访问接口View 负责把数据显示在屏幕上Delegate 负责具体绘制和编辑。View 不知道数据是什么Model 不知道界面长什么样二者靠信号和索引通信。你操作表格里的格子其实是操作 Model 里的一个 QModelIndex。这个设计的好处是同一份数据可以用表格显示、用列表显示、用图表显示互不干扰数据可以从数据库、文件、网络等途径来View 不关心来源。代价是上手时比 QTableWidget 多一层概念。但这一层概念值得学因为它决定了你写出来的代码是“能跑的 demo”还是“能改的项目”。3.2 用 QStandardItemModel 快速搭出表格日常中小型项目里我不想直接写一个自定义 QAbstractTableModel 时就会用 QStandardItemModel。它把二维表数据放进内存自己能管理行、列、数据项配合 QTableView 非常顺手。// 创建表格控件 QTableView *table new QTableView(this); // 创建模型指定行数列数 QStandardItemModel *model new QStandardItemModel(this); model-setColumnCount(4); model-setHorizontalHeaderLabels({序号, 名称, 数量, 状态}); // 填充数据 for (int i 0; i 20; i) { QListQStandardItem * row; row new QStandardItem(QString::number(i 1)); row new QStandardItem(QString(零件%1).arg(i 1)); row new QStandardItem(QString::number((i 1) * 2)); row new QStandardItem(i % 2 ? 待检 : 已入库); model-appendRow(row); } table-setModel(model); // 表头拉伸列宽自适应 table-horizontalHeader()-setSectionResizeMode(QHeaderView::Stretch);这里有个默认行为要注意新建的 QStandardItem 默认是可编辑的用户双击单元格就能改。如果只想展示、不让编辑要手动关掉标志位QStandardItem *nameItem new QStandardItem(只读内容); nameItem-setEditable(false);如果整列都只读在填数据时统一处理即可。别等到用户误改了数据再来补救防呆设计要前置。3.3 排序、居中、列宽调整与数据修正表格类需求里排序基本是标配。QTableView 自带表头排序开关table-setSortingEnabled(true);开完之后用户点表头就能排序。但这里藏着一个很典型的坑如果模型里所有单元格存的都是文本数字列的排序会按字典顺序来。比如 2、10、3 排序之后可能变成 10、2、3。解决办法是给数字列存 QVariant 原生数值不要存字符串QStandardItem *countItem new QStandardItem(); countItem-setData(12, Qt::DisplayRole); // 以 int 存入这样排序时 Qt 才能用 int 比较而不是把字符串“12”和“2”当文本比。这个细节我在实际项目里被坑过一次排查了很久才发现是数据在填充时被QString::number()转成了字符串。列宽控制也建议单独设置不要所有列都用 Stretch。展示型表格适合table-horizontalHeader()-setSectionResizeMode(0, QHeaderView::ResizeToContents); table-horizontalHeader()-setSectionResizeMode(1, QHeaderView::Stretch);序号列按内容自适应名称列拉伸填充剩余空间视觉效果比平均分列好很多。3.4 获取选中行数据的正确姿势表格点击联动是高频操作。很多人第一反应是用table-selectedItems()但这个方法拿到的是一堆单元格而且如果用户按列选中拿到的列表很乱。我更推荐用 selectionModel 的 currentRowChanged 信号拿到当前行的索引后再去模型里取数据connect(table-selectionModel(), QItemSelectionModel::currentRowChanged, this, [](const QModelIndex current, const QModelIndex previous) { if (!current.isValid()) return; QStandardItemModel *mdl qobject_castQStandardItemModel *(table-model()); if (!mdl) return; int row current.row(); QStandardItem *nameItem mdl-item(row, 1); QStandardItem *countItem mdl-item(row, 2); if (nameItem countItem) { qDebug() nameItem-text() countItem-text(); } });注意两点current 索引可能无效别上来就取 rowitem 可能为空指针尤其是空行或者某些列没有数据的时候。做空指针判断能省掉后面大量的崩溃现场。如果你想拿到多行选中数据比如用户勾选了好几行然后点击批量处理可以遍历 selectionModel 的 selectedRowsQModelIndexList rows table-selectionModel()-selectedRows(); for (const QModelIndex idx : rows) { QStandardItem *idItem mdl-item(idx.row(), 0); if (idItem) listOfIds idItem-text(); }selectedRows 默认只返回有数据的列对应行索引数量可控遍历起来很干净。4. QTreeWidget 树形控件层级数据的组织与联动4.1 树形控件到底解决什么问题列表和表格适合表达“平铺”的数据但真实世界里很多数据是有层次的设备列表下面有子设备部门下面有小组目录下面有子目录。这种父子关系如果用表格硬撸要么堆一堆前缀空格要么加一列“父级编号”可读性都很差。树形控件就是专门解决“层级展示与折叠”这件事的。QTreeWidget 是封装好的 item-based 树控件创建节点、加子节点非常直观。它尤其适合节点数量几千以内的静态层级数据比如配置面板里的分类导航、文件浏览器的目录树。节点数到了几万或者数据本身来自动态查询那就要考虑 QTreeView 自定义 Model后面我会讲判断依据。4.2 创建节点、遍历节点与删除节点先看最基本的构建QTreeWidget *tree new QTreeWidget(this); tree-setColumnCount(2); tree-setHeaderLabels({名称, 类型}); // 顶层节点 QTreeWidgetItem *root new QTreeWidgetItem; root-setText(0, 设备分组); root-setText(1, 分组); // 子节点 QTreeWidgetItem *child1 new QTreeWidgetItem(root); child1-setText(0, 数控机床); child1-setText(1, CNC); QTreeWidgetItem *child2 new QTreeWidgetItem(root); child2-setText(0, 工业机器人); child2-setText(1, ROBOT); tree-addTopLevelItem(root); tree-expandAll();这里可以偷个懒QTreeWidgetItem 的构造函数接受父节点指针创建的同时就把父子关系挂好了不需要先 new 再 addChild。但注意addTopLevelItem和addChild都会接管 item 所有权父节点销毁时会一并清理子节点。遍历树是另一个高频操作。我想统计整棵树有多少有效节点或者找出某一类型的所有节点最好的办法是写一个递归遍历函数void foreachTreeNode(QTreeWidgetItem *parent, const QString targetType, QListQTreeWidgetItem * result) { if (!parent) return; for (int i 0; i parent-childCount(); i) { QTreeWidgetItem *child parent-child(i); if (child-text(1) targetType) { result.append(child); } foreachTreeNode(child, targetType, result); } } // 使用从根节点开始 QListQTreeWidgetItem * matches; foreachTreeNode(tree-invisibleRootItem(), CNC, matches);invisibleRootItem()是 QTreeWidget 的隐藏根节点用它做遍历起点最合适不会漏掉任何一个顶层节点。删除节点时有个容易出 bug 的点直接delete item之后如果这个 item 之前被局部变量保存着局部变量的指针就成了悬空指针。更稳妥的删除方式是先摘再删QTreeWidgetItem *cur tree-currentItem(); if (!cur) return; int index cur-parent() ? cur-parent()-removeChild(cur) // 返回索引并解除父子关系 : tree-indexOfTopLevelItem(cur); if (index 0) delete cur;不过实际开发中最省心的是直接使用delete currentItem前提是确认不会再用那个指针。4.3 节点勾选、itemChanged 信号与数据同步树形控件经常用来做权限树、标签树用户通过勾选节点选择归属。开启勾选很简单QTreeWidgetItem *item new QTreeWidgetItem; item-setText(0, 可勾选的节点); item-setFlags(item-flags() | Qt::ItemIsUserCheckable); item-setCheckState(0, Qt::Unchecked);开启后监听 itemChanged 信号就能拿到勾选变化connect(tree, QTreeWidget::itemChanged, this, [](QTreeWidgetItem *item, int column) { if (column 0 item-checkState(0) Qt::Checked) { qDebug() item-text(0) 被勾选; } });重点来了itemChanged 不只是用户点击触发代码里调用setText、setCheckState、setData也会触发。如果你在槽函数里又去设置勾选状态就会形成递归触发。最直接的规避方法是在程序性修改数据时 block 信号修改完再恢复tree-blockSignals(true); item-setCheckState(0, Qt::Checked); tree-blockSignals(false);我遇到过一种更隐蔽的情况勾选父节点之后希望自动勾选所有子节点。这时候如果在 itemChanged 里遍历子节点 setCheckState每个子节点又触发 itemChanged槽函数再次进入处理不当会越勾越乱。建议在遍历子节点之前先 blockSignals做完之后统一恢复或者用一个成员布尔变量做重入保护保证同一时间只有一次联动逻辑在执行。4.4 什么时候换成 QTreeView 自定义 ModelQTreeWidget 方便但它把所有节点都加载到内存里。数据量一大创建几万个 QTreeWidgetItem 的开销立刻就能感觉到。另一个问题是节点数据结构不好复用如果数据在数据库里每次都要从表记录转换成 item逻辑重复。这时候就该上 QTreeView 自定义 Model。QTreeView 只负责显示数据由 QAbstractItemModel 提供。你可以实现只加载当前展开节点需要的子节点真正做到“按需取数”。代价是至少要理解几个纯虚函数index、parent、rowCount、columnCount、data。这个组合我会在后面的系列里单独展开这里先给一个判断标准节点数几千以内树结构固定用 QTreeWidget。节点数过万、数据来自外部存储、需要动态增删模型用 QTreeView 自定义 Model。需要多列展示、排序、拖拽、拖放QTreeWidget 能实现但别扭建议 QTreeView。5. 组合实战树表格列表用 QSplitter 联动5.1 一个设备管理窗口的三区联动需求单独讲每个控件的用法哪怕讲得再细也解决不了“这些控件怎么拼在一起”的问题。所以这一节我带你做一个很常见的窗口左侧树形区域显示设备分组中间表格区域显示当前分组下的设备明细右侧列表区域显示选中设备的最近事件。这个小窗口几乎把所有数据展示控件的用法串起来了而且你会看到信号联动、批量刷新、布局控制这些细节是怎么在真实场景里落地的。不要因为看着稍微复杂就跳过这一节的组合方式可以直接套用到很多“分类明细详情”的工具里。5.2 用 QSplitter 搭建可拖拽布局如果只是把三个控件用 QWidget 手动排布窗口大小一变比例就乱了用户也没法自己调整宽窄。QSplitter 就是来解决这个问题的。它是 Qt 里的分割器能让子控件之间出现可拖拽的分隔条用户喜欢看宽一点的明细就往右拖一拖。// 假设三个成员控件已经创建 QSplitter *splitter new QSplitter(Qt::Horizontal, this); splitter-addWidget(m_tree); splitter-addWidget(m_table); splitter-addWidget(m_list); // 设置初始宽度单位是像素 splitter-setSizes({220, 520, 300}); // 设置伸缩因子窗口变宽时中间的表格优先拉伸 splitter-setStretchFactor(0, 1); splitter-setStretchFactor(1, 3); splitter-setStretchFactor(2, 2); setCentralWidget(splitter);setStretchFactor 的第二个参数不是固定宽度而是相对伸缩比例。中间表格设为 3两边设为 1 和 2窗口拉伸时表格获得更多增量空间。这个细节决定了缩放体验别忽略。5.3 树选中联动表格与列表的完整逻辑联动思路很简单树当前节点变化时取到节点文本作为分组标识刷新表格模型表格当前行变化时拿这一行的设备 ID刷新右侧列表。树的信号连接connect(m_tree, QTreeWidget::currentItemChanged, this, [](QTreeWidgetItem *current, QTreeWidgetItem *) { if (!current) return; const QString group current-text(0); refreshTableByGroup(group); });刷新表格的核心步骤是先断开表格选中信号避免刷新过程触发无意义操作清空旧数据填入新数据恢复选中信号。void MainWindow::refreshTableByGroup(const QString group) { // 断开表格选中信号 disconnect(m_table-selectionModel(), nullptr, this, nullptr); QStandardItemModel *model qobject_castQStandardItemModel *(m_table-model()); if (!model) return; model-removeRows(0, model-rowCount()); // 这里从业务数据容器中取当前分组的数据 const QListDeviceRecord records m_dataStore.recordsByGroup(group); for (const DeviceRecord rec : records) { QListQStandardItem * row; row new QStandardItem(rec.id); row new QStandardItem(rec.name); row new QStandardItem(QString::number(rec.count)); model-appendRow(row); } // 恢复表格选中信号 connect(m_table-selectionModel(), QItemSelectionModel::currentRowChanged, this, [](const QModelIndex cur, const QModelIndex ) { if (cur.isValid()) refreshEventListByDeviceId(cur); }); statusBar()-showMessage(QString(当前分组%1设备数量%2) .arg(group).arg(records.size())); }表格选中行变化后再刷新右侧列表void MainWindow::refreshEventListByDeviceId(const QModelIndex index) { QStandardItemModel *model qobject_castQStandardItemModel *(m_table-model()); if (!model) return; QStandardItem *idItem model-item(index.row(), 0); if (!idItem) return; const QString deviceId idItem-text(); const QStringList events m_dataStore.eventsByDeviceId(deviceId); m_list-clear(); m_list-addItems(events); }这里用 QString 传递 deviceId 只是演示真实项目里建议在 model 格子的 UserRole 里存整型 ID取的时候再取出来避免字符串转换的损耗和潜在错误。5.4 刷新数据时的性能细节与信号防抖刷新表格几十行数据不算什么但如果你的数据是几百上千行几个细节就变得很重要。第一批量更新时用 setUpdatesEnabled(false) 包住整个刷新逻辑刷完再恢复 true界面不会闪。第二removeRows 之后再 appendRow会频繁触发行插入信号如果表格数据量上千建议把removeRows改成整体 resetmodel-setRowCount(0);这样一次清空免去逐行通知。第三信号防抖。我在刷新表格前 disconnect 了 selectionModel是为了避免 removeRows 和 appendRow 过程中触发 currentRowChanged导致右侧列表跟着反复刷新。恢复连接之后用户重新选中一行才刷新。这里其实还有一种更干净的做法用QSignalBlocker作用在 selectionModel 和 table 上刷完就自动恢复QSignalBlocker blocker0(m_table-selectionModel()); QSignalBlocker blocker1(m_table);QSignalBlocker 的析构会自动恢复信号不用手动 reconnect比 disconnect 更不容易出错。我后面很多代码都换成了这个写法。6. 常见问题与排查技巧实录6.1 高频问题排查速查表下面这些坑都是我在写代码和帮人调试时反复遇到的。做成表格方便你直接查现象可能原因解决办法列表插入几千行卡顿每 addItem 一次触发一次重绘批量添加前后加 setUpdatesEnabled(false/true)currentRowChanged 在刷新时频繁触发模型重建过程改变了选中状态刷新前用 QSignalBlocker 临时屏蔽表头排序后数字顺序不对数字列存成了字符串使用 setData 存入 int而不是 QString::number树节点勾选后 itemChanged 反复触发代码里 setCheckState 导致信号递归程序性修改前调用 blockSignals删除树节点后程序崩溃指针引用悬空 item删除后不再使用旧指针注意父节点所有权表格选中拿不到数据用了 selectedItems 而选中状态是选区模式改用 currentIndex 或 selectedRows 取行索引自定义角色数据取不出来UserRole 的 QVariant 类型传错存的时候和取的时候保证类型一致QListWidget setItemWidget 后样式不生效原生 widget 不响应 QSS给内嵌 widget 单独设样式或改用 Delegate表格排序后行号错乱排序后的视线行号不等于模型行号操作数据时始终基于 index.sibling 或唯一 ID6.2 关于信号触发几个容易搞混的点QListWidget 有两个信号最容易让新手懵一个是 currentRowChanged(int)一个是 itemClicked(QListWidgetItem *)。前者只在当前行变化时触发后者点击任意一行都触发哪怕点的是同一行。如果你在 itemClicked 里通过 currentItem 去拿数据用户重复点击同一行数据是拿到了但代码逻辑却执行了多遍后面再做耗时操作就会导致界面卡。做“选中详情联动”时优先用 currentRowChanged。QTreeWidget 的 itemChanged 则是最容易制造恶性循环的信号。它包含文本变化、勾选变化、图标变化、数据角色变化几乎什么都能触发。只要你在槽函数里写了 setText 或者 setCheckState就可能再次触发。处理这类联动的铁律是先判断当前变化是自己关心的类型再执行并且所有程序性修改都用 blockSignals 包住避免信号递归。QTableView 这边的信号嵌套也不省心。selectionModel 的 currentRowChanged 和表格的 clicked 信号看着像实际触发时机完全不同。currentRowChanged 是选中状态变化时触发通过上下键移动时也会触发clicked 是鼠标点击完成后触发不含键盘操作。如果你希望键盘上下键也能联动详情就应该连 currentRowChanged。6.3 数据量上来之后界面卡顿怎么破说句实话QListWidget、QTableWidget、QTreeWidget 这三个封装控件都不适合海量数据展示。它们的 item 对象是独立分配的几万个节点对象光是创建和释放就要消耗不少时间。如果你的数据量到了“万”这个级别先别急着优化控件属性要检查几个方向第一是否在批量刷新时把信号全关了。有没有用 QSignalBlocker有没有关 update。很多卡顿其实只是界面在重复刷新和控件本身没关系。第二是否完全没必要用重量级控件。比如几千行的只读列表QListWidget 每一行一个 item改成 QListView 数据模型之后显示性能能明显改善。QTableWidget 同理换成 QTableView 自定义 Model让 View 只绘制可见区域性能会好很多。第三是否可以考虑开启某些性能选项。表格设置setUniformRowHeights(true)能省下计算行高的时间树控件如果不需要展开动画关掉动画也能减少绘制压力。我个人的经验是一万行以内的数据用封装控件完全没问题只要注意上面说的刷新细节如果数据超过几万行就别硬撑了花点时间写一个精简的自定义模型收益远比想象的大。6.4 自定义角色数据丢失的排查思路很多人喜欢在 item 上挂业务数据比如setData(Qt::UserRole, record.id)这没问题。问题常常出在两个地方一是类型不匹配存入时 QVariant 包装的是 int取的时候用 toString得到一个空字符串二是排序之后原来行号对应的数据变了拿 text 却没意识到这一行已经被排到别的位置了。排查这类问题我一般会先打印 QVariant 的类型名qDebug() cur-data(Qt::UserRole).typeName(); qDebug() cur-data(Qt::UserRole).isValid() cur-data(Qt::UserRole).toString();typeName 能直接告诉你里面装的是 int 还是 QString问题基本一眼就能看出来。如果是排序之后数据对不上解决思路是不要依赖视觉行号。取数时通过索引去模型里找而不是记一个“第几行”然后在另一个信号里反推。必要的时候可以在数据里单独加一列不可见的 ID保证唯一标识永远跟着数据走。写完这一篇我最大的体会就是数据展示类控件的难点根本不在“控件”本身前面几篇讲按钮、输入框我可以说“这些控件的坑很清晰”到了列表、表格、树情况就变了。它们真正考验的不是 API 记没记住而是你对数据关系的理解。控件只是壳子数据才是里子壳子选错了还可以换数据组织若是乱的后面所有功能都会跟着遭罪。我现在的习惯是动手写界面之前先画一张简单的数据关系图。哪一层是树哪一层是表格哪一层是列表它们之间的选中关系怎么联动先想清楚再回来写代码。这个习惯帮我少踩了一半的坑也让我后面改需求时不用推翻重来。回到这期内容QListWidget 解决一维列表QTableView 解决二维表格QTreeWidget 解决层级数据QSplitter 解决布局组合。把这四个组合拳打熟日常百分之八十的数据展示窗口都能顺顺利利搭起来。等再过一阵我再把自定义 Model、Delegate 绘制和真正的海量数据优化单独写一篇把数据展示这条线彻底讲透。