我最早用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,用户按Alt+F就能打开菜单,这是老桌面交互习惯,和QKeySequence::Open(快捷键Ctrl+O)不冲突。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);同一个状态栏不要对同一个控件反复addWidget,Qt会给出重复添加的警告,布局也会乱。更安全的做法是让控件常驻,用show/hide控制显隐,stateLabel这种常驻信息一般从头到尾都在,不需要增删。
3.3 状态栏里放多个分区和自定义样式
状态栏不只是文字栏,你可以放多个QLabel、QProgressBar,甚至按钮。多个标签时建议用QHBoxLayout先包一个QWidget再addWidget,避免一个一个add导致代码零散。颜色提示用setStyleSheet很方便,比如错误信息用QLabel { color: #c0392b; },正常状态用深绿色。这样用户不需要看模态弹窗也能感知状态变化。
这里特别想说一个线程问题:耗时任务在工作线程里更新状态栏,不要直接调用setText或setValue,应当通过信号槽把进度值发到主线程。很多人直接在子线程更新QLabel,UI线程响应变慢、闪烁甚至崩溃,其实根源是同时访问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); }恢复时读取这两个key:
QSettings 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::information | StandardButton |
| 确认操作 | QMessageBox::question | StandardButton |
| 打开文件 | QFileDialog::getOpenFileName | QString |
| 保存文件 | QFileDialog::getSaveFileName | QString |
| 选择颜色 | QColorDialog::getColor | QColor |
| 单行输入 | QInputDialog::getText | QString |
QMessageBox::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,顺序看起来琐碎,但最常出问题的就是这些细节。如果你准备在项目里加入或重构窗口体系,建议先照上面的骨架搭一版最小可用程序,再往里填业务,后面会顺很多。