1. 项目概述与整体设计思路
1.1 这个标签页管理器到底解决什么问题
先聊下需求背景。标签页(Tab)是桌面软件里使用频率最高的交互形式之一,从浏览器、代码编辑器到各种配置管理工具,几乎离不开它。我在实际开发中接过好几个类似需求:做一个多文档编辑器,用户能自由打开多个文件,每个文件占一个标签页;做一个设备调试工具,每台设备单独开一个页面监控状态;甚至做一个内部数据看板,按模块动态增加页面。这些场景的共同点是同一个窗口内要容纳多个独立页面,而页面数量在程序运行时是变化的,不是写死的。
说白了,动态增删标签页就是桌面应用里“多页面管理”的底座。用户想新增一个文档就点按钮加一个 Tab,不想看了就点一下关闭按钮把 Tab 删掉,窗口不退出,程序不重启,数据不丢。这个能力听起来基础,但对交互体验影响极大,做得好用户完全感知不到,做不好各种小毛病会让人抓狂。
本文要做的,就是一个基于 Qt 的 QTabWidget 标签页管理器。它会实现两个核心动作:动态添加标签页、动态删除标签页,并且在增删过程中保证页面状态、索引跟踪和内存管理都正确。我会把完整可运行的代码贴出来,配合逐步解释,让大家不仅会抄代码,还能理解背后的机制,知道出了问题往哪个方向排查。
这个内容适合谁看?刚接触 Qt Widgets 没多久、想把桌面界面做活的初学者;以及在维护老项目时被各种 Tab 问题折磨的开发人员。只要你需要在自己的工具里加入类似“多页面”能力,这篇文章可以直接当作参考模板用。
1.2 方案选型:为什么直接用 QTabWidget 而不是自己拼
实现标签页效果,Qt 里其实有几条路可走。第一条自然是 QTabWidget,它是 QTabBar 和 QStackedWidget 的组合封装,内部已经处理好了标签切换和页面联动的逻辑;第二条是用 QTabBar + QStackedWidget 手动拼,这样Tab栏和页面区域是分开控制的,灵活性更高,但代码量明显增加;第三条是直接用 QStackedWidget 加按钮自己模拟标签外观,这就有点绕远路了,一般不建议。
我在项目里优先选择 QTabWidget,原因很实际:这个需求本质上是“页面管理”,页面之间的切换逻辑是标准化的,QTabWidget 已经替我们做完了。直接用封装好的组件,代码量少,出 Bug 的概率低,而且它提供了 addTab、removeTab、setCurrentIndex 这一整套接口,完全覆盖动态增删的需求。什么时候才需要手动拼 QTabBar?当你的标签栏需要高度定制,比如标签上要显示进度条、状态图标、右键菜单、拖拽排序,而 QTabWidget 默认满足不了的时候。但那是进阶话题,先把基础底座做好,后续再迁移不迟。工程上一个稳妥的做法,是用最简单可靠的方案先满足当前需求,为未来留好扩展点,而不是一开始就把复杂度堆上去。
1.3 动态增删功能的完整功能拆解
动笔写代码之前,先把目标拆清楚。一个功能完整的动态标签页管理器,至少要满足以下要求:
- 能通过按钮或菜单动态创建一个新的标签页,新页面自动成为当前页,标题能根据序号自动生成。
- 能通过标签栏上的关闭按钮,或右键菜单,关闭当前或指定标签页。
- 关闭后,当前选中状态要正确切换到相邻页面,不能发生“关闭一个页面,结果整个 Tab 区域空白”的尴尬情况。
- 删除页面时,页面中所有子对象(比如页面上放置的文本框、表格控件)能被正确释放,不能造成内存泄漏。
- 如果关闭的是任意位置的标签,而不是最后一个,索引变化逻辑也要正确,不会误删其他页面。
前面三点是功能需求,往深了想还连着两个技术点:一个是 Qt 的对象父子关系和内存管理机制,一个是 QTabWidget 的 currentIndex 与页面实际索引之间的关系。这两块不理解透,写出来的代码多半会在边缘场景翻车。后面我会用专门的章节把这两点讲清楚,再配合示例展开。
2. 核心机制准备:搞懂 QTabWidget 的底层逻辑
2.1 必知必会:QTabWidget 的常用接口
先过一遍 QTabWidget 的核心接口,这些都是实现动态增删要反复用到的成员函数:
| 函数原型 | 作用 | 返回/影响 |
|---|---|---|
| int addTab(QWidget *widget, const QString &text) | 向尾部追加一个新标签页 | 返回新页面的索引 |
| int insertTab(int index, QWidget *widget, const QString &text) | 在指定位置插入标签页 | 返回插入后的索引 |
| void removeTab(int index) | 移除指定索引的标签页,但注意它不会 delete 页面对象 | 索引重新排列 |
| int currentIndex() const | 获取当前显示页面的索引 | 没有页面时返回 -1 |
| QWidget *currentWidget() const | 获取当前显示的页面指针 | 没有页面时为 nullptr |
| void setCurrentIndex(int index) | 切换到指定索引页面 | index 越界时无效果 |
| void setTabsClosable(bool closeable) | 设置标签是否显示关闭按钮 | 默认 false |
| void clear() | 删除所有标签页 | 不删除页面对象 |
这里有一个新手很容易踩的坑:removeTab 只把标签从 Tab 栏上移除,并不会销毁对应的 QWidget 对象。如果你要用代码手动移除页面,需要自己决定那个页面对象是彻底销毁还是暂时保留。彻底销毁的方法一般是用delete widget(当 widget 是 new 出来的且没有被外部继续引用时)。如果只是临时隐藏页面、后续还会再用,removeTab 保留对象反而是好事。这个取舍要结合具体场景判断。
另外还有两个常用信号,是动态增删场景的“命脉”:
void tabCloseRequested(int index):用户点击标签上的关闭按钮时触发。注意这个信号默认只在 setTabsClosable(true) 后才有可能发出来,而且它只通知你“用户想关这个标签”,代码里要不要真关、怎么关,由我们自己决定。void currentChanged(int index):当前页面切换时触发,包括用户点击切换和代码里 setCurrentIndex 导致的切换。
接下来我会用实际代码演示怎么靠这些接口完成动态增删。
2.2 界面布局:MainWindow 与工具栏按钮的搭配
这次示例我选择 QMainWindow 作为主窗口,顶部用 QToolBar 放“新建标签页”按钮,中央区域放 QTabWidget。选择 QMainWindow 是因为它能直接支持工具栏、状态栏、Dock 等桌面应用常见结构,后面要扩展菜单、状态栏提示也更方便。
如果你做的是偏工具型的对话框程序,也可以用 QWidget 做根窗口,顶部放一排 QPushButton,效果一样,代码差别不大。核心不在于窗口类型,而在于 QTabWidget 与周边控件如何衔接。
界面的基本结构是这样的:
- QMainWindow(主窗口)
- QToolBar(顶部工具栏:新建按钮、关闭当前页按钮)
- QTabWidget(中央区域,承载所有动态页面)
这个布局非常直观,用户一眼就能理解“点新建就开新页面,点关闭就关掉当前页”。更重要的是它把核心逻辑都集中在一个窗口类里,便于讲解和复用。
2.3 对象父子关系与内存管理:为什么不会泄漏
QTabWidget 在内部持有的是 QWidget 指针,addTab 传入一个页面后,QTabWidget 并不会把这个页面设置为自己的子对象。这就意味着,如果页面对象是 new 出来的,且没有其他父对象,那么 removeTab 之后,这个 QWidget 就成了一块“失联”内存,如果没有手动 delete,就会泄漏。
但有一个例外:如果你在创建页面时显式指定了父对象,比如new QWidget(this),那么即便 removeTab 时没有手动 delete,等主窗口销毁时,Qt 的父子清理机制也会帮你把页面对象释放掉。在实战中,我建议这种做法:创建页面时传入一个明确的父对象(通常是主窗口或标签容器),同时在移除标签时主动 delete 页面对象。这样既不会在程序运行期间堆积无用对象,也不会在窗口销毁时产生二次释放问题,内存安全最稳妥。
为了演示得更清楚,我在代码里会刻意演示“手动 delete”的写法,并解释这样做的必要性。
3. 完整实现:从界面搭建到动态增删逻辑
3.1 一个可直接复制的完整示例
为了让示例尽量简单,便于读者复制跑通,我把全部代码放在一个 main.cpp 文件里。工程用一个最简单的 Qt Widgets Application 就能编译,不需要额外的资源文件。UI 部分也全部用代码搭建,不依赖 .ui 文件,这样可以往论坛、博客里一贴就能用。
下面是完整的实现,然后我会逐步拆解:
#include <QApplication> #include <QMainWindow> #include <QTabWidget> #include <QToolBar> #include <QAction> #include <QTextEdit> #include <QLabel> #include <QPushButton> #include <QVBoxLayout> #include <QWidget> #include <QMessageBox> class MainWindow : public QMainWindow { Q_OBJECT public: MainWindow(QWidget *parent = nullptr) : QMainWindow(parent) { // 中央标签容器 m_tabWidget = new QTabWidget(this); m_tabWidget->setTabsClosable(true); m_tabWidget->setDocumentMode(true); m_tabWidget->setMovable(true); setCentralWidget(m_tabWidget); // 工具栏 QToolBar *toolBar = addToolBar(tr("MainToolBar")); toolBar->setMovable(false); QAction *newAction = toolBar->addAction(tr("新建标签页")); QAction *closeAction = toolBar->addAction(tr("关闭当前页")); connect(newAction, &QAction::triggered, this, &MainWindow::onAddTab); connect(closeAction, &QAction::triggered, this, &MainWindow::onCloseCurrentTab); // 标签关闭请求 connect(m_tabWidget, &QTabWidget::tabCloseRequested, this, &MainWindow::onCloseTab); // 当前页变化时同步窗口标题(方便观察索引变化) connect(m_tabWidget, &QTabWidget::currentChanged, this, &MainWindow::onCurrentChanged); // 默认创建一个空白页,避免启动时界面空洞 onAddTab(); resize(900, 600); } private slots: void onAddTab() { static int tabCount = 0; ++tabCount; // 每个页面用一个 QTextEdit 作为示例, // 实际项目中可能是 QWidget 子类、表单、表格等任意组件。 QTextEdit *page = new QTextEdit(this); page->setPlainText(QString("这是第 %1 个页面,可以在这里放入自己的业务组件。") .arg(tabCount)); int index = m_tabWidget->addTab(page, QString("页面 %1").arg(tabCount)); m_tabWidget->setCurrentIndex(index); // 方便观察索引变化 statusBar()->showMessage(QString("已添加页面,当前索引:%1) .arg(m_tabWidget->currentIndex()), 2000); } void onCloseCurrentTab() { int index = m_tabWidget->currentIndex(); if (index >= 0) onCloseTab(index); } void onCloseTab(int index) { if (index < 0 || index >= m_tabWidget->count()) return; // 取出的页面指针 QWidget *page = m_tabWidget->widget(index); // 从标签容器中移除 m_tabWidget->removeTab(index); // 销毁页面对象,释放内存 if (page) delete page; statusBar()->showMessage(QString("已关闭页面,剩余标签数:%1") .arg(m_tabWidget->count()), 2000); } void onCurrentChanged(int index) { statusBar()->showMessage(QString("当前索引:%1,总标签数:%2") .arg(index) .arg(m_tabWidget->count()), 2000); } private: QTabWidget *m_tabWidget = nullptr; }; int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow window; window.show(); return app.exec(); } #include "main.moc"这段代码如果需要独立编译,注意在 .pro 文件里不需要额外配置,但如果用 CMake,也可以很容易地调整。如果是在 VS + Qt 插件环境,或者直接使用 Qt Creator,新建“Qt Widgets Application”项目,把 main.cpp 替换成上面的内容即可。
如果你不想用 moc 那套(即不想在 cpp 末尾包含 main.moc),也可以把所有槽函数改为普通成员函数,并在构造函数中用 Lambda 连接信号,同样可行。下面我给出一个完全“无 moc”的紧凑版本,适合快速验证:
#include <QApplication> #include <QMainWindow> #include <QTabWidget> #include <QToolBar> #include <QAction> #include <QTextEdit> class MainWindow : public QMainWindow { public: MainWindow(QWidget *parent = nullptr) : QMainWindow(parent) { m_tabWidget = new QTabWidget(this); m_tabWidget->setTabsClosable(true); setCentralWidget(m_tabWidget); QToolBar *toolBar = addToolBar(tr("MainToolBar")); QAction *newAction = toolBar->addAction(tr("新建标签页")); QAction *closeAction = toolBar->addAction(tr("关闭当前页")); connect(newAction, &QAction::triggered, this, [this] { static int n = 0; ++n; QTextEdit *page = new QTextEdit(this); page->setPlainText(QString("第 %1 个页面").arg(n)); int index = m_tabWidget->addTab(page, QString("页面 %1").arg(n)); m_tabWidget->setCurrentIndex(index); }); connect(closeAction, &QAction::triggered, this, [this] { int index = m_tabWidget->currentIndex(); if (index < 0) return; QWidget *page = m_tabWidget->widget(index); m_tabWidget->removeTab(index); if (page) delete page; }); connect(m_tabWidget, &QTabWidget::tabCloseRequested, this, [this](int index) { if (index < 0 || index >= m_tabWidget->count()) return; QWidget *page = m_tabWidget->widget(index); m_tabWidget->removeTab(index); if (page) delete page; }); } private: QTabWidget *m_tabWidget = nullptr; }; int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow window; window.show(); return app.exec(); }这个版本的核心逻辑与前面带插槽的版本完全一致,只是连接方式不同。实际项目里我会更推荐前者(有清晰函数名,便于后期维护和增加复杂逻辑),但初学阶段用 Lambda 版本跑通逻辑,理解起来会更直接。
3.2 动态添加标签页的实现原理
动态添加的核心在 addTab() 调用。addTab 接收一个 QWidget 指针作为页面内容,再接收一个标题字符串作为标签文本,然后把新页面追加到 QTabWidget 末尾,并返回新页面的索引。这里有一个细节:页面内容不一定是 QTextEdit,QTabWidget 可以承载任何 QWidget 子类。你可以在页面里放一个完整表单,放一个 QTableView,放一个自定义绘图控件,甚至放一个嵌套的 QTabWidget——这些都是合法的,QTabWidget 的优越性也正在于此,它把“内容组件”和“标签容器”解耦了。
示例中我用new QTextEdit(this)创建页面,并把父对象设为了主窗口。为什么不设为 m_tabWidget?原因在于,如果设为 m_tabWidget,等 removeTab 之后,这个页面的父对象仍然是 m_tabWidget,虽然 Qt 对象在父对象销毁时会自动清理,但你在程序运行期间 removeTab 后马上 delete,也没有问题。设不设父对象其实都不影响 removeTab 后手动 delete 的安全性。设父对象的主要好处是万一删除逻辑漏了,窗口销毁时也能兜底,不至于内存泄漏。从工程角度看,这个兜底很重要,所以我习惯在创建页面时带上父对象。
添加完成后立即setCurrentIndex(index),是为了让用户感受“点按钮马上看到新页面”的即时反馈。如果不设置,新页面虽然加上了,但用户停留在旧页面,容易误以为没点成功,体验很差。这也是很多半成品代码容易忽略的地方。
3.3 动态删除标签页:removeTab 和 delete 的正确配合
删除标签页时,关键关联是 tabCloseRequested(int index) 信号。setTabsClosable(true) 之后,每个标签页右上角都会出现一个关闭按钮,用户点击后 Qt 不代表我们关闭页面,而是发出 tabCloseRequested 信号,并把被点击标签的索引传出来。我们的槽函数拿到 index 后,要做三步操作:
- 根据索引取出页面对象:
QWidget *page = m_tabWidget->widget(index); - 从标签容器中移除该标签:
m_tabWidget->removeTab(index); - 销毁页面对象:
delete page;
为什么一定要在 removeTab 之后再 delete?因为 removeTab 会让 QTabWidget 重新整理内部索引,如果你先删除了页面对象,QTabWidget 还持有这个悬空指针,它内部的索引表和实际 widget 列表会不一致,后续任何与索引有关的调用都可能崩溃或产生未定义行为。所以顺序必须是先移除、后删除。
另外,delete page这一步非常容易被新手遗漏。如果只是 removeTab,页面对象确实会从界面上消失,但内存没释放,反复开关几十个页面,内存占用肉眼可见地往上涨。这种“慢泄漏”在项目上线后特别难排查,所以我干脆在写代码的第一步就把 delete 写上,形成习惯,后面省心很多。
3.4 关闭最后一个标签页的边界情况
如果用户把所有标签页都关了,QTabWidget 会变成一个空白区域,整个窗口看起来像“空壳”。这个状态本身不报错,currentIndex() 会返回 -1,但如果后续有代码拿这个 -1 索引去操作,就会出问题。
针对这个状态,处理策略取决于业务需求:
- 如果业务不允许完全没有页面,可以在 removeTab 之后判断 count() == 0,然后自动调用 onAddTab() 补一个新页面。
- 如果业务允许空状态,那就在界面空白区域放一个 QLabel 提示,比如“暂无打开的页面”,这需要配合 QStackedWidget 或创建占位页面。
- 更简单的方式,是像我们示例中那样,在 MainWindow 构造时先调用一次 onAddTab(),保证初始状态至少有一个页面,同时禁用“关闭当前页”按钮,当 count() 变为 0 时再启用新建功能。
我的项目经验是,不要把“允许空状态”和“不允许空状态”写死死在界面层,最好把判断逻辑收敛到一个函数里,比如bool maybeCreatePlaceholder(),便于以后调整。示例为了简洁,没有做这个封装,但读者在实际项目中可以自己抽出这样一个方法。
3.5 当前页索引跟踪与状态同步
当用户切换标签页、动态添加标签页、动态删除标签页时,QTabWidget 都会发出 currentChanged(int) 信号。我在示例里利用这个信号把当前索引和总数显示到状态栏,既方便调试,也方便观察索引变化规律。
实际项目中,同步状态通常要做更重的事情,比如:
- 标签页变化时保存上一个页面的编辑状态;
- 根据当前页类型启用/禁用不同的菜单动作;
- 更新窗口标题为“当前文档名 - 应用名”。
这里我最想提醒的是:信号里传出的 index 在槽函数执行期间一定是有效的,但如果你在这个槽里又进行了 addTab/removeTab 操作,那么后续的索引会立刻变化,必须重新获取,不能缓存。很多疑难 Bug 都出在“缓存 index 后没及时刷新”上。比如用户点了关闭按钮,信号回调里 removeTab,然后又在另一个地方的成员变量里保存了 oldIndex,下一次 addTab 后 oldIndex 可能就指向了错误的页面。
正确的做法是:凡是涉及索引的操作,尽量在同一个逻辑作用域内完成。如果非要在多个函数、多个类之间传递索引,尽量传 widget 指针,而不是索引值。等真正需要索引时,再用indexOf(widget)去查。这样能减少很多维护成本。
4. 常见问题与排查技巧实录
4.1 QTabWidget 中标签关闭按钮默认不显示
很多入门者第一次写动态删除标签页时,会疑惑“为什么标签上没有关闭按钮”。原因是 setTabsClosable(true) 没设置。默认情况下 QTabWidget 的标签栏是纯粹的文本标签,没有关闭按钮,自然也没有 tabCloseRequested 信号发出来。
如果你发现标签上没有“×”,优先检查两点:
- 是否调用了
m_tabWidget->setTabsClosable(true); - 是否在 setTabsClosable 之后又对 tabsClosable 属性做了误修改,比如样式表里隐藏了 close button。
另外,示例里我还设置了setDocumentMode(true)和setMovable(true)。documentMode 会让页面边框更平滑,看起来更像现代浏览器的标签页;movable 允许用户拖动标签排序,对于多标签管理场景,这个交互几乎属于标配,建议打开。但要注意,setMovable 后标签顺序会变,之前缓存的 index 就不再可靠了,务必用 widget 指针或重新查询 index。
4.2 removeTab 后页面还在内存里吗
这是后台问得最多的问题:移除标签后,页面为什么还占内存?原因前面已经讲了,removeTab 只负责“解除标签关联”,不负责销毁对象。如果你不手动 delete,这个对象会一直被持有到父对象销毁,或者程序退出。
处理方案总结如下:
| 场景 | 推荐做法 |
|---|---|
| 页面是一次性业务,关闭后不再使用 | removeTab 后立即 delete |
| 页面需要保留,后续可能要恢复 | removeTab 后不 delete,把指针存到容器中管理 |
| 不希望自己管理内存 | 创建页面时指定父对象,靠 Qt 父子机制兜底 |
| 不确定某个页面是否被多方引用 | 用 QPointer 管理指针,delete 后自动置空,避免悬空访问 |
我实际开发中最常用的组合就是“创建时指定父对象 + 移除时手动 delete”。这个组合逻辑清晰,不管是代码审查还是后续维护,都不会产生歧义。
4.3 关闭当前标签后,当前页跳到奇怪的位置
为什么会“跳到奇怪的位置”?因为 QTabWidget 在 removeTab 后会自动选择一个新的当前页,它的规则是:移除的是当前页,则尽量选中原来位置的下一个页面;如果移除的是最后一个页面,则选落在新的最后一个页面。这个行为本身是合理的,但如果页面对象是 delete 的,而某些代码又在槽函数里访问了“已经销毁的页面”,就会导致崩溃或状态错乱。
举例说明:假设当前索引是 2,你 close 了索引 2 的标签,removeTab 之后新的 currentIndex 也许还是 2(因为原来的索引 3 变成 2 了),但如果你的代码在 close 槽里先存下了“旧 currentIndex = 2”,然后用这个 2 去访问页面,那就可能访问到索引 3 的页面,而不是你以为的“被删页面之后的页面”。
要规避这类问题,建议遵循几个原则:
- 槽函数里,先根据传入的 index 取出要删除的 widget 并做必要处理,再 removeTab。
- 需要访问“新的当前页”时,永远用
m_tabWidget->currentWidget()或currentIndex()现场获取,不要缓存。 - 如果删除了很多标签,遍历时不要用 for (int i = 0; i < count; ++i) 正向删除,因为删除会导致索引前移。正确做法是从后往前删,比如
for (int i = count - 1; i >= 0; --i),或者循环里每次删除后都重新判断。
这几个原则看起来简单,却能消灭大部分和 index 相关的疑难杂症。
4.4 标签页切换时想保存页面内容,应该在哪里做
很多实际项目里,用户在一个标签页编辑了内容后切到另一个标签页,我们希望在切换瞬间自动保存当前页数据。这个需求可以在 currentChanged(int index) 信号里实现,但要注意:这个信号只在切换完成后发出,它并不会告诉你是“从哪个页面切走的”。如果你想在离开旧页面时保存内容,不能只依赖这个信号。
我的做法是:在切换信号出现之前,主动获取“即将离开”的页面。能在哪里拿到?-void QTabWidget::currentChanged(int index)触发时,新的 index 已经设置好,此时 m_tabWidget->currentWidget() 已经是新页面了,想拿旧页面就只能自己保存一份“lastWidget”指针。
一个比较稳的模式是这样做:
connect(m_tabWidget, &QTabWidget::currentChanged, this, [this](int index) { if (m_lastWidget) { // 保存上一个页面内容 MyBasePage *page = qobject_cast<MyBasePage *>(m_lastWidget); if (page) page->saveState(); } m_lastWidget = m_tabWidget->widget(index); });初始时,把 m_lastWidget 指向当前页面。这样每次切换发生,都能保存“离开的页面”。这个模式几乎成了我所有含编辑动作的标签页应用的标配。
4.5 样式表定制:让关闭按钮不那么突兀
默认的关闭按钮样式非常朴素,如果在界面风格上想更协调,可以用 Qt 样式表调整。比如让关闭按钮在 hover 时才高亮:
m_tabWidget->setStyleSheet(R"( QTabBar::close-button { image: url(:/icons/close_normal.png); subcontrol-origin: padding; subcontrol-position: right; margin: 2px; border: none; } QTabBar::close-button:hover { image: url(:/icons/close_hover.png); } )");如果没有现成图标,也可以用代码绘制一个简单的关闭符号,或者在样式表里直接用字符代替。样式表的具体写法依赖项目资源,这里不展开,只提醒一点:如果你发现自己 after 修改样式后关闭按钮消失或点击区域变小,多半是subcontrol-position或margin设置不当导致的,把这两个值调一下就好。
4.6 一个项目中遇到过的真实 Bug 记录
最后分享一段我自己踩过的坑。那时做的是一个设备管理工具,每个标签页对应一台设备,标签标题要跟着设备状态变化。我最初在添加设备时保存了一份 QHash<设备ID, int> 记录“设备ID -> 标签索引”,后续通过索引刷新标题。结果一旦用户手动拖动标签排序,或者关闭某台设备,整个哈希表全部失效,刷新时经常把标题改到错误的设备上。
踩了几次坑之后,我把索引存储全部改成指针存储:QHash<设备ID, QWidget*>,刷新标题时先用m_tabWidget->indexOf(widget)动态获取索引,再 setTabText。这样即使标签被拖动、被增删,只要 widget 对象还在,关系就不会错乱。那次改动之后,标签相关的报错基本绝迹。
这个例子给我的经验是:QTabWidget 的 index 是动态的,不要把它当作稳定标识。需要长期记住某个页面时,永远存 widget 指针,用的时候再查 index。这句话的含金量,等我写 QTabWidget 相关代码的时候,会被反复验证。希望这篇文章能让大家少走一段弯路。
如果你正打算在项目里加入多标签管理功能,建议先用这里的最小示例跑通流程,再根据业务抽象自己的页面基类和标签管理接口。基础打牢了,后面再加右键菜单、拖拽排序、状态保存都会顺很多。