1. 先把“两个独立窗口”这件事问清楚“Qt Creator 里怎么弄两个独立的窗口”这句话我在技术群里见过太多版本。有人是在设计器里点来点去想把第二个.ui文件从同一个窗口里拆出来有人是写代码时把子窗口的父指针随手写了this结果点回主界面子窗口就沉底还有人做了个工具窗口任务栏里从头到尾只有一个图标以为是自己代码写错了。这几种情况表面上都在问“两个独立的窗口”实际要解决的问题完全不同选错方向会白折腾一整天。先给结论在 Qt Creator 这套工具链里两个独立的窗口指的是应用程序运行时存在两个各自拥有标题栏、可以单独最小化、单独摆在桌面上、互不遮挡的顶层窗口。它跟“在一个窗口里用 QStackedWidget 切页面”“用 QSplitter 分左右”是两码事也跟“在 Qt Creator 编辑器里开两个代码编辑窗格”是两码事。前者关系到QWidget的父子关系、窗口类型标志、对象的生命周期管理后者只是 IDE 的界面布局问题。这篇内容主攻前者因为绝大多数人卡住的都是前者。适合谁来读刚学完 Qt 的控件和信号槽、能跑通一个单窗口 Demo正打算做一个“主界面 独立工具面板”或者“登录窗 主窗”的小项目的人也适合已经写过几万行 Qt 代码、但一直在用“父指针写nullptr然后提心吊胆管内存”这套土办法想系统梳理一遍的人。不要求你懂多深的内存模型但至少要能看懂new和connect这两样东西。1.1 先确定你问的是 IDE 窗口还是应用窗口这两种“窗口”经常被混淆分开说清楚能省下不少时间。IDE 层面Qt Creator 的编辑区本身支持分屏也有把文档标签拖出主窗体变成独立浮动编辑窗口的操作。这类操作在“窗口”菜单和标签栏的右键菜单里能找到不同版本的位置略有差别功能本身不难找。做界面时一边看头文件一边看实现文件分屏确实顺手但它跟你的程序运行时行为没有半点关系。很多人截图问“为什么我的窗口跑不出来”配图是 Qt Creator 的分屏编辑器那就是纯粹的理解偏差。应用层面你写的程序运行时屏幕上出现两个窗口。这才是需要动代码的部分。下文全部围绕这个展开涉及的是QWidget、QMainWindow、QDialog这三个基类的选择以及parent参数、窗口类型标志、对象销毁时机这些实在的东西。提示动手之前先在心里默念一遍你要的效果——两个窗口是否都要在任务栏出现图标关掉其中一个另一个要不要跟着关关掉最后一个程序要不要退出这三个问题的答案直接决定后面代码里每一行该怎么写。1.2 “独立”这个词的四个维度逐个拆开看不少人调窗口调不好是因为把“独立”当成一个开关以为设一个属性就全独立了。实际上它至少包含四层含义每一层由不同的机制控制。第一层是视觉独立窗口有边框、有标题栏、能拖动、能改大小。这由窗口类型标志决定Qt::Window是基础值Qt::Dialog是它的一种变体。第二层是堆叠独立点主窗口时工具窗口不会被压到后面或者反过来工具窗口始终浮在主窗口之上。这一层受影响最大的是父子关系——只要QWidget的父对象是一个窗口它在窗口管理系统里就会跟父窗口形成一组堆叠顺序受父窗口牵制。第三层是生命周期独立主窗口关掉子窗口是继续活着还是一片消失。父对象不为空时Qt 会替你连带销毁这部分不需要你操心但也不给你选择权父对象为空时全靠自己灵活但容易漏。第四层是任务栏独立在 Windows 和多数 Linux 桌面上顶层窗口默认会有自己的任务栏按钮而Qt::Tool类型的窗口一般没有。macOS 的情况要单独说它的窗口管理模型本来就倾向于把同一进程的窗口归到同一个应用图标下所以“两个独立任务栏图标”这个诉求在 macOS 上基本不成立别在那里浪费时间调试。把这四层想明白接下来选型就不会纠结了。1.3 三条常见路线的取舍对照我把实际项目里最常用的三种做法列成表先看结论再往下读细节会轻松得多。做法父指针生命周期任务栏按钮适用场景完全独立顶层窗口nullptr自己管理WA_DeleteOnClose或QPointer护航父子各有独立工具面板、预览窗、监视窗有父的顶层窗口this且设Qt::Window父窗口关闭时连带销毁通常只有父窗口有跟随主界面的辅助面板模态对话框this栈上对象exec()返回即销毁无参数设置、确认框、短流程第一列和第三列是绝大多数项目的答案。中间那条路线最容易被误用——它表面上满足了“看起来是两个窗口”但任务栏合并、堆叠顺序跟随、关闭连带这三件事会让新手反复困惑明明是两个窗口为什么行为跟我想的不一样。等你读完第 3 节关于窗口类型的部分就会明白它其实是Qt::Tool类窗口的典型形态而不是“独立窗口”。2. 工程准备把第二个窗口类拆出来动手写代码之前先在工程结构上把两个窗口拆干净后面排错会轻松一个数量级。我见过太多项目把两个窗口的所有代码塞进一个mainwindow.cpp两千行下去之后改一个按钮都得靠搜索。2.1 在 Qt Creator 里新建窗口类的完整流程推荐用设计器表单类别手写.ui文件。路径是File → New File or Project → Qt → Qt Designer Form Class然后选模板。模板有三种常见选择Widget、Dialog with Buttons Bottom、Main Window。第二个窗口如果是工具面板、预览区、日志窗选Widget最轻没有多余的菜单栏和状态栏。如果是设置页、短流程确认选Dialog with Buttons Bottom它会自动给你放好确定/取消按钮和对应的信号槽骨架。只有当你确实需要菜单栏、工具栏、停靠窗口时才选Main Window。很多新手给辅助窗口也套QMainWindow结果窗口里空荡荡一片还得手动删掉一堆东西。输入类名时保持和文件名一致比如ChildWindow生成childwindow.h、childwindow.cpp、childwindow.ui。这里有个小细节Qt Creator 生成的头文件里带namespace Ui { class ChildWindow; }这个前置声明ui指针通过std::unique_ptr持有析构函数必须在.cpp里定义——因为头文件里Ui::ChildWindow还是不完整类型。如果你图省事把析构写成 default放在头文件里编译期大概率报“incomplete type”的错改成在源文件里写ChildWindow::~ChildWindow() default;就好。生成之后立刻去改窗口标题setWindowTitle(...)在构造函数里加一行别等界面做完再回头找。标题是用户区分两个窗口的第一直觉这里含糊后面全是麻烦。2.2 qmake 和 CMake 两套构建配置的差异新工程在 Qt 6 里默认是 CMake但老项目、教程、公司遗留代码大量是 qmake。两种配置里“加一个新窗口类”要动的地方不同漏一处就是链接错误。CMake 下qt_add_executable的源文件列表里要把三个文件都写进去cmake_minimum_required(VERSION 3.16) project(TwoWindows LANGUAGES CXX) find_package(Qt6 REQUIRED COMPONENTS Widgets) qt_standard_project_setup() qt_add_executable(TwoWindows main.cpp mainwindow.cpp mainwindow.h mainwindow.ui childwindow.cpp childwindow.h childwindow.ui ) target_link_libraries(TwoWindows PRIVATE Qt6::Widgets)qt_standard_project_setup()会自动打开AUTOMOC、AUTOUIC、AUTORCC这些自动化处理.ui文件被uic转成ui_childwindow.h这一步不需要你手动干预。注意.ui文件必须写进源文件列表否则AUTOUIC找不到它编译时报ui_childwindow.h不存在。这一条我踩过不止一次。qmake 下则是两个变量QT core gui widgets greaterThan(QT_MAJOR_VERSION, 4): QT widgets SOURCES main.cpp \ mainwindow.cpp \ childwindow.cpp HEADERS mainwindow.h \ childwindow.h FORMS mainwindow.ui \ childwindow.ui改完.pro一定要执行一次Build → Run qmake只按构建按钮有时不会重新解析工程文件尤其是新增文件的时候。CMake 那边对应的是重新执行一次配置Qt Creator 一般会自动帮你做但如果发现新文件“找不到符号”手动跑一次 CMake 配置最快。注意如果main.cpp里没写#include QApplication或者QT widgets漏了出现的错误是“undefined reference”而不是“找不到头文件”容易误判成链接库的问题。遇到链接错误先检查构建配置再看代码。2.3 类名、对象名与职责边界两个窗口各管各的事这条从第一天就要立规矩。主窗口负责调度和全局状态子窗口只管自己界面的输入输出不直接摸主窗口的成员变量。听起来是废话但真到写的时候“顺手qobject_castMainWindow*(parent())拿一下”这种代码会迅速把两个类缠成一团。我自己的习惯是子窗口对外只暴露信号和少量公开方法。信号用于“我这边有结果了”公开方法用于“你让我显示成什么样”。数据流向永远是单向的子窗口不反向调用主窗口的方法。这条约定执行半年之后你会发现子窗口可以单独拿出来做成一个库测试也方便得多。对象名objectName也别乱。设计器里给控件起名pushButton、pushButton_2是灾难信号槽的自动连接靠的是on_控件名_信号名这个命名约定名字一乱自动连接失效你还得手动写connect。给按钮起btnConfirm、btnCancel这类有意义的名字省下的时间远超起名花的那几秒钟。3. 让窗口真正独立四种写法与关键参数到正题了。同样是“点按钮打开第二个窗口”写法不同行为天差地别。下面按从简到繁的顺序讲每一种都说说它的实际表现和适用边界。3.1 父指针传 nullptr最直接也最容易漏内存的写法“父指针传空就是独立窗口”——这句话本身没错但它只说了一半。完整的事实是QWidget的构造函数签名里有QWidget *parent nullptr这个参数同时承担两个身份。当parent非空且是一个普通控件时这个 widget 成为子控件被嵌进父控件的布局区域里当parent为空时它成为顶层窗口拥有自己的窗口系统资源。你没看错一个参数决定了两件性质完全不同的事。最朴素的写法是这样// mainwindow.cpp void MainWindow::onOpenChild() { auto *w new ChildWindow(nullptr); // 关键父对象为空 w-show(); }跑起来能看到第二个窗口有标题栏、能单独拖动、任务栏里也有图标看起来完美。但这里埋了三颗雷。第一颗w是个裸指针局部变量函数返回后你就再也拿不到它了。用户关掉这个窗口QWidget只是隐藏对象还在堆上再点一次按钮又 new 一个前一个永远泄漏。反复开关几十次内存曲线一路向上。第二颗主窗口关掉之后这个子窗口因为父对象为空不会被连带销毁程序进程可能还挂在那里任务管理器里显示一个没有界面的幽灵进程。第三颗如果用户先关子窗口、再关主窗口QApplication在所有窗口关闭时默认退出事件循环这个行为本身没问题但如果主窗口先关子窗口还开着程序就停在那儿不退用户会觉得你的软件关不掉。所以这个写法必须配合生命周期管理下一小节展开。3.2 setWindowFlag 动态升级改标志必须在 show 之前另一个高频需求是这个控件本来是嵌在主界面的布局里的我想让它弹出来变成一个独立窗口。做法是给已经存在的 widget 设Qt::Window标志m_panel-setWindowFlag(Qt::Window, true); m_panel-setWindowTitle(独立面板); m_panel-show();这行代码的效果是m_panel从子控件变成顶层窗口会跳出父窗口的布局。但有两个坑必须记住。第一个坑改完标志必须重新show()。QWidget::setWindowFlags内部会调用setParent这个过程会把 widget 隐藏掉。文档里写得很清楚你必须再调一次show()才能让它重新出现。实测确实如此忘了这一步的表现是“点了按钮窗口没了”非常迷惑。第二个坑Qt::Window不会切断父子关系。这里是最容易反直觉的地方。即使设了Qt::Window只要构造时的parent不是空这个窗口在窗口管理器里仍然归属于父窗口父窗口最小化时它跟着隐藏父窗口关闭时它被销毁堆叠顺序上它基本总在父窗口之上。想要彻底独立得在setParent(nullptr)之后再加Qt::Window标志解决“长什么样”父指针解决“归谁管”两件事必须分开处理。另外给一个由布局管理的控件临时改成顶层窗口布局会重新计算回来之后尺寸可能已经不是你想要的。这种“动态升级”的写法只在少数场景值得用比如可拖出的停靠面板。日常需求还是老老实实新建一个窗口类更清爽。3.3 窗口类型标志对任务栏和边框的实际影响Qt::WindowType这一串枚举值看着多实际常用的就五六个。我按实际效果整理成表标志标题栏任务栏按钮典型用途Qt::Window有有普通独立窗口Qt::Dialog有视平台而定对话框、设置页Qt::Tool有可能更窄一般没有工具面板、浮动工具条Qt::Popup无边框无下拉菜单、气泡提示Qt::SplashScreen无边框无启动画面表格里“视平台而定”和“一般没有”这两个措辞是刻意的因为任务栏行为受桌面环境、窗口管理器、系统主题影响不同机器上跑出来的结果可能不完全一致。所以我的经验是不要依赖微妙的默认行为重要场景显式指定标志。想让窗口有任务栏按钮就明确写Qt::Window想让工具面板不占任务栏就明确写Qt::Tool别指望默认值。还有个容易忽略的组合Qt::FramelessWindowHint去掉系统边框之后窗口就不能拖动、不能改大小了需要自己用鼠标事件实现拖拽。新手常常为了“好看的界面”加上这个标志然后发现窗口动不了回过头来补拖拽逻辑又是一轮调试。除非设计上确实要求自定义标题栏否则别碰。多屏环境下还有一个实用技巧在打开窗口之前算出合适的初始位置别让窗口总是出现在主屏角落。void ChildWindow::placeOnScreen(const QRect anchorRect) { QScreen *scr QGuiApplication::screenAt(anchorRect.center()); if (!scr) scr QGuiApplication::primaryScreen(); const QRect avail scr-availableGeometry(); QRect g frameGeometry(); g.moveCenter(anchorRect.center()); // 把窗口限制在可用区域内 if (g.right() avail.right()) g.moveRight(avail.right()); if (g.bottom() avail.bottom()) g.moveBottom(avail.bottom()); if (g.left() avail.left()) g.moveLeft(avail.left()); if (g.top() avail.top()) g.moveTop(avail.top()); move(g.topLeft()); }frameGeometry()是含边框的几何geometry()是客户区几何这两个在算位置时经常混用。要贴着屏幕边缘摆窗口用frameGeometry()要在客户区内部布局用geometry()。在带阴影的现代窗口系统上这两个值差得很明显选错了窗口就会露出半个边框在屏幕外。3.4 谁来 new 谁来 delete三种生命周期管理模型这是整篇内容里最需要认真对待的部分。窗口泄漏和野指针崩溃十个里有九个出在这里。模型一栈上对象 模态执行。最简单也最安全适合短流程。void MainWindow::onOpenSettings() { SettingsDialog dlg(this); if (dlg.exec() QDialog::Accepted) { applySettings(dlg.values()); } }dlg在栈上exec()阻塞到对话框关闭返回值告诉你用户确认还是取消函数退出自动析构一行delete都不用写。限定条件窗口必须是“临时问一句就走”的类型不能长时间开着。模型二堆上对象 WA_DeleteOnClose。适合独立工具窗口。void MainWindow::onOpenChild() { if (m_child) { // QPointer 自动置空 m_child-showNormal(); m_child-raise(); m_child-activateWindow(); return; } m_child new ChildWindow(nullptr); m_child-setAttribute(Qt::WA_DeleteOnClose, true); m_child-show(); }这里m_child的类型必须是QPointerChildWindow不能是裸指针。WA_DeleteOnClose会在窗口关闭时触发deleteLater对象被销毁裸指针这时候就变成了野指针下次判断if (m_child)永远为真调用它会直接崩。QPointer是 Qt 提供的弱引用包装对象销毁后自动变成nullptr判断安全。再加一句raise()和activateWindow()是有讲究的如果窗口已经开着但被别的程序挡住用户再点按钮你只是show()的话它可能还是被埋在下面用户以为“点了没反应”。raise()提到同层级最前activateWindow()抢焦点并让系统把窗口带到前台。这两个调用在 Windows 上偶尔会被系统的前台锁限制但常规场景下足够可靠。模型三堆上对象 显式管理。只在窗口数量固定、生命周期跟主窗口强绑定的时候用比如固定三个面板。在MainWindow的析构里显式delete或者干脆把父指针设成this交给 Qt 管。MainWindow::~MainWindow() { delete m_logPanel; // 或用 QScopedPointer 托管 m_logPanel nullptr; }三种模型里第二种是独立窗口的默认答案第一种是对话框的默认答案。真要说哪种最容易出错反而是第三种——写的时候觉得清楚改的时候忘了同步delete一个已经被 Qt 销毁的对象直接段错误。4. 两个窗口之间的通信与关闭联动窗口拆开了紧接着的问题就是它们怎么说话。主窗口要告诉子窗口“把当前选中的数据展示出来”子窗口要告诉主窗口“用户点了应用”。这套数据流设计不好代码会迅速腐烂。4.1 主窗口持有子窗口指针并暴露接口单向依赖是基本原则主窗口知道子窗口存在子窗口不知道主窗口存在。子窗口暴露公开方法给外面设置数据// childwindow.h class ChildWindow : public QWidget { Q_OBJECT public: explicit ChildWindow(QWidget *parent nullptr); void setDataSource(const QListRecord records); signals: void itemActivated(int index); private: Ui::ChildWindow *ui; };主窗口在打开窗口后立即灌数据m_child new ChildWindow(nullptr); m_child-setAttribute(Qt::WA_DeleteOnClose, true); m_child-setDataSource(m_records); m_child-show();另一种常见做法是数据在构造函数里传参省一次调用。这个看数据量构造参数超过三个就改成 setter否则调用点会变成一长串位置参数读的人得回去数参数顺序。如果窗口已经开着用户又改了主界面数据得同步过去。别指望用户会先关窗口再重开实话讲没人这么用。在主窗口的数据变化处加一句if (m_child) m_child-setDataSource(m_records);就够QPointer保证空指针安全。4.2 子窗口用信号把结果抛回主窗口反向通信只走信号。子窗口内部做完事情emit itemActivated(index)主窗口连接connect(m_child, ChildWindow::itemActivated, this, MainWindow::handleItemActivated);连接时机在new之后立刻做不要拖到后面。有个隐蔽的坑如果你在子窗口的构造函数里就emit了信号此时主窗口还没连上这一发信号就丢了。构造函数里不要发信号把首次通知挪到showEvent或者由调用方显式触发。还有一种做法是不让主窗口持有子窗口改成子窗口发信号、主窗口通过sender()取来源。这个在窗口数量动态变化时有用但sender()返回的是QObject*还得qobject_cast实际用起来比持有指针麻烦。固定两个窗口的场景老老实实持有指针。跨窗口传结构体时注意类型要注册qRegisterMetaTypeQListRecord(QListRecord);跨线程的排队连接需要这一步同线程直连不需要。但如果你现在不注册、将来某天把子窗口放到工作线程debug 起来会非常痛苦——信号发出去了槽没反应日志什么都没有。养成习惯自定义类型一律注册代价是一行代码。4.3 关闭一个窗口不要连带退出整个程序QApplication有个属性叫quitOnLastWindowClosed默认是true所有顶层窗口都关掉时事件循环退出exec()返回程序结束。这个默认值在单窗口应用里是对的多窗口应用里经常不是你要的。场景主窗口是个托盘常驻程序用户关掉主界面只是收进后台工具窗口还开着。这时候主窗口一关程序直接退出用户以为崩了。处理方式有两种。第一种是全局关掉自动退出int main(int argc, char *argv[]) { QApplication app(argc, argv); QApplication::setQuitOnLastWindowClosed(false); MainWindow w; w.show(); return app.exec(); }代价是所有窗口关掉之后程序还在后台跑你必须在明确的退出时机比如托盘菜单里点“退出”调用qApp-quit()否则进程挂在那里。这个属性改了之后一定要全工程搜一遍退出路径确认每一处都补上了quit()。第二种是保留默认行为但把“隐藏”和“关闭”区分开。主窗口的关闭事件里拦一下void MainWindow::closeEvent(QCloseEvent *e) { if (m_minimizeToTray) { e-ignore(); // 拒绝关闭 hide(); // 只隐藏 return; } // 真正退出先关掉所有子窗口避免残留 for (QWidget *w : QApplication::topLevelWidgets()) { if (w ! this w-isVisible()) w-close(); } QMainWindow::closeEvent(e); }这段代码解决了一个很实在的痛点主窗口关了独立子窗口还悬在桌面上用户不知道该怎么办。遍历topLevelWidgets()把它们一并关掉退出流程就干净了。注意判断w ! this否则会递归关自己。isVisible()这个判断也别省隐藏的窗口调close()会触发它的closeEvent如果你在子窗口里放了“确认关闭”的提示框用户会被莫名其妙弹一堆对话框。4.4 模态、非模态与返回值按场景选QDialog的exec()是模态会开一个局部事件循环阻塞调用点直到对话框关闭。show()是非模态立刻返回。模态的使用边界很明确用户必须先回答这个问题才能继续操作。参数设置、文件覆盖确认、必须填完的表单属于这一类。用模态的时候父指针给this好处是对话框居中在父窗口上、父窗口关掉时对话框跟着关、任务栏不会多出一个按钮。非模态适合日志窗口、预览窗口、工具箱、属性面板。这类窗口用户要一边看主界面一边操作它模态会把主界面锁死体验极差。非模态窗口记得给nullptr父指针并且走 3.4 里的模型二管理生命周期。还有个中间态很多人不知道setWindowModality(Qt::WindowModal)。它只阻塞自己的父窗口不影响同一进程里的其他窗口。做“编辑某个对象”的窗口时这个最合适——编辑窗开着的时候不能动主界面但其他工具窗口照常可用。用show()配合WindowModal就能做到比exec()灵活因为exec()一定是应用级或者窗口级模态而且会阻塞代码流。返回值的传递方式有两种exec()的返回码QDialog::Accepted/Rejected或者自定义信号。前者的前提是你用exec()用show()就必须走信号。我在项目里统一走信号因为切换exec/show的时候接口不用改。5. 实测踩过的坑与排查速查表下面这些是我和同事在实际项目里真踩过的按“现象 → 原因 → 处理”整理。遇到问题先查表能省掉大量盲猜时间。5.1 现象到原因的对照表现象最可能的原因处理方式子窗口点一下就沉到主窗口后面父指针非空或没设Qt::Window父指针改nullptr子窗口没有任务栏按钮类型是Qt::Tool或父指针非空显式设Qt::Window并清空父指针关掉子窗口后程序直接退出主窗口已隐藏quitOnLastWindowClosed生效设setQuitOnLastWindowClosed(false)或改隐藏策略第二次打开子窗口崩溃用了裸指针 WA_DeleteOnClose改用QPointer子窗口一闪就消失对象建在栈上函数返回即析构改堆上new或改用exec()主窗口关了子窗口还在进程不退父指针为空且没做联动关闭closeEvent里遍历关闭点按钮打开窗口没反应setWindowFlag之后忘了show()补一次show()窗口尺寸resize()无效布局的最小尺寸约束或设了setFixedSize查minimumSizeHint去掉固定尺寸打开第二个窗口后整个界面卡住用了exec()但本意是非模态改show()界面中文显示成方块源文件编码与编译器不一致源码统一 UTF-8编译器加/utf-8关闭窗口时偶发崩溃closeEvent里访问了已销毁的成员检查销毁顺序用QPointer表格第三行值得多说一句。这个现象的经典复现路径是主窗口先被用户关掉或者被隐藏到托盘此时屏幕上只剩子窗口用户关掉子窗口触发了“最后一个窗口关闭”事件循环退出。如果你的本意是程序继续在后台跑这就是 bug如果你的本意是退出那就没问题。判断标准只有一个——产品需求说关掉主界面之后程序该不该活着。5.2 退出崩溃和野指针的实录最典型的一次崩溃是这样子窗口设了WA_DeleteOnClose主窗口里用裸指针ChildWindow *m_child持有。用户关子窗口对象被deleteLater销毁此时主窗口的某个定时器还在跑定时器里访问m_child-update()指针早就野了。表现是关窗口之后过几秒钟程序闪退堆栈指向一个完全无关的位置debug起来极其费劲。根因在于deleteLater是延迟到事件循环空闲才执行销毁的那一刻没有“信号”通知持有方。解决要么用QPointer销毁后自动置空要么连destroyed信号connect(m_child, QObject::destroyed, this, [this]{ m_child nullptr; });两种都能解决QPointer更省事我一般直接上QPointer。它唯一的限制是不能用在QObject之外的类型上对窗口类没影响。另一个高频崩溃点是closeEvent里的逻辑。有个项目在子窗口的closeEvent里弹了个“是否保存”的确认框用户选择取消之后调了e-ignore()看起来没问题。但如果这时候主窗口正在被销毁整个对象树在析构过程中事件循环不转了弹框的exec()卡死表现是关程序时整个界面冻住。修法是加保护在qApp-closingDown()为真的时候跳过确认逻辑直接放行。void ChildWindow::closeEvent(QCloseEvent *e) { if (!qApp-closingDown() m_dirty) { auto ret QMessageBox::question(this, tr(确认), tr(内容尚未保存确定关闭)); if (ret ! QMessageBox::Yes) { e-ignore(); return; } } QWidget::closeEvent(e); }closingDown()这个接口文档里提得不多但在退出流程里非常实用。5.3 多屏、高 DPI 与窗口位置记忆高 DPI 在 Qt 6 里基本不用管默认就开着缩放Qt 5 需要在main里QApplication::setAttribute(Qt::AA_EnableHighDpiScaling)而且必须在构造QApplication之前调用顺序错了不生效也不会报错只会默默不缩放。窗口位置记忆是体验分水岭。一个能记住上次位置大小的工具窗口用起来会舒服非常多。子窗口保存geometry()主窗口在saveState()之外再存一份geometryvoid ChildWindow::closeEvent(QCloseEvent *e) { QSettings s; s.setValue(childWindow/geometry, saveGeometry()); QWidget::closeEvent(e); } void ChildWindow::restoreGeometryFromSettings() { QSettings s; const QByteArray g s.value(childWindow/geometry).toByteArray(); if (g.isEmpty()) { // 首次启动给个合理默认位置 resize(640, 480); placeOnScreen(parentWidget() ? parentWidget()-frameGeometry() : QRect(0, 0, 1280, 800)); } else { restoreGeometry(g); } }restoreGeometry会自动处理“上次的屏幕已经不在了”这种情况它会把窗口拉回可见区域这点比自己算坐标省心。但如果上次的窗口位置在第二块屏用户后来拔掉了外接屏restoreGeometry的兜底行为在不同平台上有差异保险起见在恢复后做一次可见性检查。多屏还有一个坑QWidget::screen()在 Qt 5.14 之前不存在老代码里通常用QDesktopWidget迁移时要改。新代码统一用QGuiApplication::screenAt(pos)和QWidget::screen()别再用QDesktopWidget它在 Qt 6 里已经移除了。6. 让这套结构长期可维护的几个习惯窗口拆分本身不难难的是三个月后回来改的时候还能看懂。这几个习惯是我从维护老项目里总结的成本低收益高。6.1 窗口单例化与重复打开的防护用户连点三次按钮弹出三个一模一样的窗口这是最常见的体验问题。防重入的写法在前面的onOpenChild()里已经体现了先判断指针是否有效有效就raiseactivateWindow。这里再补一个细节——QPointer失效之后对象已销毁指针变成nullptr判断仍然成立所以if (m_child)这一句同时覆盖了“从没打开过”和“打开过但已关闭”两种情况不需要额外状态位。如果窗口不只一个比如允许同时开多个监视窗口那就用QListQPointerChildWindow关闭时记得从列表里剔除。剔除时机放在连接destroyed信号的槽里比自己在closeEvent里删更可靠因为对象也可能被别处销毁。还有个小技巧给窗口设一个固定的objectName用findChild反查是否已存在。这个做法适合不方便持有指针的场景比如窗口由工厂函数创建但性能上不如直接持有指针数量多的项目别用。6.2 统一的关闭流程与配置持久化工程里窗口一多关闭流程会散在各处有的在主窗口closeEvent里有的在aboutToQuit信号里有的在析构函数里。建议收敛到一处所有“保存配置、关闭子窗口、释放资源”的动作挂在QApplication::aboutToQuit上closeEvent只管拦截用户操作和确认逻辑。connect(qApp, QCoreApplication::aboutToQuit, this, [this]{ saveLayoutToSettings(); for (auto *w : QApplication::topLevelWidgets()) { if (w ! this) w-close(); } });aboutToQuit在事件循环即将退出时发出此时窗口树还完整访问成员变量是安全的。把清理逻辑放这里无论用户是点主窗口的关闭按钮、点托盘菜单退出还是系统关机触发走的都是同一条路径不会漏。配置持久化用QSettings就够别自己写 ini 解析。注册表 vs ini 文件的选择交给默认策略QSettings不带参数构造会自动选平台推荐位置需要可移植配置的时候再传QSettings::IniFormat显式指定。有个小坑QSettings的value()返回QVariant取整数一定要给默认值value(k, 0).toInt()否则第一次运行读到空QVarianttoInt()返回 0看似没问题但如果是取尺寸、取枚举默认值缺失会导致窗口初始状态莫名其妙。最后提一句代码风格上的事。两个窗口之间的connect尽量集中在构造函数的一块区域里别散落在各个槽函数中。我见过一个项目connect写了七十多处分布在二十多个函数里后来要改信号签名直接重构了三天。集中管理这件事写的时候多花五分钟改的时候省三天。我个人在实际项目里的体会是独立窗口这件事代码量从来不是问题十几行就能跑起来真正决定项目能不能顺利往下走的是父指针、生命周期、关闭联动这三件事有没有在一开始就想清楚。见过太多项目在 Demo 阶段图快父指针随手写this等到要加第三个窗口、要做托盘常驻、要支持多屏记忆位置的时候前面埋的东西全部要推倒重来。反倒是那些一开始就老老实实写nullptr加QPointer的项目后面加窗口就是复制粘贴改个类名的事。如果只能记一条那就记这条窗口独不独立看的不是它长什么样是它的父指针指向谁。
阅读完成 · 觉得有帮助?