QT树形列表菜单实战:从QTreeWidget到自定义模型
QT学习制作树形列表菜单从入门到摸清门道最近折腾QT树形列表菜单踩了不少坑也总结出了一套能直接复用的实现思路。如果你正在搜QT安装、QT界面设计、QT项目实战这些关键词大概率也是被树形结构“卡”过一下。这类控件在桌面端几乎绕不开导航侧栏、设备管理列表、分类目录、日志过滤树全都靠它撑场面。这篇文章不打算堆教科书概念而是按我自己的调试路线把树形菜单从最简单写法到老板看了会加鸡腿的工程级做法完整走一遍。先说结论QT里做树形列表表面是“怎么往界面上塞几个节点”背后其实是一整套模型、视图、代理、样式表如何协作的问题。初学者最容易一头扎进QTreeWidget使劲堆代码进阶后才发现QTreeView加上自定义模型才是王道。这篇文章适合刚学完QT基础、准备做第一个完整控件的朋友也适合已经会拖控件但想搞清楚性能、扩展性和数据解耦的开发者。1. 先想清楚你需要的到底是QTreeWidget还是QTreeView1.1 两种控件的核心差异我第一次做树形菜单时判断标准特别朴素哪个拖上去能用就选哪个。实际工程里这俩差距非常大。QTreeWidget是“便捷类”内部默认帮你配好了一套隐藏的模型你只管往item上塞文字、图标、数据属于快速原型的好手。QTreeView则是“框架类”它本身不存任何数据所有显示内容全靠外面的模型来喂属于正规军路线。做一个类比QTreeWidget像街边快餐店点单即出餐效率高但菜品固定QTreeView像自己开火做饭食材、火候、摆盘都自己控制前期费功夫后期什么菜都能做。如果你的树形菜单只在某个小面板里固定几层节点用QTreeWidget完全够。如果这个树数据量上千、结构频繁变化、还可能要对接数据库或网络数据那就必须考虑QTreeView加自定义模型否则后面性能和维护都会很痛苦。1.2 选型考虑的三个硬指标我一般按下面三个维度做选型大家可以拿着这张表对着需求勾选判断维度用QTreeWidget用QTreeView 自定义模型数据量几百个节点以内也还行上千甚至上万节点依然流畅数据来源临时构造、手工填写数据库、JSON、协议数据、动态加载需求变化速度界面固定、逻辑简单经常要加列、加角色、加拖拽排序新手友好度上手极快需要理解Model/View架构另外有一个容易忽略的点QTreeWidget虽然写起来爽但它默认帮你封装了模型逻辑一旦遇到“同一份数据要在树、表格、下拉框里联动显示”这种需求就会很尴尬。数据不一致、刷新不同步、到处要手动同步都是我在实际项目里遇到过的烦恼。反过来如果你用Model/View架构同一份模型可以挂到多个视图上数据自动同步省心不是一点点。2. QTreeWidget快速实现新手也能写出能跑的树形菜单2.1 五分钟搭出三层导航结构如果你目前只想要一个能演示、能点击、能收起展开的树那我推荐直接上手QTreeWidget。操作逻辑不复杂先建树控件再建节点接着把节点一层层挂进去。我一般把这段代码放在构造函数或者初始化函数里思路非常直白// mainwindow.cpp 片段 #include QTreeWidget #include QHeaderView // 在MainWindow构造函数或初始化函数里 QTreeWidget *tree new QTreeWidget(this); tree-setColumnCount(1); // 只显示一列够用 tree-setHeaderLabel(功能区); // 创建顶层节点 QTreeWidgetItem *topItem new QTreeWidgetItem(); topItem-setText(0, QString(项目管理)); tree-addTopLevelItem(topItem); // 给顶层节点挂子节点 QTreeWidgetItem *childItem1 new QTreeWidgetItem(); childItem1-setText(0, QString(任务列表)); topItem-addChild(childItem1); // 给子节点继续挂孙节点 QTreeWidgetItem *grandItem new QTreeWidgetItem(); grandItem-setText(0, QString(已完成任务)); childItem1-addChild(grandItem); // 默认全部展开省去反复点击 tree-expandAll();这段代码跑起来窗口左上角就会出现一个三层结构的树形列表顶层是“项目管理”下面有“任务列表”任务列表下还有“已完成任务”。我在第一版项目里就靠这十几行代码在两天内撑起了整个导航框架。不过别高兴太早真正写业务代码时你会发现光有文字还不够还得处理图标、右键菜单、双击事件、数据绑定。2.2 绑定数据的小技巧别用text硬编码很多教程到这里就结束了但实际项目里节点通常要跟业务ID关联。比如你点击“任务列表”程序需知道它对应数据库里的哪一条记录。初学时我喜欢用text直接存ID比如“123_任务列表”取的时候再字符串切割麻烦且容易出错。后来发现QTreeWidgetItem自带一个数据存储机制可以塞任意类型的数据// 保存数据把任务ID存入item item-setData(0, Qt::UserRole, QVariant(10086)); // 读取数据从item取出业务ID int taskId item-data(0, Qt::UserRole).toInt(); qDebug() 当前节点对应的任务ID是: taskId;Qt::UserRole就好比给每个节点额外开了个小抽屉专门放业务数据。点击事件里不用再去解析文字直接从小抽屉里取ID既稳定又高效。这里提醒一句setData存储的是QVariant所以整数、字符串、对象指针都能存但对象指针要小心生命周期最好存ID而不是裸指针。2.3 右键菜单与编辑开关树形菜单几乎没有不加右键菜单的。实现方式不复杂在QTreeWidget上设置上下文菜单策略然后在槽函数里判断用户点的是哪一项// 开启右键菜单触发 tree-setContextMenuPolicy(Qt::CustomContextMenu); // connect 之后写槽函数 connect(tree, QTreeWidget::customContextMenuRequested, this, MainWindow::onTreeContextMenu); void MainWindow::onTreeContextMenu(const QPoint pos) { QTreeWidgetItem *item tree-itemAt(pos); if (!item) { QMenu menu(this); menu.addAction(新建顶层节点); menu.exec(tree-viewport()-mapToGlobal(pos)); return; } // 右键点击在了有效节点上根据层级给不同菜单 QMenu menu(this); if (item-parent() nullptr) { menu.addAction(添加子节点); } else { menu.addAction(删除当前节点); menu.addAction(重命名); } menu.exec(tree-viewport()-mapToGlobal(pos)); }比较隐蔽的问题是menu.exec的位置映射。初学者往往直接用pos做全局坐标结果菜单跑到屏幕左上角去。正确做法是经过viewport()-mapToGlobal()转换这属于不踩一次记不住的经验。重命名这块简单做法是设置editTriggerstree-setEditTriggers(QAbstractItemView::DoubleClicked | QAbstractItemView::EditKeyPressed);这样双击节点就能原地修改文字。但如果你想限制只有通过右键菜单才能重命名那就不要开全局编辑触发改成在菜单动作里调用tree-editItem(item)灵活度更高。3. 进阶操作必学QTreeView加自定义模型把数据与界面彻底解耦3.1 为什么QTreeWidget重数据后开始卡顿我实际遇到过一次很典型的卡顿一个设备配置工具树形列表里要展示几千个设备节点每个节点下面还有配置文件、日志、报警记录。最初用QTreeWidget实现删除一个节点时界面竟然卡了半秒。原因很简单QTreeWidget把数据和视图绑得太紧每次插入、删除节点都要通知视图全量刷新节点一多性能和代码结构都扛不住。于是趁着一次需求迭代我把树形菜单重写为QTreeView加自定义模型这也是我在项目里做过最正确的一次“技术债清偿”。Model/View架构最直观的理解是模型负责管理数据和上报变更视图只负责“照着模型给的指示去画”两者通过信号槽通信。你想要修改数据不用再手动找到对应控件里的item只要改模型里的底层数据然后发一个dataChanged信号视图会自动更新。3.2 手写一个能用的QAbstractItemModel自定义模型看起来吓人其实核心就几个纯虚函数index()、parent()、rowCount()、columnCount()、data()。我建议数据结构先用最简单的“节点类”来表示里面存储名称、父子关系、业务ID// treenode.h struct TreeNode { QString name; QString dataId; TreeNode *parent nullptr; QVectorTreeNode* children; ~TreeNode() { qDeleteAll(children); } };对应的模型头文件长这样// treemodel.h class TreeModel : public QAbstractItemModel { Q_OBJECT public: explicit TreeModel(QObject *parent nullptr); ~TreeModel() override; QModelIndex index(int row, int column, const QModelIndex parent QModelIndex()) const override; QModelIndex parent(const QModelIndex child) const override; int rowCount(const QModelIndex parent QModelIndex()) const override; int columnCount(const QModelIndex parent QModelIndex()) const override; QVariant data(const QModelIndex index, int role Qt::DisplayRole) const override; void addNode(TreeNode *parentNode, const QString name, const QString dataId); private: TreeNode *rootItem; };实现这两个最关键的索引函数时一定要理解QModelIndex的本质它其实就是一个指向模型中某个位置的三元组内部封装了行号、列号以及一个内部指针。很多人卡在index()和parent()想不明白我的经验是让QModelIndex的内部指针直接指向对应的TreeNode*行号列号仅作辅助会简单很多。QModelIndex TreeModel::index(int row, int column, const QModelIndex parent) const { if (!hasIndex(row, column, parent)) return QModelIndex(); // 从parent的内部指针取出父节点 TreeNode *parentNode static_castTreeNode*(parent.internalPointer()); // 如果父节点无效说明要找顶层节点 if (!parentNode) parentNode rootItem; // 越界保护 if (row 0 || row parentNode-children.size()) return QModelIndex(); TreeNode *childNode parentNode-children.at(row); return createIndex(row, column, childNode); } QModelIndex TreeModel::parent(const QModelIndex child) const { if (!child.isValid()) return QModelIndex(); TreeNode *childNode static_castTreeNode*(child.internalPointer()); TreeNode *parentNode childNode-parent; if (!parentNode || parentNode rootItem) return QModelIndex(); // 父节点位于祖父节点的第几行 int row parentNode-parent-children.indexOf(parentNode); return createIndex(row, 0, parentNode); }我当时第一次写完这两个函数编译通过后树形图一瞬间就显示出来了那种成就感确实爽。但这里必须先泼一盆冷水手写模型的过程中最容易出的问题是parent()返回错误导致树形结构错乱。解决办法也不难做一个临时的QTreeView跑起来把每层节点的名字和父子关系用qDebug打印出来对照检查就好。3.3 data函数给节点提供文字、图标、对齐方式data()函数是模型跟界面交互的核心枢纽。所有视图在绘制时都会调用它询问“某个位置、某个角色要显示什么”。Qt::DisplayRole负责显示文字Qt::DecorationRole负责图标Qt::UserRole则可以用来存放业务ID方便点击时取用。QVariant TreeModel::data(const QModelIndex index, int role) const { if (!index.isValid()) return QVariant(); TreeNode *node static_castTreeNode*(index.internalPointer()); if (!node) return QVariant(); switch (role) { case Qt::DisplayRole: return node-name; case Qt::DecorationRole: // 顶层节点给文件夹图标子节点给文件图标需要时再细化 if (node-parent rootItem) return QIcon(:/icons/folder.png); return QIcon(:/icons/file.png); case Qt::UserRole: return node-dataId; default: return QVariant(); } }很多初学者不理解Role机制其实本质就是“同一个坐标点、不同维度下的多种数据”。文字是一维颜色是一维图标是一维是否可编辑、是否可选中又是另一维。这种设计保证模型只需要一份数据就能同时满足各种视图的复杂绘制需求。3.4 插入与删除还要记得发信号自定义模型跟QTreeWidget还有一个非常大的不同你不能改完数据就撒手必须通过信号通知视图重新拉取。用QTreeWidget时你调用addTopLevelItem副作用由控件内部处理。但自定义模型里这些都要自己来void TreeModel::addNode(TreeNode *parentNode, const QString name, const QString dataId) { // 确定父节点为空则加到根 if (parentNode nullptr) parentNode rootItem; int row parentNode-children.size(); // 重要先调用beginInsertRows再真正插入数据 QModelIndex parentIndex createIndex(row, 0, parentNode); beginInsertRows(parentIndex, row, row); parentNode-children.append(new TreeNode{name, dataId, parentNode, {}}); // 重要以endInsertRows收尾 endInsertRows(); }这里有个细节非常关键beginInsertRows的parent参数要吃一个QModelIndex。如果你随便传一个默认构造的无效索引视图会认为你在顶层插入行显示就会错乱。我一开始直接把parentNode转换出来的行的行号写成0结果插入到同名节点下面时树形结构直接裂开。后来老老实实按照“从父节点构造索引”的流程来问题就消失了。4. 树形菜单美化与交互扩展4.1 用QSS让默认树不再“裸奔”默认的QTreeView外观有点朴素直接把控件丢给客户看视觉上肯定过不了关。好在QT支持QSS样式表可以像写CSS一样美化树控件。我多次实践后认为只需要重点配置三个部分整体背景、选中项高亮、展开箭头。QTreeView { background-color: #f8f9fa; alternate-background-color: #f1f3f4; border: none; outline: 0; } QTreeView::item { height: 32px; padding-left: 6px; border-radius: 4px; margin-bottom: 2px; } QTreeView::item:hover { background-color: #e9ecef; } QTreeView::item:selected { background-color: #3390ff; color: #ffffff; } QTreeView::branch { background-color: transparent; }这里有一个小坑要特别说明默认情况下树形菜单选中项的焦点框会带一个很丑的虚线边框即使用户用鼠标点击也会出现。需要在QTreeView的样式表或者代码里加上outline: 0;或者setFocusPolicy(Qt::NoFocus)。我见过很多半成品界面功能好好的就是选中后外面套个虚线框显得特别业余这个细节务必处理。4.2 复选框三态联动权限树的高频需求权限管理、配置项分组这类业务里树形菜单往往要带复选框而且要求父节点的勾选状态能联动子节点。QTreeWidget自带的setCheckState只能管单个项联动逻辑需要自己写。我在项目里封装过一个简单方法节点勾选变化时向子节点全量同步向上让父节点根据所有子节点状态重新计算。void MainWindow::onItemChanged(QTreeWidgetItem *item, int column) { if (column ! 0) return; Qt::CheckState state item-checkState(0); // 向下子节点跟随父节点状态 for (int i 0; i item-childCount(); i) { item-child(i)-setCheckState(0, state); } // 向上重新计算父节点状态 QTreeWidgetItem *parentItem item-parent(); if (parentItem) { int checkedCount 0; int childCount parentItem-childCount(); for (int i 0; i childCount; i) { if (parentItem-child(i)-checkState(0) Qt::Checked) checkedCount; } if (checkedCount 0) parentItem-setCheckState(0, Qt::Unchecked); else if (checkedCount childCount) parentItem-setCheckState(0, Qt::Checked); else parentItem-setCheckState(0, Qt::PartiallyChecked); } }注意使用这个功能时连接信号要避开初始化阶段。否则你正在建树时itemChanged就被反复触发导致半成品节点提前进入联动逻辑界面瞬间错乱。可以加一个bool isLoading标志位建树期间置为true槽函数里先判断这个标志再决定要不要走联动。4.3 排序与拖拽让树形菜单更符合直觉树形列表默认不打乱层级顺序但有些需求希望用户能自定义排列。QTreeWidget开启拖拽很简单两行代码搞定tree-setDragEnabled(true); tree-setAcceptDrops(true); tree-setDropIndicatorShown(true); tree-setDragDropMode(QAbstractItemView::InternalMove);InternalMove模式表示只允许在当前树内拖动跨控件拖入拖出会被禁止。但这里有一个体验问题默认的树拖拽没有“是否允许移动到某个节点上面”的判断用户可能把A节点拖到自己的子孙节点里逻辑就乱了。如果需要做合法性校验建议重写dropEvent。实际上如果你已经用了自定义模型拖拽的逻辑通常会放到模型中处理通过supportedDropActions和flags函数配合代码更规范。排序方面QTreeWidget可以直接setSortingEnabled(true)但你很快会发现点击表头排序后用户手动调整的排列顺序瞬间被打乱。对于侧边导航类菜单我默认不开排序只有数据展示类的树才考虑开。如果非要开建议排序后把用户ID重排映射回数据源避免“界面顺序”和“数据顺序”脱节。5. 实际项目中的坑与排查清单5.1 树形菜单常见报错与处理开发QT项目时有一些高频坑几乎人人会踩。我这里整理一份排查清单结合我在项目里的实际调试经验给大家省点时间。现象常见原因解决办法树节点显示不全文字被截断列宽固定太小或未设伸缩header()-setSectionResizeMode(QHeaderView::ResizeToContents)点击节点没有反应未连接clicked/itemClicked信号确认信号名称、入参带有QTreeWidgetItem*双击无法编辑文本editTriggers没开启设置DoubleClicked或调用editItem删除当前item后崩溃item有父节点但只删除指针未从父节点移除用parent()-removeChild(item)再delete树节点上的复选框联动失效itemChanged信号在初始化期间被覆盖加初始化标志位控制槽函数逻辑项目编译报serialport相关错误Qt安装时未勾选SerialPort模块安装时选择完整组件或通过维护工具添加模块关于“unknown module(s) in qt: serialport”这个问题我搜索热词时看到近期被问了很多次。这其实不是树形菜单的问题而是QT环境安装不完整。默认在线安装器如果只选了MinGW核心组件很多附加模块不会带上。解决办法有两个一是打开Qt维护工具勾选对应模块重新安装二是干脆下载离线安装包5.14或5.15.2完整版一次装齐。问这个问题的人很多顺手写在这里供大家排查环境时查对。5.2 数据量上千时的性能优化如果你节点数突破了5000即使QTreeView加自定义模型默认的“一次性全量展开”也会卡。不必慌张先做两件事一是默认收起所有节点只展开顶层二是把创建节点时的数据填充拆到鼠标展开的时机。简单说就是“懒加载”。懒加载实现其实不复杂。监听expanded信号当某个节点第一次被展开时再去数据库查询它的子节点动态添加到模型。我在一个设备树里实践过两万个设备节点首屏加载控制在200ms以内体验完全能接受。假如你的数据量是几万、几十万这种量级那还要考虑用QSortFilterProxyModel做过滤同时配合局部刷新只更新可见区域。5.3 代码在Windows和Linux上表现不一致QT是跨平台的但树形列表在不同平台的默认渲染差异很大。Windows上默认带网格线和系统主题色Linux上可能完全没有选中高亮。为了避免这种飘忽不定的表现建议在main函数里做一次全局样式统一int main(int argc, char *argv[]) { QApplication a(argc, argv); // 使用Fusion风格屏蔽系统差异 a.setStyle(Fusion); // 全局QSS也尽量统一 QFile qssFile(:/style/global.qss); qssFile.open(QFile::ReadOnly); a.setStyleSheet(qssFile.readAll()); MainWindow w; w.show(); return a.exec(); }Fusion风格的好处是它在所有平台长得几乎一样美术同学给的效果图校准完之后不会因为换一台机器就对不齐。注意用了全局QSS之后个别控件的细节样式有可能被覆盖排查时优先找QSS里的冲突规则。5.4 发布打包时的隐藏问题树形菜单本身不涉及什么发布难题但项目整体打包发布时经常有人踩坑。我发现提问很多集中回复一次。推荐直接用windeployqtWindows或linuxdeployqtLinux工具它会自动把用到的Qt基础库拷贝到可执行文件目录。操作顺序是先用Release模式编译生成exe然后在命令行进入exe所在目录执行windeployqt yourapp.exe如果项目用到了SerialPort、WebEngine、Charts这些模块务必再检查对应DLL是否被正确复制。我遇到过发布后程序启动闪退排查半天发现就是缺了Qt5SerialPort.dll。更好的做法是在打包前用依赖分析工具比如Dependencies或Process Explorer检查一遍所有依赖。6. 树形菜单还能玩出哪些花活6.1 多列展示把树变成信息面板树不是只能显示一列文本。设置列数大于1后每行可以同时展示多个属性。比如设备管理树第一列显示设备名称第二列显示在线状态第三列显示IP地址。在自定义模型里data()函数的column参数会告诉你当前问的是第几列你可以按列返回不同数据。QVariant TreeModel::data(const QModelIndex index, int role) const { if (!index.isValid()) return QVariant(); TreeNode *node static_castTreeNode*(index.internalPointer()); if (role Qt::DisplayRole) { if (index.column() 0) return node-name; else if (index.column() 1) return node-status; else if (index.column() 2) return node-ipAddress; } return QVariant(); }这种“树加列”的做法在导航和配置类的界面里非常实用一屏信息密度直接翻倍。只要注意列宽分配合理效果会比多个独立控件拼在一起好太多。6.2 自定义Delegate让节点拥有按钮、进度条、输入框QTreeView的每个单元格默认只能渲染文字或图标但用Delegate可以塞进任何自定义控件。例如文件下载列表里树形节点的每一行都带一个进度条和一个取消按钮。Delegate的原理是视图在绘制某个格子时委托Delegate返回一个全新的编辑或绘制逻辑而你甚至可以把控件实例放进去。我建议初学者先把基础树写扎实再碰Delegate。不过必须承认掌握了Delegate之后树形菜单就从“可用”进化到了“真正好用”。比如权限树里在某个节点上放一个下拉框切换操作权限用户体验直接上一个档次。6.3 树形结构与JSON、配置文件的互相转换业务系统里树形结构几乎都要跟JSON互相转换。树形菜单做完了保存用户自定义的导航顺序或者将后台管理的机构树加载进来都是常见需求。我在项目里一般用QJsonDocument做序列化和反序列化。一个简化的存储格式如下{ name: 项目管理, dataId: 100, children: [ { name: 任务列表, dataId: 10001, children: [] } ] }对应的写入逻辑就是递归遍历每个节点生成QJsonObject再装入数组。读取逻辑则反过来读一个节点就new一个TreeNode递归往下装配。这部分代码很容易写但容易产生深层递归问题如果树的层数特别深建议把递归改成显式的栈加循环稳妥很多。6.4 搜索与定位给树加上“CtrlF”树一长用户就想要搜索。最朴素的实现是文本框里输入关键词遍历所有节点把名称含关键词的节点展开并高亮。这里有一个性能优化点不要每次都全树遍历可以用QTreeView::visitAllItems类似的遍历器缓存一份文本到节点指针的映射后续搜索直接查内存表。我试过给两万节点的树做即时搜索用缓存表能做到毫秒级响应不卡界面。配合展开的话要注意“展开父路径”这个操作。确定匹配节点后从它的parent一路往上拿到根路径逐个setExpanded(true)用户才能看到命中节点挂在树上的位置。只高亮不展开用户根本不知道搜到的东西在哪个分支里体验会大打折扣。7. 一个完整的工程化实践建议7.1 代码目录与模块划分参考看完前文的实现有基础的朋友可能已经在脑子里拼出了一个大项目的样子。这里我再给出一个推荐目录结构无论个人项目还是团队协作都建议按这个思路组织MyApp/ ├── main.cpp ├── models/ │ ├── TreeNode.h │ ├── TreeNode.cpp │ ├── TreeModel.h │ └── TreeModel.cpp ├── views/ │ ├── MainWindow.h │ ├── MainWindow.cpp │ └── MainWindow.ui ├── delegates/ │ └── ProgressBarDelegate.h └── resources/ ├── icons/ └── styles/核心原则是模型不依赖视图视图不直接操作底层数据。业务层面需要修改树结构时只调用模型提供的addNode、removeNode、loadFromJson接口再由模型发信号驱动视图更新。这样将来换界面皮肤、换数据源互相之间都不会波及。7.2 调试树形菜单的三个好用小技巧一是用一个独立的demo窗口来跑树形结构屏蔽其他业务代码干扰专注观察模型层变化。我通常会在这个demo里打印每个节点的row、column、internalPointer地址方便定位模型索引错乱问题。二是善用QAbstractItemModel::debugTree()或者自己写递归打印函数每次增删节点后把树的结构完整打印一遍。树结构的视觉表现容易隐藏问题数据层面的打印能快速发现父节点指错、行号越界等情况。三是在进行大量“先删后插”操作时先把updatesEnabled关掉等批量操作完成后再一次性刷新界面。这个开关就像给视图贴了个“暂停刷新”的告示执行大批量变更时极其有效。tree-setUpdatesEnabled(false); // 执行批量增删节点的代码 tree-setUpdatesEnabled(true); tree-viewport()-update();8. 最后聊点实际操作中的体会树形列表菜单这个东西说简单拖个控件加两行代码确实能显示说复杂模型、视图、委托、样式表、序列化、懒加载每一环都能写出一整篇文章。我绕了一圈最大的体会是用QT做界面不能只背API要理解背后的“数据驱动界面”这套思想。QTreeWidget看起来方便但数据一复杂早晚要债QTreeView加自定义模型前期投入多一点但后面扩展起来是真的省心。如果你正在做自己的第一个QT项目我的建议是先用QTreeWidget把功能跑通建立信心然后把已经跑通的逻辑改成QTreeView加自定义模型认真走一遍从index到parent到data的实现过程最后再回过头来加样式、加委托、加懒加载。这个过程走完你会发现QT的很多控件背后都是同一套Model/View思想那些看似高深的列表、表格、树形联动底层逻辑其实是相通的。按这个路线练上两三个小项目你手里的QT技能就算真正落地了。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →