Qt窗口体系全解析:从QMainWindow到五大组件
我最早用Qt做桌面工具的时候最容易被一堆窗口概念绕晕菜单栏、工具栏、状态栏、浮动窗口、对话框每个看起来都是一个“窗口”但它们在QMainWindow里的职责和生命周期完全不一样。菜单栏挂在窗口顶部工具栏可以拖来拖去状态栏趴在底部浮动窗口能停靠也能飘出来而对话框通常是独立于主窗口弹出、用完即关的短生命周期界面。这篇文章就围绕这套窗口骨架展开讲清楚组件之间的层级关系、每个组件的创建方式、实际开发里容易踩的坑最后给一份可以直接跑起来的综合示例。无论你刚接触Qt还是已经写过几个小Demo只要没系统梳理过窗口体系这份内容应该都能帮上忙代码兼容Qt 5和Qt 6。1. 先把QMainWindow的骨架关系理清centralWidget和五个子区域的职责1.1 为什么必须用QMainWindow而不是QWidget很多人第一个Qt程序用QWidget当顶级窗口然后手动往里塞控件等想加菜单栏时发现QWidget根本没有menuBar()这个接口。原因其实很简单菜单栏、工具栏、状态栏和浮动窗口的停靠区域都是QMainWindow自带布局的一部分不是普通控件。QWidget绝对可以做顶级窗口但它不提供这套管理能力你只能用QMenuBar、QToolBar、QStatusBar自己拼一个“迷你版”窗口框架出来停靠功能还要自己实现越写到后面越痛苦。重点还得看两个进阶能力第一个是工具栏和浮动窗口的布局可以保存和恢复QMainWindow提供了saveState/restoreState第二个是QDockWidget能自动和菜单栏、状态栏协调停靠区域这两件事如果用QWidget手工模拟工作量会大很多。做标准桌面客户端时只要你不是在做异形窗口或者纯无边框播放器顶层窗口都默认选QMainWindow。MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { QWidget *central new QWidget(this); QVBoxLayout *layout new QVBoxLayout(central); layout-addWidget(new QTextEdit(central)); setCentralWidget(central); menuBar()-addMenu(tr(文件(F))); statusBar()-showMessage(tr(就绪)); }这里new QTextEdit(central)和layout-addWidget(new QTextEdit(central))两种方式都可以区别只是父对象在构造时指定还是由布局自动接管。对于只有一个核心控件的窗口两种都能跑但如果你在同一个容器里要放多个控件布局就是必须的否则控件位置和大小不会跟随窗口缩放很多新手会在这里遇到“控件叠在一起”的诡异现象。1.2 centralWidget的布局不能省有朋友直接把QTextEdit传给setCentralWidget只有一个核心控件时没问题因为QMainWindow会把它填满中央区域。可一旦要在中央区上方加搜索栏、下方加状态面板setCentralWidget只接受一个控件你不包一个容器Widget再放布局就只能靠绝对坐标写死窗口一拉伸就是灾难。所以我建议统一做法先创建一个QWidget作为centralWidget容器给它装QVBoxLayout再把业务控件加进布局最后setCentralWidget(container)。后续加一个分割条或者侧边栏基本只改布局代码。还有一个父子对象问题加到布局里的控件要么构造时传容器做父对象要么在addWidget之后让布局自动接管父对象。父对象错了窗口关闭时内存释放顺序会乱虽然多数情况下不会立刻崩溃但排查起来非常耗时间我见过不少新手因为随手new出控件但不指定父对象最后程序关闭时偶发崩溃这种问题用调试器不一定能稳定复现最难处理。2. 菜单栏与工具栏QAction才是真正的核心2.1 QAction如何串起菜单、工具栏和快捷键真正做过一个像样客户端之后你会认同一个结论菜单栏和工具栏只是外壳QAction才是核心。同一个QAction可以同时添加到菜单、工具栏甚至出现在右键菜单里你不需要为每个位置单独写一套逻辑triggered信号只连一次。动作一旦在某些状态下禁用比如没有打开文件时把“保存”置灰所有位置的按钮会一起变灰因为它们是同一个对象状态天然同步。快捷键也挂在QAction上比如setShortcut(QKeySequence::Open)这样菜单、工具栏、快捷键三种入口都统一走同一个动作。别小看这个模型很多入门Demo会在菜单槽里重复写处理逻辑又在工具栏按钮里再复制一遍后期改一处漏一处。正确做法是把action先创建好再往菜单和工具栏上挂。QAction *openAction new QAction(tr(打开), this); openAction-setShortcut(QKeySequence::Open); openAction-setStatusTip(tr(打开文件)); connect(openAction, QAction::triggered, this, MainWindow::openFile); QMenu *fileMenu menuBar()-addMenu(tr(文件(F))); fileMenu-addAction(openAction); QToolBar *toolBar addToolBar(tr(文件)); toolBar-addAction(openAction);QAction还有几个常用属性值得留意setCheckable(true)可以把动作变成“开关型”比如“显示工具栏”这种选项setEnabled(false)可以临时禁用某个功能setIcon可以让菜单项和工具栏按钮共用同一个图标。这些属性一旦设置所有挂载位置的表现都会跟着变开发效率很高。2.2 菜单栏创建过程中容易被忽略的细节中文字符串编码Qt 5之后源码默认UTF-8在Windows MSVC下如果不加/utf-8编译选项中文字符串可能乱码。以前见过项目把源码改成GBK来“解决”乱码反而让Qt的tr()在跨平台时更麻烦。建议统一UTF-8并在pro里给MSVC加编译参数反正Qt Creator默认就是UTF-8问题基本只出现在手动迁移的老工程上。助记符tr(文件(F))可以显示一个带下划线的F用户按AltF就能打开菜单这是老桌面交互习惯和QKeySequence::Open快捷键CtrlO不冲突。Windows用户很熟悉这套逻辑做跨平台产品时保留它没有坏处。右键菜单如果只是给某个控件加右键菜单推荐setContextMenuPolicy(Qt::CustomContextMenu)加customContextMenuRequested信号在槽里new一个QMenu(this)把需要的action加进去再menu-exec(pos)。注意这里的QMenu不要用栈对象因为exec会进入事件循环期间对象生命周期最好由Qt管理加上父对象后窗口关闭时自动销毁也少一个内存管理点。菜单分组用addSeparator()做分组不要每一项都直接堆。动作很多时子菜单比平铺更好用menu-addMenu()返回一个QMenu指针再往里加action即可。分组和子菜单用得好整个菜单栏的层级会清晰很多。2.3 工具栏按钮的显示策略和图标资源坑工具栏的按钮样式和菜单不一样菜单通常只有文字工具栏还要考虑图标、文字、图标和文字的排列。Qt默认是图标模式但如果一个QAction没有设置图标工具栏里就会退化成只显示文字不同平台表现还不一致。建议在设计初就明确setToolButtonStyle(Qt::ToolButtonTextUnderIcon)或者Qt::ToolButtonTextOnly别让平台默认值替你做决定。图标资源是新手最容易踩的坑。直接写QIcon(open.png)程序在Qt Creator里运行可能没问题一旦把可执行文件复制到别处或通过快捷方式启动工作目录变了图片就加载不出来。最稳妥的做法是把图片加进qrc资源文件代码里写QIcon(:/icons/open.png)。这里的:前缀不是普通路径是Qt资源系统的统一入口不依赖运行目录。QToolBar *fileToolBar addToolBar(tr(文件)); fileToolBar-addAction(openAction); fileToolBar-setToolButtonStyle(Qt::ToolButtonTextUnderIcon); fileToolBar-setMovable(false); fileToolBar-setIconSize(QSize(24, 24));工具栏默认可以被用户拖动到其他位置甚至拖出窗口。如果产品不希望工具栏被随意摆放可以setMovable(false)。但要注意有些用户会依赖这种自定义能力做得太死反而奇怪。多个toolbar并且需要恢复布局时每个toolbar都要有objectName否则saveState会分不清哪个是哪个。3. 状态栏的三类消息临时提示、常驻信息与右侧控件3.1 showMessage的临时提示只覆盖左侧区域状态栏的经典用途是“临时反馈”。QStatusBar::showMessage(message, timeout)显示消息默认显示在左侧超时后自动消失适合“保存完成”“文件已加载”这类一次性提示。但注意它显示期间会覆盖掉左侧通过addWidget加入的普通控件所以如果你用addWidget放了一个常驻标签又想用它显示临时消息就会看到标签被顶掉的效果容易误以为控件消失了。QAction自带statusTip机制鼠标悬停到菜单项或工具栏按钮上状态栏会自动显示statusTip内容。这个功能不花多少成本但对软件上手体验提升明显。给常用动作都设上statusTip用户把鼠标放上去就能看到功能说明相当于免费做了一版引导。3.2 addWidget与addPermanentWidget的左右分工状态栏其实分成两块区域左侧普通区右侧常驻区。左侧由addWidget加入普通控件会被showMessage覆盖右侧由addPermanentWidget加入不受临时消息影响适合放大写状态、网络状态、版本号这种一直不能丢的信息。理解这个分工之后很多“我更新了状态栏怎么没反应”的问题都能避免临时消息用showMessage持久状态用addPermanentWidget。QLabel *stateLabel new QLabel(tr(就绪), this); statusBar()-addPermanentWidget(stateLabel); QProgressBar *progress new QProgressBar(this); progress-setRange(0, 100); progress-setValue(0); progress-hide(); statusBar()-addWidget(progress); progress-show(); progress-setValue(50);同一个状态栏不要对同一个控件反复addWidgetQt会给出重复添加的警告布局也会乱。更安全的做法是让控件常驻用show/hide控制显隐stateLabel这种常驻信息一般从头到尾都在不需要增删。3.3 状态栏里放多个分区和自定义样式状态栏不只是文字栏你可以放多个QLabel、QProgressBar甚至按钮。多个标签时建议用QHBoxLayout先包一个QWidget再addWidget避免一个一个add导致代码零散。颜色提示用setStyleSheet很方便比如错误信息用QLabel { color: #c0392b; }正常状态用深绿色。这样用户不需要看模态弹窗也能感知状态变化。这里特别想说一个线程问题耗时任务在工作线程里更新状态栏不要直接调用setText或setValue应当通过信号槽把进度值发到主线程。很多人直接在子线程更新QLabelUI线程响应变慢、闪烁甚至崩溃其实根源是同时访问UI对象而不是简单的“线程安全”四个字能概括的。Qt的信号槽会自动把跨线程调用排队到接收者线程所以正确写法是子线程发信号主线程槽函数里做更新。4. 浮动窗口QDockWidget的可停靠机制与布局持久化4.1 QDockWidget到底在“浮动”什么浮动窗口这个词容易让人误解以为就是随便悬浮在窗口外面的小窗。其实QDockWidget更像一个可停靠的面板容器它是主窗口布局的一部分可以拖到四边停靠也可以从主窗口拖出来变成独立窗口。最重要的是内部的内容控件常见做法是先new一个QDockWidget再在里面放QListWidget、QTreeView或者自定义绘制面板然后用dock-setWidget()放入。QDockWidget *dock new QDockWidget(tr(信息面板), this); dock-setObjectName(QStringLiteral(InfoDock)); dock-setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); dock-setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable); QListWidget *list new QListWidget(dock); list-addItem(tr(文件列表)); dock-setWidget(list); addDockWidget(Qt::RightDockWidgetArea, dock);这段代码里setObjectName特别重要后面保存布局时要靠它识别面板。setAllowedAreas限定用户能停靠的方向如果四个方向都能停界面容易被拖乱setFeatures移除DockWidgetClosable用户就无法关闭面板。如果想保留关闭能力但不允许浮动就只保留Movable和Closable按产品需求调整。4.2 多个dock的堆叠与toggleViewAction多个dock同时存在时tabifyDockWidget(dock1, dock2)可以把两个dock合并成一组标签页用户能来回切换节省界面空间。要注意合并后的dock如果再次被用户拖出布局会重新计算和普通停靠排列不完全一样所以要提前决定哪些面板适合叠成标签哪些适合左右排列。更常用的是toggleViewAction()。每个QDockWidget都自带一个QAction它天然带着显示/隐藏切换和复选状态。把它加进“视图”菜单用户关闭dock后还能从菜单重新打开不用自己监听visibilityChanged信号。只靠dock标题栏关闭按钮的界面用户一旦误关就不知道怎么找回来体验会打折扣。4.3 保存与恢复窗口布局saveState/restoreState的正确姿势用户会拖dock、调整工具栏位置如果应用不记住每次启动都打回原形体验很差。QMainWindow::saveState可以保存所有工具栏和dock的布局状态返回QByteArray配合QSettings写进配置saveGeometry保存整个窗口的位置和大小这两个建议成对使用。restoreState一定要在创建完所有菜单、工具栏和dock之后再调用最好等centralWidget也设置好。顺序错了dock可能找不到自己对应的位置数据布局就会乱套。另外saveState和restoreState应该带同一个版本号比如saveState(1)restoreState(state, 1)。将来你新增或删除dock版本号不匹配时直接忽略旧布局避免出现诡异状态。void MainWindow::closeEvent(QCloseEvent *event) { QSettings settings(QStringLiteral(DemoOrg), QStringLiteral(WindowDemo)); settings.setValue(QStringLiteral(geometry), saveGeometry()); settings.setValue(QStringLiteral(state), saveState(1)); QMainWindow::closeEvent(event); }恢复时读取这两个keyQSettings settings(QStringLiteral(DemoOrg), QStringLiteral(WindowDemo)); restoreGeometry(settings.value(QStringLiteral(geometry)).toByteArray()); restoreState(settings.value(QStringLiteral(state)).toByteArray(), 1);有个容易被忽略的点saveGeometry保存窗口在屏幕上的位置和大小saveState保存内部布局二者要一起保存、一起恢复否则窗口位置变了dock布局还停在旧尺寸里视觉上非常不协调。5. 对话框模态/非模态、内置对话框和自定义QDialog的取舍5.1 exec()和show()之间的区别以及父子关系QDialog的exec()会进入一个局部事件循环代码停在那里直到对话框关闭并返回QDialog::Accepted或Rejected适合“必须处理完才能继续”的场景。因为阻塞用栈上局部变量就很安全。show()则立即返回对话框和主窗口可以同时交互这种情况下的对话框通常需要new出来并设置Qt::WA_DeleteOnClose否则局部栈对象会立刻析构画面一闪就没。QDialog dlg(this); dlg.exec(); // 对话框关闭后才执行到这里 QDialog *dlg2 new QDialog(this); dlg2-setAttribute(Qt::WA_DeleteOnClose); dlg2-show();父子关系同样很关键。有parent的对话框会跟随主窗口最小化和置顶不会额外出现在任务栏无parent的对话框可能独立出现在任务栏点击主窗口时甚至会被主窗口盖住找不到。很多“对话框消失”的诡异案例最后基本都是没传parent导致的。所以我的经验是能传this就传this。5.2 内置对话框能直接用就用Qt已经封装了一批高频对话框静态方法最省事。下面这张表基本覆盖了日常需求场景方法典型返回值普通提示QMessageBox::informationStandardButton确认操作QMessageBox::questionStandardButton打开文件QFileDialog::getOpenFileNameQString保存文件QFileDialog::getSaveFileNameQString选择颜色QColorDialog::getColorQColor单行输入QInputDialog::getTextQStringQMessageBox::StandardButton ret QMessageBox::question( this, tr(确认), tr(确定要删除吗), QMessageBox::Yes | QMessageBox::No); if (ret QMessageBox::Yes) { // 执行删除 }注意判断返回值时要用QMessageBox::Yes不要用true或false因为question返回的是按钮枚举不是布尔值。这是个特别容易犯的低级错误一旦按钮文字变了或者默认按钮位置调整用bool判断就会出现逻辑错位。内置对话框如果需要多选文件可以用实例方式因为静态方法getOpenFileName只返回一个文件QFileDialog dlg(this); dlg.setFileMode(QFileDialog::ExistingFiles); if (dlg.exec() QDialog::Accepted) { QStringList files dlg.selectedFiles(); }另外不要试图修改系统对话框的内部布局它们由平台样式表绘制强行塞自定义控件很容易出现兼容问题。静态方法只是常用场景的快捷方式复杂需求直接用实例会更清晰。5.3 自定义对话框的典型骨架和返回值传递业务里总有系统对话框覆盖不了的场景比如批量导入时输入名称和路径。自定义对话框继承QDialog核心组合是QVBoxLayout加QLineEdit或QComboBox再加一个QDialogButtonBox。QDialogButtonBox的accepted和rejected信号分别连到accept()和reject()这样点“确定”和“取消”的语义就直接映射到对话框返回值不需要额外写业务逻辑。class NameDialog : public QDialog { Q_OBJECT public: explicit NameDialog(QWidget *parent nullptr) : QDialog(parent) { lineEdit new QLineEdit(this); auto *buttons new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); auto *layout new QVBoxLayout(this); layout-addWidget(new QLabel(tr(名称), this)); layout-addWidget(lineEdit); layout-addWidget(buttons); connect(buttons, QDialogButtonBox::accepted, this, QDialog::accept); connect(buttons, QDialogButtonBox::rejected, this, QDialog::reject); } QString name() const { return lineEdit-text(); } private: QLineEdit *lineEdit; };调用方这样使用NameDialog dlg(this); if (dlg.exec() QDialog::Accepted) { QString name dlg.name(); // 使用返回值 }自定义对话框的返回值用一个getter暴露就够不要在对话框内部直接操作主窗口的数据。保持低耦合的好处是这个对话框以后在其他项目里也能复用测试和维护都省心。实际编译时记得把QLineEdit、QVBoxLayout、QDialogButtonBox等头文件按需包含进来。6. 把五块拼成完整窗口一份可跑的示例代码和关键坑位6.1 工程结构在Qt Creator新建项目时选择Qt Widgets Application或者手写一个最简单的pro文件。由于这里用了QMainWindow、QDockWidget、QFileDialog这些类项目必须要链接Qt Widgets模块。QT widgets SOURCES main.cpp mainwindow.cpp HEADERS mainwindow.h如果用CMake就在CMakeLists里链接Qt6::Widgets或Qt5::Widgets其他模块不需要额外引入。6.2 核心代码下面这份示例把前面讲到的五个组件都串起来了菜单栏提供打开、保存、关于工具栏复用同一组action状态栏有常驻信息右侧停靠了一个信息面板对话框通过打开文件功能触发。// main.cpp #include QApplication #include mainwindow.h int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow w; w.show(); return app.exec(); }// mainwindow.h #ifndef MAINWINDOW_H #define MAINWINDOW_H #include QMainWindow #include QCloseEvent class QAction; class QMenu; class QToolBar; class QTextEdit; class QDockWidget; class QLabel; class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent nullptr); ~MainWindow() override default; protected: void closeEvent(QCloseEvent *event) override; private slots: void openFile(); void saveFile(); void about(); private: void createActions(); void createMenus(); void createToolBars(); void createStatusBar(); void createDockWindows(); QTextEdit *textEdit; QLabel *statusLabel; QDockWidget *infoDock; QAction *openAction; QAction *saveAction; QAction *aboutAction; }; #endif// mainwindow.cpp #include mainwindow.h #include QAction #include QMenu #include QMenuBar #include QToolBar #include QStatusBar #include QDockWidget #include QTextEdit #include QLabel #include QProgressBar #include QFileDialog #include QMessageBox #include QVBoxLayout #include QListWidget #include QSettings #include QFile MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) { textEdit new QTextEdit(this); setCentralWidget(textEdit); createActions(); createMenus(); createToolBars(); createStatusBar(); createDockWindows(); setWindowTitle(tr(Qt 窗口组件综合示例)); resize(900, 600); } void MainWindow::createActions() { openAction new QAction(tr(打开), this); openAction-setShortcut(QKeySequence::Open); openAction-setStatusTip(tr(打开一个文本文件)); connect(openAction, QAction::triggered, this, MainWindow::openFile); saveAction new QAction(tr(保存), this); saveAction-setShortcut(QKeySequence::Save); saveAction-setStatusTip(tr(保存当前内容)); connect(saveAction, QAction::triggered, this, MainWindow::saveFile); aboutAction new QAction(tr(关于), this); connect(aboutAction, QAction::triggered, this, MainWindow::about); } void MainWindow::createMenus() { QMenu *fileMenu menuBar()-addMenu(tr(文件(F))); fileMenu-addAction(openAction); fileMenu-addAction(saveAction); fileMenu-addSeparator(); fileMenu-addAction(aboutAction); } void MainWindow::createToolBars() { QToolBar *fileToolBar addToolBar(tr(文件)); fileToolBar-setObjectName(QStringLiteral(FileToolBar)); fileToolBar-addAction(openAction); fileToolBar-addAction(saveAction); fileToolBar-setToolButtonStyle(Qt::ToolButtonTextOnly); fileToolBar-setMovable(false); } void MainWindow::createStatusBar() { statusLabel new QLabel(tr(就绪), this); statusBar()-addPermanentWidget(statusLabel); QProgressBar *progress new QProgressBar(this); progress-setRange(0, 100); progress-setValue(0); progress-hide(); statusBar()-addWidget(progress); } void MainWindow::createDockWindows() { infoDock new QDockWidget(tr(信息面板), this); infoDock-setObjectName(QStringLiteral(InfoDock)); infoDock-setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea); QListWidget *listWidget new QListWidget(infoDock); listWidget-addItem(tr(文件列表)); listWidget-addItem(tr(搜索记录)); infoDock-setWidget(listWidget); addDockWidget(Qt::RightDockWidgetArea, infoDock); QMenu *viewMenu menuBar()-addMenu(tr(视图(V))); viewMenu-addAction(infoDock-toggleViewAction()); } void MainWindow::openFile() { const QString file QFileDialog::getOpenFileName( this, tr(选择文件), QString(), tr(文本文件 (*.txt);;所有文件 (*))); if (file.isEmpty()) return; QFile f(file); if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { QMessageBox::warning(this, tr(提示), tr(无法打开文件)); return; } textEdit-setPlainText(QString::fromUtf8(f.readAll())); statusBar()-showMessage(tr(已打开%1).arg(file), 5000); } void MainWindow::saveFile() { const QString file QFileDialog::getSaveFileName( this, tr(保存文件), QString(), tr(文本文件 (*.txt);;所有文件 (*))); if (file.isEmpty()) return; QFile f(file); if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) { QMessageBox::warning(this, tr(提示), tr(无法写入文件)); return; } f.write(textEdit-toPlainText().toUtf8()); statusBar()-showMessage(tr(已保存%1).arg(file), 5000); } void MainWindow::about() { QMessageBox::about(this, tr(关于), tr(一个演示菜单栏、工具栏、状态栏、浮动窗口和对话框的示例程序。)); } void MainWindow::closeEvent(QCloseEvent *event) { QSettings settings(QStringLiteral(DemoOrg), QStringLiteral(WindowDemo)); settings.setValue(QStringLiteral(geometry), saveGeometry()); settings.setValue(QStringLiteral(state), saveState(1)); QMainWindow::closeEvent(event); }这段代码可以直接跑起来看效果。信息面板的显示切换在“视图”菜单里工具栏只有文字按钮因为示例没有额外设计图标实际项目里把ToolButtonTextOnly换成带图标的样式再配合qrc资源文件即可。6.3 关键坑位回顾QAction生命周期所有action都new到this上不要在槽函数里局部new。否则窗口关闭后动作对象无人管理或者多次触发同一动作时反复创建对象信号和内存都容易乱。dock和toolbar必须设objectName不设的后果是saveState恢复时找不到对应面板用户上次拖好的布局无法恢复。状态栏的临时消息和常驻widget各自独立临时消息覆盖左侧普通区不影响右侧常驻区这个分工记牢就好。对话框传parent所有QDialog尽量带this避免窗口层级错乱非模态show出来的对话框要加WA_DeleteOnClose。跨线程更新UI请走信号槽工作线程不要直接操作状态栏、进度条这些界面对象否则轻则闪烁、重则崩溃。最后说说我自己的体会。这套窗口骨架我每隔一段时间就会翻出来用一次代码结构基本固定先建中央控件再把动作、菜单、工具栏、状态栏、浮动窗口逐个创建最后处理布局保存和恢复。真正让我少踩坑的是“所有对象都要有明确生命周期”这件事QAction父对象传this、dock设objectName、非模态对话框加WA_DeleteOnClose顺序看起来琐碎但最常出问题的就是这些细节。如果你准备在项目里加入或重构窗口体系建议先照上面的骨架搭一版最小可用程序再往里填业务后面会顺很多。