ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Qt树形列表菜单开发指南:从QTreeWidget到QTreeView+Model实战

Qt树形列表菜单开发指南:从QTreeWidget到QTreeView+Model实战 很多Qt初学者学完QLabel、QPushButton、QLineEdit这些基础控件之后会突然卡在一个地方我想做一个像样的、带侧边栏导航的桌面程序左边点一下右边内容跟着变但是左边这一栏到底用什么控件来实现答案就是树形列表菜单。无论是文件资源管理器左侧的目录树还是IDE里的工程文件列表甚至是系统设置里的分组导航背后都是同一套东西一个可以无限层级展开收起的树形结构。这篇文章就围绕这个话题展开从最基础的QTreeWidget暴力上手到工程上更推荐的QTreeView加QStandardItemModel标准姿势再到右键菜单、复选框、节点编辑、QSS美化、JSON持久化这些真实项目里躲不开的细节。文章基于Qt 5.15.x加Qt Creator测试代码在Windows和Linux下都通用你只需要会建QWidget工程、看得懂信号槽就能跟着一步步做出来。1. 先说选型QTreeWidget、QTreeView和QStandardItemModel到底怎么选1.1 三个方案各自解决什么问题打开Qt的帮助文档你会发现有两个长得非常像的类QTreeWidget和QTreeView旁边还站着一个QStandardItemModel。很多新手直接懵了到底该用哪个说人话就是QTreeWidget是“傻瓜版”它内部帮你把数据和显示捆在一起你new一个QTreeWidgetItem塞进去树就长出来了适合快速出效果、层级固定、节点数量不大的场景。QTreeView是“专业版”它本身只负责显示不存任何数据数据放在一个独立的Model里View只是把Model的内容画出来。这样做的好处是同一个数据源可以挂到不同的View上比如同一批数据左边用树形显示右边用表格显示底层数据不用动。QStandardItemModel就是那个“专业版”的默认Model选择。它像一个容器装着一堆QStandardItem每个Item可以携带文本、图标、复选框状态甚至能附加自定义数据。这套组合是Qt里最通用的MVC实现View是展示层Model是数据层Delegate是编辑层。你只需要操作ModelView会自动刷新界面。1.2 选型标准数据量、动态性和维护成本我把三个方案的差异整理成一个表方便你对照自己的项目做选择。方案上手难度数据量承载结构动态变化可扩展性QTreeWidget最低几行代码出效果千级以内没问题可以但代码写多了很乱一般深耦合QTreeView QStandardItemModel中等需要理解Model概念万级流畅方便操作Model即可较好数据与界面解耦QTreeView 自定义Model较高需要实现QAbstractItemModel接口十万级以上最灵活最强可对接数据库、文件系统我的建议非常直接如果你只是做一个配置项很少的管理工具或者课程作业直接用QTreeWidget别浪费时间。但凡是正儿八经要长期维护的工程或者节点数量可能膨胀到几千上万的从一开始就用QTreeView加QStandardItemModel。我见过太多项目用QTreeWidget写到中后期为了一个“给所有叶子节点加个标志”的需求不得不遍历所有QTreeWidgetItem代码又长又脆。换用Model之后这种需求只是改一个字段的事。关于Model/View架构可以用一个类比帮助理解View就像餐厅的菜单Model就像后厨的食材仓库。客人看菜单点菜时菜单变了不用动仓库仓库进了新食材也不用重印菜单。你只需要一个“服务员”Delegate在两者之间传话。这个理解到位之后后面所有代码逻辑都顺了。2. 5分钟搭一个能用的树形菜单QTreeWidget快速路线2.1 从零创建一个三层的设置导航树先用QTreeWidget做一版让你直观感受树形菜单长什么样。我以一个典型的“软件设置”导航为例顶级菜单是“常规设置”、“高级设置”每个顶级下面挂子节点。在Qt Designer里拖一个QTreeWidget到界面或者直接用代码创建都可以。先看纯代码的版本逻辑更透明#include QTreeWidget #include QTreeWidgetItem // 假设在MainWindow的构造函数里 QTreeWidget *tree new QTreeWidget(this); tree-setColumnCount(1); tree-setHeaderLabel(设置); // 创建一级节点 QTreeWidgetItem *generalGroup new QTreeWidgetItem(tree); generalGroup-setText(0, 常规设置); // 创建二级节点挂到一级节点下面 QTreeWidgetItem *accountItem new QTreeWidgetItem(generalGroup); accountItem-setText(0, 账号与安全); QTreeWidgetItem *networkItem new QTreeWidgetItem(generalGroup); networkItem-setText(0, 网络连接); // 再建一个一级节点 QTreeWidgetItem *advGroup new QTreeWidgetItem(tree); advGroup-setText(0, 高级设置); QTreeWidgetItem *logItem new QTreeWidgetItem(advGroup); logItem-setText(0, 日志查看); QTreeWidgetItem *resetItem new QTreeWidgetItem(advGroup); resetItem-setText(0, 恢复出厂); tree-expandAll(); // 默认全部展开这段代码有两点要注意当你new QTreeWidgetItem(tree)时这个Item已经自动挂到tree下面了不用再手动add。同理new QTreeWidgetItem(parent)也是直接挂到parent下面。看起来省事但这也是QTreeWidget代码容易失控的原因之一树的结构关系分散在大量的构造函数调用里改一处结构就要去翻好几处new。2.2 响应点击信号选择决定交互体验树建出来以后最重要的事情是让点击左侧节点时右侧内容跟着切换。QTreeWidget有多个信号可选itemClicked、currentItemChanged、itemActivated。这三者的区别非常关键itemClicked鼠标左键点一下触发不管有没有切换当前项同一个节点反复点击也会触发。currentItemChanged当前选中项发生变化时触发参数包含前一个和后一个Item。适合用来联动右侧页面切换。itemActivated双击或按下回车时触发语义上更像“确认选择”。做导航菜单我推荐用currentItemChanged因为它天然过滤了“重复点击同一个节点”的情况而且能同时拿到新旧两个Item方便做页面离开和进入的逻辑connect(tree, QTreeWidget::currentItemChanged, this, [](QTreeWidgetItem *current, QTreeWidgetItem *previous) { if (!current) return; QString key current-text(0); // 根据key切换到右侧对应页面 qDebug() 从 (previous ? previous-text(0) : 无) 切换到 key; });不过用text(0)当标识符有一个隐患一旦界面上显示的文字改了关联逻辑就断了。比如“账号与安全”改成“账户与隐私”代码里所有判断这个字符串的地方全部失效。更稳的做法是给Item挂一个稳定的ID这就是Qt::UserRole登场的地方。accountItem-setData(0, Qt::UserRole, account_page); // 后续取值 QString pageKey current-data(0, Qt::UserRole).toString();数据存到Item里显示归显示逻辑归逻辑互不干扰。这算是我接触Qt早期踩过坑之后总结出的一个经验永远不要拿用户看得见的文本当程序里的逻辑标识符。2.3 QTreeWidget的局限为什么项目一大就想换QTreeWidget上手确实快但它有个先天问题数据和视图绑在一起你想单独保留一份“当前树结构的逻辑数据”非常别扭。比如需求变成了“从配置文件读取树结构同时节点还要记录创建时间和最后修改时间”QTreeWidgetItem虽然也能塞数据但当你需要做整棵树的状态同步、撤销重做、批量更新时就得写一堆遍历代码维护成本直线上升。另一个典型场景是节点很多时的性能。QTreeWidget是一次性把所有Item都创建出来的节点上万时创建过程肉眼可见地卡顿。而QTreeView加Model走的是按需取数的逻辑View滚动到哪才向Model要哪一部分的数据这是两者性能差异的根源。所以如果你只是练手QTreeWidget完全够用如果你预感到这个树会越来越复杂我建议往下看第三部分直接养成用Model的习惯。3. 从玩具到工程QTreeView加QStandardItemModel的标准姿势3.1 Model/View的关系真正掌控数据的入口QTreeView本身不存任何节点数据它只是个“画家”。数据都在Model里View通过Model的接口去查询有多少行、每行显示什么、父节点是谁、子节点有几个。所以切入点是先建Model再往里塞数据最后把这个Model设置给View。我以“设备管理”这个场景为例树的顶级是设备分组第二级是具体设备每个设备携带设备序列号、状态等信息。代码是这样的#include QTreeView #include QStandardItemModel QStandardItemModel *model new QStandardItemModel(this); model-setHorizontalHeaderLabels(QStringList() 设备名称 状态); // 拿到隐藏的根节点所有顶层Item都挂在它下面 QStandardItem *rootItem model-invisibleRootItem(); // 一级传感器组 QStandardItem *sensorGroup new QStandardItem(传感器); sensorGroup-setData(group_sensor, Qt::UserRole); rootItem-appendRow(sensorGroup); // 二级具体设备 QStandardItem *sensor1 new QStandardItem(温度传感器-01); sensor1-setData(SN-TEMP-001, Qt::UserRole); sensor1-setData(运行中, Qt::UserRole 1); sensorGroup-appendRow(sensor1); QStandardItem *sensor2 new QStandardItem(湿度传感器-01); sensor2-setData(SN-HUMI-002, Qt::UserRole); sensorGroup-appendRow(sensor2); // 模型设置完成后让View显示它 QTreeView *view new QTreeView(this); view-setModel(model); view-expandAll();注意到invisibleRootItem()这个接口了吗它是整个树模型的虚拟根你创建的每一个顶级Item都得挂在它下面。很多从QTreeWidget转过来的人第一次都会在这里卡住为什么setModel之后树是空的八成就是忘了把Item挂到invisibleRootItem()下面。3.2 多列树第二列、第三列怎么填充树形菜单不一定只有一列。设备管理场景里第二列显示设备状态第三列显示最后通信时间都是很常见的需求。QStandardItem的规则是“按行”组织一行里的每一列都是一个独立的QStandardItem。QListQStandardItem * row1; row1 new QStandardItem(温度传感器-01); row1 new QStandardItem(运行中); row1 new QStandardItem(2025-01-06 10:23); QStandardItem *group new QStandardItem(传感器); group-appendRow(row1); rootItem-appendRow(group);这里有个很多人踩过的坑appendRow一行多列时你是调用这一行首个列的Item。比如上面代码里是group-appendRow(row1)因为group就是这一行的第一列。如果你拿到的是第二列、第三列的Item上面的逻辑就会混乱。原则是每一行的第一个Item才是这一行的“代表”层级关系都挂在它身上。3.3 用setData把业务数据藏进节点里Model/View架构里Qt::UserRole是最实用的角色之一。它就像一个抽屉你想存什么就存什么。设备和它的序列号绑定就是这样显示文本是“温度传感器-01”但程序真正需要的SN码藏在Qt::UserRole里。QStandardItem *item new QStandardItem(温度传感器-01); item-setData(SN-TEMP-001, Qt::UserRole); // 业务主键 item-setData(36.5, Qt::UserRole 1); // 实时温度值 item-setEditable(false); // 禁止在界面上直接编辑取数据时也非常方便QString sn currentIndex.data(Qt::UserRole).toString(); double temp currentIndex.data(Qt::UserRole 1).toDouble();QStandardItemModel里经常提“角色”很多新手被这个词吓到。可以理解为一个Item身上能带很多套衣服Qt::DisplayRole管理的是显示文本Qt::DecorationRole管理的是图标Qt::UserRole则是按你的需要自定义的一套衣服。你通过setData(value, role)穿上某套衣服再通过data(role)脱下来看。角色本质就是“给不同的数据贴不同的标签”。为什么推荐用QModelIndex去操作树而不是直接操作QStandardItem因为View发出的所有信号比如点击、双击、拖拽带的全是QModelIndex。你如果只会操作QStandardItem拿到Index后还得调用model-itemFromIndex(index)才能转回Item。反过来一旦你熟悉了通过Index接口配合Model去读写数据代码的可移植性会好很多未来如果换用自定义Model改动量也小。4. 右键菜单、复选框、节点编辑这些交互细节怎么做4.1 右键菜单contextMenuPolicy的正确打开方式树形菜单最常见的需求就是在某个节点上点右键弹一个功能菜单比如“新增子节点”、“重命名”、“删除”。实现思路很清晰先让控件发射customContextMenuRequested信号然后拿到鼠标的全局坐标在坐标位置弹一个QMenu。view-setContextMenuPolicy(Qt::CustomContextMenu); connect(view, QWidget::customContextMenuRequested, this, [this, view](const QPoint pos) { // 视图坐标转成模型Index这一步至关重要 QModelIndex index view-indexAt(pos); if (!index.isValid()) return; QMenu menu(this); QAction *addAction menu.addAction(新增子节点); QAction *renameAction menu.addAction(重命名); QAction *deleteAction menu.addAction(删除节点); QAction *selected menu.exec(view-viewport()-mapToGlobal(pos)); if (selected addAction) { // doAddChild(index); } else if (selected renameAction) { view-edit(index); // 直接进入编辑状态 } else if (selected deleteAction) { model-removeRow(index.row(), index.parent()); } });这里有两个经常出问题的细节。第一个是坐标转换信号给你的pos是控件内部的坐标也就是viewport坐标而QMenu::exec需要的是屏幕绝对坐标必须用viewport()-mapToGlobal(pos)转一下否则菜单会弹到奇怪的位置。第二个是空区域右键indexAt(pos)返回的是一个非法Index说明你点在空白处这种位置弹菜单往往不是用户想要的提前return掉。4.2 复选框让树形菜单变成可勾选的任务清单树形菜单不只是导航另一大用途是做权限分配、任务勾选之类的清单。每个节点前面带一个复选框选中父节点时自动级联勾选所有子节点这是几乎每个项目都会要求的交互。在QStandardItemModel里复选框的三态逻辑可以写得很优雅。Qt::ItemIsUserCheckable让节点支持勾选checkState记录勾选状态关键在于级联更新。我给出一套完整的实现void setNodeChecked(QStandardItem *item, Qt::CheckState state) { item-setCheckState(state); // 递归处理所有子节点 for (int row 0; row item-rowCount(); row) { setNodeChecked(item-child(row), state); } }父节点的状态需要根据所有子节点的状态反推只有全部勾选才是Checked全部未勾选才是Unchecked部分勾选就显示PartiallyChecked。连接itemChanged信号在状态变化时做联动这是最标准的做法代码量不大但逻辑要理清。复选框和右键菜单的组合在我做过的设备点检工具里是绝配左侧树勾选需要点检的设备右侧表格显示所有已选设备最后统一生成一条工单。如果没有树形List这个交互不知道要费多少功夫。4.3 拖拽、编辑、快捷键让树真正“活”起来树形列表菜单做出来不能是死的用户很可能想拖动节点改变归属。QTreeView自带拖拽开关view-setDragDropMode(QAbstractItemView::InternalMove); view-setDefaultDropAction(Qt::MoveAction);InternalMove是“仅在当前View内部移动”适合树内调整层级关系。如果你的树是作为菜单使用不建议开启拖拽容易误触如果是做部门调整、任务排序之类的功能这个开关价值很大。编辑能力默认情况下QStandardItem是可以双击编辑的。如果你的树是纯菜单不想让用户改文字记得设置不可编辑。model-setEditTriggers(QAbstractItemView::NoEditTriggers); // 只对访问者锁定注意大小写方法名是setEditTriggers不是setEditTrigger另外这个控件的很多属性方法放在View上Model只负责存数据别在这两者之间混淆。4.4 信号选择对比clicked、pressed还是currentItemChanged第三部分提过currentItemChanged适合导航场景。这里把树形控件常用信号做个对比遇到不同需求可以快速选。信号触发时机典型场景clicked左键按下后释放普通点击反馈pressed左键按下瞬间需要立即响应、判断拖拽起点doubleClicked双击编辑节点、打开文件activated双击或回车确认类操作currentItemChanged当前项切换时右侧页面联动、面包屑更新expanded / collapsed节点展开收起懒加载、埋点统计我的经验是导航联动用currentItemChanged编辑类操作用doubleClicked或activated需要拖拽逻辑时用pressed记录起始Index。不同信号服务于不同的交互语义选错了就会出现“连点两次页面跳两下”这种奇怪体验。5. 树形菜单的QSS美化与节点图标方案5.1 一套能直接用的树形菜单QSSQt自带的树形控件默认样式非常朴素如果没有自定义做出来的界面一眼“试验品”。好在这套控件的样式表定制能力很强我用一套比较通用但不花哨的QSS让树形导航看起来有现代感。QTreeView { background-color: #f8f9fa; border: none; outline: none; } QTreeView::item { height: 32px; border: none; border-radius: 6px; margin: 2px 8px; padding-left: 6px; color: #333333; } QTreeView::item:hover { background-color: #e9ecef; } QTreeView::item:selected { background-color: #c7d2fe; color: #1e3a8a; } QTreeView::branch { background-color: transparent; }点上这套样式之后树的项会有圆角悬停背景、圆角选中背景视觉上比默认的方块式好看不少。需要注意:hover、:selected这些状态选择器写的是QTreeView::item不是QTreeView刚开始学QSS的人经常把状态挂在控件上结果怎么都不生效。还有一个容易忽略的细节很多默认分支线在::branch这个伪元素上如果觉得展开收起的小箭头太丑可以通过QTreeView::branch配合border-image替换成自定义箭头图片。不过在实际项目里我通常只用简单的QProxyStyle去控制箭头在QSS里处理图片路径在不同平台下的相对路径问题有点烦不如直接用系统默认箭头只要整体配色统一效果也不会差。5.2 节点图标QStyle自带图标库的正确用法树形菜单只有文字会显得单薄给一级节点配几个图标界面质感立刻上来。Qt内置了一套标准图标不需要额外准备素材QTreeView *view new QTreeView(this); view-setIconSize(QSize(20, 20)); QStandardItem *sensorGroup new QStandardItem( style()-standardIcon(QStyle::SP_ComputerIcon), 传感器);style()-standardIcon返回当前样式下的标准图标日常开发够用了。但如果你想用自己的资源图更规范的做法是把图标文件放到qrc资源文件中然后用:资源路径/文件名.png的形式加载。QIcon groupIcon(:/images/folder.png); QIcon deviceIcon(:/images/device.png); QStandardItem *group new QStandardItem(groupIcon, 传感器); QStandardItem *device new QStandardItem(deviceIcon, 温度传感器-01);QStandardItem的构造函数可以直接传图标和文本比先创建再setIcon省一行代码。图标建议用PNG格式尺寸控制在16到24像素树形列表的行高通常是28到36像素图标太大容易挤压文字空间。5.3 给层级设置不同的缩进和字体树形菜单的层级感除了靠图标还靠缩进和字体粗细来强调。如果你想让一级节点加粗、二级节点正常显示可以结合QFont和行高设置void applyGroupStyle(QStandardItem *item) { QFont font item-font(); font.setBold(true); font.setPointSize(font.pointSize() 1); item-setFont(font); item-setSizeHint(QSize(0, 36)); // 让行更高 }这种“按层级差异化”的思路在一套配置树里非常实用。用户一眼扫过去就知道哪些是分组、哪些是叶子不用靠文字内容去猜。配合之前说的Qt::UserRole存层级标识就能在创建节点时统一处理不用到处散落if判断。6. 树形数据的保存与恢复JSON序列化实践6.1 为什么要做持久化树形菜单的配置比如用户自定义的分组、勾选状态、节点名总不能每次重启程序都还原成默认。好的做法是把树结构完整序列化到文件里下次启动时再重新加载。Qt对JSON的支持非常成熟QJsonDocument配合QJsonObject、QJsonArray几十行代码就能把一棵树完整保存下来。我选择JSON而不是配置文件的理由是树形结构天然就是嵌套的JSON的层级表达力最强现代桌面程序的配置用JSON也方便后续做导入导出。虽然Qt也有QSettings但那个适合键值对配置真正嵌套深的树结构用JSON更舒服。6.2 一个通用的树到JSON转换工具函数下面这套代码是我在多个项目里反复用的核心思想是递归每个节点转成一个JSON对象节点名放name字段业务数据放data字段子节点放children数组。这样一套结构既完整又不臃肿。static QJsonObject itemToJson(QStandardItem *item) { QJsonObject obj; obj[name] item-text(); obj[data] item-data(Qt::UserRole).toString(); QJsonArray children; for (int row 0; row item-rowCount(); row) { QStandardItem *child item-child(row); children.append(itemToJson(child)); } if (!children.isEmpty()) { obj[children] children; } return obj; } void saveTreeToFile(QStandardItemModel *model, const QString filePath) { QJsonArray rootArray; QStandardItem *root model-invisibleRootItem(); for (int row 0; row root-rowCount(); row) { rootArray.append(itemToJson(root-child(row))); } QJsonObject docObj; docObj[tree] rootArray; QFile file(filePath); if (!file.open(QIODevice::WriteOnly)) { qWarning() 无法写入文件 filePath; return; } file.write(QJsonDocument(docObj).toJson(QJsonDocument::Indented)); file.close(); }这里的关键是递归函数的出口叶子节点的children数组为空就不写入children字段保证生成的JSON尽量精简。写到文件时用Indented格式化方便调试时直接用文本编辑器查看如果是发布给用户用的配置可以改成Compact压缩体积。6.3 从JSON恢复树结构反向递归重建加载的过程是保存的镜像操作递归读取JSON逐个创建QStandardItem支持多列的情况可以把状态字段也恢复出来。static QStandardItem *jsonToItem(const QJsonObject obj) { QStandardItem *item new QStandardItem(obj[name].toString()); item-setData(obj[data].toString(), Qt::UserRole); QJsonArray children obj[children].toArray(); for (const QJsonValue childVal : children) { QJsonObject childObj childVal.toObject(); item-appendRow(jsonToItem(childObj)); } return item; } void loadTreeFromFile(QStandardItemModel *model, const QString filePath) { QFile file(filePath); if (!file.open(QIODevice::ReadOnly)) { qWarning() 无法打开文件 filePath; return; } QJsonDocument doc QJsonDocument::fromJson(file.readAll()); file.close(); QJsonObject docObj doc.object(); QJsonArray rootArray docObj[tree].toArray(); // 清空模型避免和旧数据叠加 model-clear(); QStandardItem *root model-invisibleRootItem(); for (const QJsonValue val : rootArray) { root-appendRow(jsonToItem(val.toObject())); } }加载前必须调用model-clear()这是很多新手容易忽略的一步。如果不清空反复加载配置会发现树越拉越长新旧节点混在一起。JSON持久化这节看起来简单但在实际项目里它几乎每次都能立竿见影地提升使用体验。用户调整好的树形菜单重启软件后原样恢复这种“记住了我的设置”的感觉是很多成熟软件的核心体验之一。7. 实测中的常见坑与排查思路7.1 树显示为空最常见的四个原因树形列表菜单开发里遇到最多的问题就是“明明写了appendRow界面却一片空白”。根据我的排查经验按照出现频率排原因大概有这么几个现象原因解决办法完全空白没调用setModel(model)View设置模型完全空白顶层Item没挂到invisibleRootItem()检查根节点挂载有表头没内容数据挂到了错误的父节点下逐行检查appendRow的调用对象有内容但反问号列数设置不合理通过setColumnCount设置列数我见过最诡异的一种情况是数据明明在Model里model-rowCount()返回正确但View死活不显示。最后发现是某段代码在设置Model之后又调用了一次view-setModel(nullptr)把模型清掉了。这种问题最好的排查方式不是肉眼盯代码而是在数据写入后加断点确认model-invisibleRootItem()-rowCount()的值是否符合预期这个值正常而界面不显示再去检查View的配置这个值本身就不对问题一定出在数据的写入逻辑上。7.2 程序崩溃关于父节点和子节点生命周期Qt里父对象会负责删除子对象这条规则在QStandardItem上依然成立但有个例外你要格外小心QStandardItem不属于QObject体系它的生命周期由Model管理Model析构时会delete所有Item。如果你的代码里又手动delete了某个Item就会造成双重释放程序直接崩溃而且崩溃时机不固定特别难查。一个典型的场景是右键删除节点你既调用了model-removeRow(index.row(), index.parent())又在之后手动delete pointerToItem。反复操作几次后程序就崩了而且堆栈信息指向的往往是无辜的其他代码。这是典型的“悬空指针加双重释放”问题。正确做法是一律通过model-removeRow或model-removeRows删除删完立刻置空指针绝不再手动delete任何通过Model取到的Item。这条规则从我开始用Qt到现在几乎每天都在提醒自己。7.3 复选框状态一闪而过复选框点击后状态没有保持往往是因为Item缺少Qt::ItemIsUserCheckable这个flag。QStandardItem默认情况下是可以勾选的但如果你在某个环节重设了flags就要自己把Qt::ItemIsUserCheckable加回来。item-setFlags(item-flags() | Qt::ItemIsUserCheckable); item-setCheckState(Qt::Unchecked);改flags的坑在于你等于把Qt默认给你配好的能力“覆盖”了。正确的姿势是在原有flags基础上做加减运算而不是直接写死一个全新的数字。7.4 QSS样式不生效时的排查思路树形菜单QSS写完之后往往刷新无效。最容易被忽略的情况是你设置样式的对象是QTreeView但实际生效的绘制对象是它的viewport。比如想改某项的背景色直接写QTreeView { background-color: ... }发现根本不生效。正确的做法分两层外层背景写在QTreeView上项的样式写在QTreeView::item上。如果你自定义了委托那么QSS的优先级会被委托的绘制代码盖过这一点尤其隐蔽。排查思路是逐层缩小范围先给QTreeView设置纯红色背景验证控件本身再逐步加上::item、:hover这些子选择器每一步都要确认生效后再继续。这样能快速定位是选择器写错还是委托代码内部把样式绘制覆盖了。7.5 展开状态丢失与懒加载思路树形菜单恢复时重置了模型展开状态自然丢失。你可能希望加载后保持上次的展开状态那么保存时要记录每个节点的展开布尔值恢复时再显式调用view-setExpanded(index, true)。如果树的层级特别深、节点特别多插入几千个节点时界面会卡顿一个优化思路是“懒加载”先只创建一级节点等到用户点击展开时再动态加载该节点的子节点。这涉及到监听expanded信号拦截展开事件后按需填充。connect(view, QTreeView::expanded, this, [this, model](const QModelIndex index) { QStandardItem *item model-itemFromIndex(index); if (item-rowCount() 0) { // 这里做真实的子节点加载 loadChildren(item); } });给“空节点”加一个占位子节点是懒加载里常用的技巧。否则用户看到的树没有展开箭头不知道还能展开。占位子节点加载时可以删除再填充真实数据。这个方案在设备树、文件树这种大数据量场景里能把启动速度从秒级优化到毫秒级。写到这里我给这篇树形列表菜单的分享做个收尾。回想我自己从QTreeWidget写起到被迫转向QTreeView QStandardItemModel再到自定义Model的过程最大的感悟是这类控件真正值钱的地方不在“把树画出来”而在“把树的数据结构设计好”。节点里存什么角色、用什么标识符、如何序列化、如何响应复杂交互这些才是决定你能不能把它稳定用在真实项目里的关键。如果你现在还在用QTreeWidget或者刚接触QTreeView建议把文章里的代码都自己敲一遍然后试着做一个至少三层、多列、带右键菜单和JSON持久化的完整Demo跑通之后你对Qt的Model/View框架会有完全不一样的理解。
返回列表