1. 从一个窗口的生死说起:QT窗口显示与隐藏到底在做什么
很多人第一次接触QT,是从拖控件开始的。MainWindow拖出来,按钮一放,信号槽一连,编译跑起来,窗口弹出来,任务就算完成了。等到项目稍微做大一点,问题就来了:登录窗口验证通过后要关掉、主界面要弹出来;设置面板要能反复开合;托盘图标点一下主窗口要最小化到后台、再点一下要还原;某个子窗口关闭的时候不能真的销毁,因为里面还存着用户填了一半的数据。这时候你才发现,QT窗口的显示和隐藏、关闭,根本不是一句 show() 和 hide() 就能打发的事情。
这篇文章要解决的就是这类问题。它适合已经能写出第一个QT窗口、但对窗口生命周期管理还比较模糊的朋友,也适合做了几个项目、被"窗口关了程序没退出""窗口隐藏了内存还在涨""子窗口一关主窗口跟着挂"这些问题折磨过的开发者。我不会只给你API清单,那种东西官方文档比你我都写得全。我要讲的是:这些API背后发生了什么、什么场景该用哪个、用错了会踩什么坑、以及我在实际项目里验证过的处理套路。
先说一个最容易被忽略的前提。QT里所谓的"窗口",本质上是 QWidget 及其子类,而 QWidget 有一个非常关键的性质:它可以不显示,但依然存在。这句话是整个话题的根。一个窗口对象被 new 出来之后,即使从来没调用过 show(),它也是实实在在占着内存的、它的成员变量、它的信号槽连接、它持有的数据,全都活着。显示和隐藏只是控制"这块绘制区域要不要出现在屏幕上",关闭则涉及到"这个对象要不要被销毁"。把这三种状态分清楚,后面的所有选择才有依据。
我见过太多代码把 hide() 和 close() 混着用,功能测试时看着都对,因为界面上确实看不见了。但跑上几个小时,内存曲线一直往上走,或者改了个配置再打开设置面板,发现里面还是上次的旧值,甚至更邪门的——子窗口关了两次,程序直接崩掉。这些都是窗口生命周期没管明白的直接后果。
接下来我按"整体设计思路 → 核心API细节 → 实操流程 → 问题排查"这条线往下讲,每一块都会给到能直接抄的代码和参数说明。你如果现在手上正好有个多窗口的项目,可以边看边对照自己的代码改。
2. 窗口显示隐藏关闭的整体设计思路
2.1 三种状态、两条路径,先在心里画清楚
要在项目里把窗口管理做对,得先建立一个清晰的心智模型。QT里一个 QWidget 对象的生命周期,可以拆成三种可见性状态和两条销毁路径。
三种状态分别是:
- 未显示(hidden but alive):对象已构造,但没有出现在屏幕上。hide() 之后、show() 之前都是这个状态。此时它的 paintEvent 不会被触发,但它依然响应信号槽、依然持有数据。
- 已显示(visible):调用了 show()、showNormal()、showMaximized() 等之后的状态,窗口出现在屏幕上,参与事件循环的绘制和鼠标键盘事件分发。
- 已销毁(destroyed):对象被 delete,或者因为父对象析构而被连带销毁,或者设置了 Qt::WA_DeleteOnClose 后在 close 时自动销毁。销毁之后这个指针就是野指针,再碰它必崩。
两条销毁路径则是:
- 手动接管:自己 new、自己 delete,或者交给 QScopedPointer、std::unique_ptr 管理。
- 父子托管:new 的时候传了 parent,父对象析构时自动 delete 所有子对象。这是QT对象树的核心机制,也是最省心的方式。
把这两组概念交叉一下,"关闭"这个动作的语义就清楚了:close() 默认只是把窗口从屏幕上拿掉,并不销毁对象。它发出 QCloseEvent,你可以在 event 里 accept 或 ignore。真正决定对象生死的是另外的机制——要么设了 WA_DeleteOnClose 属性,要么父对象被销毁,要么你手动 delete。
这就是为什么很多人的登录窗口关闭之后,用 QApplication::allWidgets() 还能把它捞出来,指针还有效,但屏幕上没了。这不是bug,是设计。
2.2 为什么不用"关闭即销毁"做默认策略
有人会问,既然要关掉,为什么不干脆销毁省内存?这就要说到选型逻辑了。
我早期做过一个串口配置面板,用户点"参数设置"就弹出来,点确认就关掉。当时图省事,在关闭时直接 deleteLater()。问题很快暴露:用户调参数是高频操作,反复开合,每次都要重新构造整个面板、重新建立串口列表、重新读一遍当前配置,界面明显卡顿,而且构造过程中还有个几次毫秒的空白闪烁。后来改成面板只创建一次,反复 show() 和 hide(),流畅度立刻正常了。
反过来也有例子。一个批量任务结果展示窗口,里面可能塞几万行日志和缩略图。这种窗口如果藏起来不删,内存就一直占着,用户开着好几个,内存直接爆。这种场景就必须用完即销毁,甚至销毁前要手动清空模型数据。
所以判断标准其实很朴素:
| 场景特征 | 推荐策略 | 理由 |
|---|---|---|
| 打开频繁、内容基本不变、构造开销大 | 单例 + show/hide 复用 | 避免反复构造,响应快 |
| 打开次数少、数据量大、内存敏感 | 创建后关闭即销毁 | 及时释放内存 |
| 需要保留用户输入、切换后要回到原状态 | 隐藏不销毁 | 状态得以保留 |
| 模态对话框、用完即走、无状态 | 栈上对象或 deleteLater | 代码简单,无泄漏风险 |
这张表我在多个项目里反复用,基本没出过大错。核心就一句:这个窗口"贵不贵"(构造代价和内存占用),以及"要不要记住上次的样子"。贵且要记状态,就复用;便宜或不记状态,就销毁。
2.3 主窗口与子窗口的关系怎么摆
还有一个结构性问题:主界面和弹出的子窗口,谁当谁的父亲。
QT的对象树规则是,给构造函数传 parent 的对象会被父对象托管,父对象析构时一起析构。子窗口传了主窗口指针当 parent,好处是生命周期自动绑定,不会漏删;坏处是如果不想要这种绑定,就会出问题。
我踩过的坑是这样的:主窗口作为 parent,子窗口设置为 Qt::Window 类型(独立顶层窗口外观),用户点主窗口的关闭按钮,主窗口析构,子窗口跟着被销毁。但如果子窗口当时还开着、并且用户正在里面填数据,就会看到子窗口莫名其妙消失。这不是错误,是父子托管在起作用。要避免,要么子窗口的 parent 传 nullptr 自己管,要么在关闭主窗口前先妥善处理所有子窗口。
我现在的习惯是:功能面板、设置窗口这类跟随主界面存在的,传 parent;工具窗口、需要独立出现在任务栏的,parent 传 nullptr,用单独的智能指针或者 QPointer 管理。QPointer 特别值得推荐,它是弱引用,对象被销毁后它会自动变成 nullptr,判断if (ptr)就知道窗口还在不在,比裸指针安全得多。
3. 核心API逐个拆解:show、hide、close到底做了什么
3.1 show 家族:不只是让窗口出现
show() 是显示窗口的入口,但它内部做的事情比名字看起来多。调用 show() 时,QT会做几件事:评估窗口的合适尺寸(如果没有显式设置尺寸,会根据布局和 sizeHint 计算)、决定是否触发首次绘制、把窗口标记为可见并提交给窗口系统、最后进入事件循环等待绘制事件。
show 家族里还有几个近亲,语义差别必须分清:
- showNormal():以正常状态显示,如果窗口之前是最小化或最大化状态,会恢复到普通大小。这在托盘还原场景里特别有用。
- showMaximized():最大化显示。
- showMinimized():最小化显示,窗口还在,但收成任务栏或图标。
- showFullScreen():全屏显示,没有标题栏边框,用于展示、看板场景。
这里有个实践细节值得说。从最小化状态还原,如果你只调 show(),窗口可能还保持着最小化标志,表现就是"调了没用"。正确的做法是showNormal(),或者先setWindowState(Qt::WindowNoState)再 show。我在做托盘还原功能时就被这个坑过,日志里明明打印了 show 调用成功,界面上就是不出来,查了半天才发现状态标志没清。
另外,重复调用 show() 是安全的但也不是零成本。QT内部有可见性判断,已经可见的窗口再 show() 不会重复触发完整流程,但一些关联的 setVisible 逻辑还是会走一遍。如果一个窗口在很短周期内被高频 show/hide(比如某些自定义动画里),建议改成判断当前状态再决定是否调用,避免不必要的重绘。
3.2 hide 的真正含义:从屏幕消失但依然活着
hide() 做的是把窗口标记为不可见,从屏幕上移除,但对象本身完全保留。这个保留包括:
- 所有成员变量、控件状态、用户输入的内容
- 所有的信号槽连接
- 窗口的位置和尺寸记忆(下次 show 还在原位置)
- 它如果设置了布局和子控件,这些子控件的状态也全在
这就是"隐藏不销毁"策略的实现基础。我用它做过多步骤表单:第一步填完点"下一步",把当前面板 hide,下一个面板 show,用户点"上一步"回来,之前填的内容一个不差。如果每步都销毁重建,要额外做一套数据持久化逻辑,工作量翻倍还容易漏字段。
不过 hide 有个容易忽略的点:窗口隐藏之后,它的定时器依然在跑,信号槽依然会响应。有人隐藏了一个带轮询的窗口,以为它停了,其实后台还在不停发请求。如果某些操作应该在窗口不可见时暂停,你得显式处理,比如在 hideEvent 里停掉定时器,在 showEvent 里再启动。这个 showEvent 和 hideEvent 是配套的,重写它们可以精确感知窗口的显示状态变化。
注意:hide() 不会触发关闭事件,也不会发出 close 相关的信号。如果你希望"隐藏时执行某些清理逻辑",必须靠 hideEvent,别指望 closeEvent。
3.3 close 的双重身份与关闭事件的拦截
close() 是最容易被误解的一个。它的语义是"请求关闭窗口",而且这个请求是可以被拒绝的。
调用 close() 时,QT会向窗口发送一个 QCloseEvent。窗口的 closeEvent() 函数被调用,你能拿到这个 event 对象,然后决定接受还是忽略:
void MyWidget::closeEvent(QCloseEvent *event) { if (m_hasUnsavedChanges) { QMessageBox::StandardButton ret = QMessageBox::warning( this, QStringLiteral("提示"), QStringLiteral("有未保存的修改,确定要关闭吗?"), QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); if (ret == QMessageBox::Cancel) { event->ignore(); // 拒绝关闭,窗口留在屏幕上 return; } if (ret == QMessageBox::Save) { saveAll(); } } event->accept(); // 接受关闭 }这段代码是窗口关闭保护的典型写法。关键点在于event->ignore(),它让关闭动作无效,窗口继续存在。很多新手写关闭确认时,弹框点取消后窗口还是没了,就是因为忘了 ignore。
还有一个细节:close() 被接受后,窗口默认只是隐藏,不销毁。要让它销毁,两个办法:在 closeEvent 里显式deleteLater(),或者构造时设置setAttribute(Qt::WA_DeleteOnClose)。后者更省事,但记得它只对顶层窗口或独立窗口有意义,普通子控件设了不会有预期效果。
如果窗口是程序的主窗口,或者它是最后一个可见的顶层窗口,close 被接受后,QApplication 可能会触发"最后一个窗口关闭"逻辑,进而 quit 整个程序。这个行为受QApplication::setQuitOnLastWindowClosed(bool)控制,默认是 true。做托盘驻留类程序时,必须把它设成 false,否则用户一点关闭,主窗口没了,程序整个退出,托盘图标也跟着消失,这是托盘程序最常见的翻车点。
3.4 deleteLater 与直接 delete 的取舍
销毁窗口时,大概率你会看到两种写法:delete w;和w->deleteLater();。它们不是可以随意互换的。
直接 delete 会立刻析构对象。如果这个时机落在对象自己的事件处理函数里,或者落在它正在处理的信号槽调用栈中间,delete 之后调用栈返回时还会访问到已经释放的内存,直接崩。这就是著名的"在槽函数里删自己"问题。
deleteLater() 则是向事件循环投递一个删除请求,等当前事件处理结束、控制权回到事件循环时才真正删除。这样避免了上述的自删崩溃。所以结论很明确:只要删除动作可能发生在对象自身的事件流中(包括各种信号触发的槽、事件处理函数),一律用 deleteLater()。
那什么时候可以直接 delete?在对象的构造和事件处理之外的、你能明确知道调用栈不会回到该对象的上下文里,比如在另一个完全不相关的管理类里清理一个已经确认不再使用的窗口。但说实话,为了省那几个字节的开销去赌调用栈,不值得。我现在几乎所有窗口销毁都走 deleteLater(),除非有明确的性能理由。
提示:使用 WA_DeleteOnClose 属性时,QT内部走的就是 deleteLater 机制,所以它是安全的。这也侧面说明官方推荐的销毁方式就是延后删除。
4. 多窗口切换的实操:登录、主界面、设置面板怎么串
4.1 登录窗口到主界面的正确交接方式
登录到主界面是几乎每个桌面程序都要面对的第一道多窗口题。看一个我在项目里用了很久的写法。
假设登录窗口是 LoginDialog,主窗口是 MainWindow。如果直接这么写:
// 错误示范:登录成功后在登录窗口里直接构造主窗口 void LoginDialog::onLoginSuccess() { MainWindow *w = new MainWindow(); w->show(); this->close(); // 看起来没问题? }这段代码运行时可能碰巧正常,但隐患不少。第一,MainWindow 没有父对象,成了野生的顶层窗口,程序退出时如果没被正确处理,可能残留或者泄漏。第二,登录窗口 close 之后如果没设 WA_DeleteOnClose,它一直占着内存。第三,如果后面还要从这个主窗口返回到登录(比如"切换账号"),你发现登录窗口的数据还停留在上次的状态,因为它根本没销毁。
更稳的写法是把控制权交给一个统一的入口,比如 main 函数或者一个专门的 Controller:
// main.cpp 里的流程控制 int main(int argc, char *argv[]) { QApplication app(argc, argv); app.setQuitOnLastWindowClosed(false); // 手动控制退出时机 LoginDialog login; if (login.exec() != QDialog::Accepted) { return 0; // 用户取消登录,直接退出 } MainWindow mainWin; mainWin.show(); return app.exec(); }这里用 QDialog::exec() 以模态方式跑登录,返回 Accepted 才继续。登录对话框的生命周期由栈管理,函数退出自动析构,干净。主窗口活在主函数作用域内,程序结束自然释放。这种"入口统一调度、窗口各自管好自己"的结构,比在窗口里互相 new 要可控得多,尤其是在有多个入口、需要反复登录切换的场景。
如果你的登录是无边框自定义标题栏的窗口,exec() 也照样能用,只是要在构造里设好 Qt::FramelessWindowHint 和相关属性。模态性只影响事件分发,跟外观无关。
4.2 设置面板复用:单例加显示切换
设置面板是"贵且要记状态"的典型。用单例加 show/hide 复用最合适:
class SettingsPanel : public QWidget { public: static SettingsPanel *instance() { static SettingsPanel panel; // 静态局部,C++11 起线程安全 return &panel; } void popup() { refreshFromConfig(); // 每次显示前同步最新配置 show(); raise(); // 提到最前 activateWindow(); // 抢焦点 } protected: SettingsPanel(QWidget *parent = nullptr) : QWidget(parent) {} void closeEvent(QCloseEvent *e) override { saveToConfig(); // 关闭前保存 hide(); e->ignore(); // 关键:拒绝真正的关闭,转为隐藏 } };这段代码有几个设计点值得琢磨。
static SettingsPanel panel;用的是静态局部变量,好处是首次调用时构造、程序结束时析构,不用手动管理也不用担心泄漏。但注意,静态局部变量不能传 parent,因为它不属于对象树,这没问题,因为它是复用型的常驻面板。
closeEvent 里用e->ignore()把关闭转成隐藏,是"关闭即隐藏"这个操作习惯的标准实现。很多应用的用户点窗口的 X 按钮,期望的就是"收起来"而不是"彻底关掉再重开"。
popup()里先刷新配置再显示,是因为配置可能被别处修改过,每次显示都应该是最新的。raise()和activateWindow()的组合是用来把窗口从可能被遮挡的状态提到前台,如果窗口之前被隐藏或者被别的窗口压住,这两个调用能确保它出现在用户眼前。实测下来,单独用 raise() 有时不够,配合 activateWindow() 更稳,尤其是在 Windows 上。
注意:如果面板是单例且用静态局部变量实现,就不要在它上面设 WA_DeleteOnClose。属性一旦生效,面板第一次 close 就被销毁,下一次 instance() 拿到的指针就是悬空的。
4.3 托盘程序的隐藏与还原
托盘程序的核心逻辑是:主窗口关闭时隐藏到托盘而不是退出,托盘菜单点击时还原。
MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { initTray(); // 创建 QSystemTrayIcon connect(m_tray, &QSystemTrayIcon::activated, this, &MainWindow::onTrayActivated); } void MainWindow::closeEvent(QCloseEvent *event) { if (m_reallyQuit) { // 真正退出标志 event->accept(); return; } hide(); // 关闭转隐藏 event->ignore(); } void MainWindow::onTrayActivated(QSystemTrayIcon::ActivationReason reason) { if (reason == QSystemTrayIcon::Trigger) { // 单击托盘图标 if (isVisible()) { hide(); } else { showNormal(); // 还原,注意不是 show() raise(); activateWindow(); } } }这里三个细节都是踩坑换来的。第一,必须有 m_reallyQuit 这样的标志,否则从托盘菜单点"退出"时,closeEvent 又会把窗口藏起来,程序永远退不掉。第二,还原用 showNormal() 而不是 show(),防止之前处于最小化状态。第三,必须配合 main 里的setQuitOnLastWindowClosed(false),否则窗口 hide 之后,程序判定"最后一个窗口已关闭",直接退出,托盘就是个摆设。
我见过有人把托盘功能做出来,测试时发现点关闭确实缩到托盘了,但几秒后程序还是自己没了,就是因为漏了 setQuitOnLastWindowClosed(false)。这个错误不好查,因为现象和"崩溃"很像,其实只是应用正常退出了。
5. 模态与非模态:窗口显示方式的选择与影响
5.1 模态窗口到底"模"住了什么
模态(modal)是个很实用的概念,但很多人只记住了"模态窗口会挡住后面的窗口",没理解它真正的作用范围。
模态的本质是限制事件分发。当一个模态窗口显示时,QT会进入一个局部的嵌套事件循环,把原本发给其他窗口的鼠标键盘事件拦下来,只发给这个模态窗口(以及它的子控件)。这才是不管你怎么点后面窗口都没反应的真正原因,不是简单的视觉遮挡。
QT提供三个层级的模态:
- Qt::ApplicationModal:阻塞整个应用的所有窗口,直到这个窗口关闭。适用于必须立即处理的提示、确认框。
- Qt::WindowModal:只阻塞它的父窗口、所有祖先窗口以及它们的子窗口,其他不相关的窗口不受影响。适用于某个文档窗口内部弹出的对话框。
- Qt::NonModal:非模态,不阻塞任何东西,窗口独立存在。
设置方式有两种,一种是在构造或显示前setWindowModality(Qt::ApplicationModal),另一种是直接用exec()。对 QDialog 来说,exec() 等价于设置了 ApplicationModal 并进入嵌套事件循环,这也是为什么 exec() 会"卡住"在那一行直到对话框关闭。
// 方式一:显式设置模态属性,再用 show QDialog dlg(this); dlg.setWindowModality(Qt::ApplicationModal); dlg.show(); // 后续代码继续执行 // 方式二:直接用 exec,阻塞等待 QDialog dlg(this); int ret = dlg.exec(); // 这里会等待对话框关闭 if (ret == QDialog::Accepted) { /* ... */ }两者的区别要记牢:show() 不阻塞,exec() 阻塞。如果你用 exec() 却期待后面的代码立刻执行,那必然出错;反过来,用 show() 又想拿返回值,也是拿不到的。我建议需要返回值、需要顺序执行的用 exec();只是展示信息、可以并行操作的用 show() 加模态属性。
5.2 嵌套事件循环的副作用
exec() 的便利是有代价的,那就是嵌套事件循环。exec() 期间,虽然调用它的那行代码停住了,但整个程序的事件循环还在转,意味着:
- 其他定时器可能还在触发
- 其他窗口的信号槽可能还在响应
- 用户在模态框打开期间做的操作,可能通过信号影响到背后的逻辑
这个特性会制造一些"诡异"的现象。比如你在 exec() 打开对话框之前启动了某个轮询定时器,对话框开着的时候,定时器依然在跑,可能触发槽函数去改数据,等对话框关闭返回时,数据已经和你预期的不一样了。
我在一个数据采集程序里就遇到过:打开模态配置框修改采样参数,期间后台定时器还在按旧参数存数据,等配置确认返回,发现已经多存了几条按旧参数的数据。解决办法是在打开模态框前停掉定时器,关闭后再按新参数重启。凡是依赖"当前状态"的后台任务,在进入模态嵌套循环前都要考虑暂停。
还有一种情况是窗口在关闭时触发了另一个窗口的显示,形成"关闭即打开"。如果不小心在这个链条里重入了同一个 exec(),就会看到对话框叠了一堆。避免的办法是设置标志位防止重入,或者把这类"关闭后打开新窗口"的逻辑从 closeEvent 里挪到更上层的调度逻辑里。
6. 实操中真实踩到的窗口问题与排查技巧
6.1 窗口关了内存不降的排查路径
内存只增不减是窗口管理最典型的问题。我整理了一套排查顺序,基本能覆盖九成情况。
第一步,确认窗口有没有被真正销毁。用 QPointer 或者 QObject::destroyed 信号来观察:
QPointer<MyPanel> tracker = new MyPanel(); connect(tracker, &QObject::destroyed, this, [](){ qDebug() << "panel destroyed"; });如果关闭后一直没打印 "panel destroyed",说明对象生命周期没结束,内存自然不会降。
第二步,检查是不是忘了设 WA_DeleteOnClose,或者关闭逻辑走的是 hide 而不是 close。很多"自定义关闭按钮"里写的是this->hide()而不是this->close(),那对象当然一直在。
第三步,如果确认销毁了但内存曲线还是不降,那多半不是泄漏,而是分配器不归还内存。现代内存分配器出于性能考虑,释放的内存往往留在进程里备用,不会立刻还给操作系统。用任务管理器看进程内存,看到"释放后不降"很正常,真正判断泄漏要看多次开合之后的趋势,或者用专门的内存分析工具。这点我一开始也误解过,白排查了半天。
第四步,如果确实是反复开合后阶梯式上涨,那就找是不是有信号槽连接没断开、有数据容器只增不减、或者有对象挂在某个全局列表里没摘除。窗口销毁不代表它持有的数据被清理,如果你的窗口在全局容器里注册过自己,得在析构里注销。
6.2 关闭二次触发导致崩溃
"双击关闭按钮程序崩了"这种情况我遇到过不止一次。原因通常是关闭逻辑不幂等:第一次 close 触发了销毁流程,第二次 close 时对象已经析构,访问它的成员就是野指针。
解决办法有几个层次。最简单的是销毁后把指针置空(前提是用 QPointer 或者你手动管理指针),调用前判断:
if (m_panel) { m_panel->close(); m_panel = nullptr; // 或者靠 QPointer 自动置空 }更根本的是区分"请求关闭"和"已经关闭"。可以借助 destroyed 信号统一清理:
connect(m_panel, &QObject::destroyed, this, [this](){ m_panel = nullptr; });这样不管窗口是怎么被销毁的(手动删、父对象删、WA_DeleteOnClose),指针都会被自动同步。用这个模式之后,我基本没再遇到过二次关闭崩溃。
6.3 常见问题速查表
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
| 窗口 hide 后程序自己退出 | 最后一个窗口关闭触发退出,且 quitOnLastWindowClosed 为 true | 设setQuitOnLastWindowClosed(false) |
| show 调用了窗口不出现 | 窗口还带最小化状态标志 | 改用 showNormal() 或先清 windowState |
| 关闭确认框点取消窗口还是没了 | closeEvent 里没调event->ignore() | 补上 ignore |
| 关闭后指针失效导致崩溃 | 对象已销毁而指针未置空 | 用 QPointer 或监听 destroyed 同步置空 |
| 模态框打开期间后台数据错乱 | 嵌套事件循环里定时器仍在运行 | 进入前停相关定时器,退出后重启 |
| 设置面板每次打开都是旧数据 | 面板被复用但没刷新 | 显示前调用刷新逻辑从配置同步 |
| 托盘图标点了程序没反应 | 还原用了 show() 而非 showNormal(),或标志位没处理 | 检查状态标志与调用方式 |
| 反复开合内存阶梯上涨 | 信号未断、数据未清、全局引用未注销 | 在析构里做完整清理 |
这张表我建议直接存下来,出问题先对号入座,能省下大量试错时间。
6.4 几个不容易想到的避坑细节
最后分享几个常规文档里不太提、但实操中很值钱的经验。
窗口尺寸和位置的记忆。如果你希望复用的窗口每次出现在上次的位置和大小,hide 之前不需要特别保存,QT会给它留着。但如果窗口被销毁重建,就得自己存 QSettings 或者配置文件,在构造后 restoreGeometry。我在做多显示器适配时特别注意这点,同一个窗口在副屏关闭、主屏打开时,如果位置没做边界检查,窗口可能出现在屏幕外,用户直接看不到。显示前用QGuiApplication::screenAt()判断一下位置是否在某个屏幕范围内,不在就居中。
隐藏窗口的绘制暂停。一个隐藏的窗口不会收到 paintEvent,所以它不会消耗绘制资源。但如果窗口里嵌了需要持续更新的图表或视频,隐藏时虽然不绘制,后台计算可能还在跑。做性能敏感的应用时,我习惯在 hideEvent 里暂停这类后台计算,showEvent 里恢复,能省下可观的资源。
父子窗口的焦点传递。父窗口 hide 的时候,焦点会转移给谁是有规则的。如果父窗口管理不当,可能出现焦点丢失、快捷键失灵。我处理这类问题的方式是,在窗口显示后明确调用setFocus()给期望的控件,不要依赖默认焦点分配。这个细节在做自定义控件和复杂表单时尤其重要。
Qt::WA_DeleteOnClose 的连带效应。给一个有 parent 的子窗口设这个属性时要小心。窗口 close 被接受后触发 deleteLater,对象被销毁,父对象的子对象列表里也就移除了它。如果你的代码里还留着这个子窗口的裸指针用于别处访问,下一次访问就是崩溃。用 QPointer 是最省心的防护。
信号连接的清理时机。窗口销毁时,QT会自动断开它作为发送者的所有连接,也会断开指向它的接收者连接,这部分是安全的。但如果连接用的是 lambda 且捕获了外部对象,而外部对象的生命周期短于窗口,lambda 里的捕获对象就成了悬空引用。这类问题不崩溃则已,一崩就很难定位。我的习惯是不在长生命周期窗口的信号里捕获短生命周期对象的裸指针,要么用 QPointer 捕获,要么把连接挂在正确的上下文对象上,利用 QT 的上下文自动断开机制。
窗口的显示、隐藏和关闭,看起来是QT里最基础的操作,但它牵扯到对象生命周期、事件循环、内存管理和用户交互习惯这几条线,随便一条没理清都可能变成线上问题。我自己的经验是,与其出了问题一点点查,不如在最开始就把"窗口归谁管、什么时候销毁、关闭时怎么响应"这三件事在设计阶段定死,代码量不大,省下的是后面反复的调试时间。