☰
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 表格:核心玩法是 QStandardItemModel

3.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) { QList<QStandardItem *> 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_cast<QStandardItemModel *>(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 索引可能无效,别上来就取 row;item 可能为空指针,尤其是空行或者某些列没有数据的时候。做空指针判断,能省掉后面大量的崩溃现场。

如果你想拿到多行选中数据,比如用户勾选了好几行然后点击批量处理,可以遍历 selectionModel 的 selectedRows:

QModelIndexList 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, QList<QTreeWidgetItem *> &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); } } // 使用:从根节点开始 QList<QTreeWidgetItem *> 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 + 自定义 Model

QTreeWidget 方便,但它把所有节点都加载到内存里。数据量一大,创建几万个 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_cast<QStandardItemModel *>(m_table->model()); if (!model) return; model->removeRows(0, model->rowCount()); // 这里从业务数据容器中取当前分组的数据 const QList<DeviceRecord> records = m_dataStore.recordsByGroup(group); for (const DeviceRecord &rec : records) { QList<QStandardItem *> 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_cast<QStandardItemModel *>(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改成整体 reset:

model->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 或唯一 ID

6.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 绘制和真正的海量数据优化单独写一篇,把数据展示这条线彻底讲透。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询