QMainWindow 与应用程序框架:菜单栏、工具栏、状态栏
引言
前几篇完成了 Qt 环境、事件循环、构建流程和模块的准备。从这一篇开始,程序终于不再只是"弹出一个窗口",而是有了传统桌面软件的基本外形:顶部菜单栏、常用操作的工具栏、承载页面的中心区域,以及在底部反馈状态的状态栏。
在 Qt Widgets 中,这个外形由 QMainWindow 提供。它不是一个"比 QWidget 更大的控件",而是已经规定好区域职责的应用程序外壳。入门时先会把功能放到正确位置;程序变大后,再理解为什么命令应该复用、业务不应都塞进窗口类。
本文面向 Qt 6 Widgets,示例按 Qt 6.8.x 编写。读完后应能完成这些事:
- 用
QMainWindow搭好一个可运行的桌面主窗口; - 让一个
QAction同时服务菜单和工具栏; - 正确使用状态栏的临时消息与永久状态区;
- 知道
QMainWindow的内置结构适合什么,不适合什么; - 在程序变复杂后,将窗口代码与业务代码逐步分开。
一、先认识主窗口的五个区域
QMainWindow 内部提供了一套专门的主窗口布局框架,负责管理菜单栏、工具栏、停靠窗口、中心控件和状态栏等区域。我们不需要自己计算这些区域的位置,只需要通过对应的 API 把控件交给 QMainWindow 管理。

图 1:QMainWindow 管理应用外壳;中心控件中的内容再由自己的布局管理。
| 区域 | 常用 API | 应该放什么 |
|---|---|---|
| 菜单栏 | menuBar()、addMenu() |
按类别组织的完整命令集合,如"文件""编辑" |
| 工具栏 | addToolBar() |
用户高频操作的快捷入口,如新建、保存、刷新 |
| 中心区域 | setCentralWidget() |
页面、表格、编辑器、QStackedWidget 等主内容 |
| 停靠区 | addDockWidget() |
可显示、隐藏或拖动的辅助面板,如属性、日志 |
| 状态栏 | statusBar() |
短暂提示、连接状态、进度和坐标等轻量信息 |
先记住一个边界:一个 QMainWindow 只能设置一个中心控件 。如果中心区域中要放多个控件,就给这个中心控件安装 QVBoxLayout、QGridLayout 等布局。
cpp
auto *centralWidget = new QWidget(this);
auto *layout = new QVBoxLayout(centralWidget);
layout->addWidget(new QLabel(tr("用户名")));
layout->addWidget(new QLineEdit);
layout->addWidget(new QPushButton(tr("登录")));
setCentralWidget(centralWidget);
setCentralWidget() 设置的是"中心区域的根控件",而不是说中心区域只能放一个控件。
另一个常见错误是对 QMainWindow 直接调用 setLayout()。不要这样做。QMainWindow 已有专门的内部布局,菜单栏、工具栏、停靠区和状态栏正是由它安排的;应该给 centralWidget() 或独立页面设置布局。
二、先跑起来:一个最小但完整的主窗口
先创建一个 Widgets 项目,并确认构建脚本链接了 Widgets 模块:
cmake
cmake_minimum_required(VERSION 3.21)
project(MainWindowStarter LANGUAGES CXX)
find_package(Qt6 REQUIRED COMPONENTS Widgets)
qt_add_executable(MainWindowStarter
main.cpp
mainwindow.h
mainwindow.cpp
)
target_link_libraries(MainWindowStarter PRIVATE Qt6::Widgets)
main.cpp 依旧遵循第三篇中的入口结构。使用 QMainWindow 仍然需要 QApplication,因为它属于 Widgets 体系。
cpp
#include <QApplication>
#include "mainwindow.h"
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
MainWindow window;
window.show();
return app.exec();
}
下面的 MainWindow 没有借助 Designer,目的是先看清每个区域由哪一行代码创建。分别保存为 mainwindow.h 和 mainwindow.cpp。
cpp
// mainwindow.h
#pragma once
#include <QMainWindow>
class QAction;
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = nullptr);
private slots:
void createDocument();
void saveDocument();
private:
void createActions();
void createMenus();
void createToolBars();
void createStatusBar();
QAction *m_newAction = nullptr;
QAction *m_saveAction = nullptr;
};
cpp
// mainwindow.cpp
#include "mainwindow.h"
#include <QAction>
#include <QKeySequence>
#include <QLabel>
#include <QMenu>
#include <QMenuBar>
#include <QStatusBar>
#include <QTextEdit>
#include <QToolBar>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
setWindowTitle(tr("记事本示例"));
resize(900, 600);
// 中心控件是真正编辑内容所在的区域。
auto *editor = new QTextEdit(this);
editor->setPlaceholderText(tr("在这里输入内容..."));
setCentralWidget(editor);
createActions();
createMenus();
createToolBars();
createStatusBar();
}
void MainWindow::createActions()
{
m_newAction = new QAction(tr("新建(&N)"), this);
m_newAction->setShortcut(QKeySequence::New);
m_newAction->setStatusTip(tr("新建一个空白文档"));
connect(m_newAction, &QAction::triggered,
this, &MainWindow::createDocument);
m_saveAction = new QAction(tr("保存(&S)"), this);
m_saveAction->setShortcut(QKeySequence::Save);
m_saveAction->setStatusTip(tr("保存当前文档"));
connect(m_saveAction, &QAction::triggered,
this, &MainWindow::saveDocument);
}
void MainWindow::createMenus()
{
QMenu *fileMenu = menuBar()->addMenu(tr("文件(&F)"));
fileMenu->addAction(m_newAction);
fileMenu->addAction(m_saveAction);
}
void MainWindow::createToolBars()
{
QToolBar *fileToolBar = addToolBar(tr("文件"));
fileToolBar->setMovable(false);
fileToolBar->addAction(m_newAction);
fileToolBar->addAction(m_saveAction);
}
void MainWindow::createStatusBar()
{
statusBar()->showMessage(tr("就绪"));
auto *connectionLabel = new QLabel(tr("未连接"), this);
statusBar()->addPermanentWidget(connectionLabel);
}
void MainWindow::createDocument()
{
auto *editor = qobject_cast<QTextEdit *>(centralWidget());
editor->clear();
statusBar()->showMessage(tr("已新建空白文档"), 2000);
}
void MainWindow::saveDocument()
{
// 本例只演示命令流,真实保存会在下一篇文件对话框文章中补齐。
statusBar()->showMessage(tr("已保存"), 2000);
}
运行后,按下 Ctrl+N、点击"文件 > 新建",或点击工具栏"新建",都会调用同一个槽函数。这里最值得学的不是控件摆放,而是下面的 QAction 设计。
运行图如下:输入文本并使用 Ctrl+s 进行保存

三、不要为菜单和工具栏各写一套逻辑:使用 QAction
QAction 表示一个抽象的"用户命令",而不是一个按钮。QAction 没有负责绘制按钮或菜单项,它更接近一个"命令对象"。 QMenu、QToolBar 等控件只是把这个命令以不同形式展示出来。
它可以保存文本、图标、快捷键、是否可用、提示文字和触发信号;菜单项、工具栏按钮甚至右键菜单都可以展示同一个动作。
cpp
QAction *saveAction = new QAction(tr("保存(&S)"), this);
saveAction->setShortcut(QKeySequence::Save);
saveAction->setEnabled(false);
fileMenu->addAction(saveAction);
fileToolBar->addAction(saveAction);
这样做的直接收益是状态一致。比如文档尚未修改时只需要写一行:
cpp
m_saveAction->setEnabled(documentModified);
菜单中的"保存"、工具栏的"保存"和快捷键都会同步禁用。反过来,如果为菜单按钮和工具栏按钮分别连接槽函数、分别维护可用状态,功能增加后很容易出现一个入口可点、另一个入口不可点的错误。
3.1 文本、快捷键和翻译
菜单文本中的 & 表示助记键,例如 tr("文件(&F)") 在多数平台会让用户通过键盘定位菜单。QKeySequence::Save 使用平台习惯的保存快捷键,在 Windows 通常是 Ctrl+S,在 macOS 会适配为 Command+S。
所有面向用户的文字都应使用 tr(),包括动作文本、状态提示和窗口标题。第十五篇会继续讲 tr() 如何生成可翻译资源;现在先养成习惯即可。
需要注意,&F 和 Ctrl+F 是两个不同概念:前者是菜单/控件的助记键,后者是 QAction 的快捷键。
cpp
//文件(&F)
//对应的是菜单定位:Alt + F
m_saveAction->setShortcut(QKeySequence::Save);
//对应的是命令快捷键:Ctrl + S
3.2 高频工具栏建议提供图标
工具栏通常比菜单更依赖图标识别。若图标放在 Qt 资源文件中,可以这样创建动作:
cpp
QAction *openAction = new QAction(
QIcon(":/icons/open.svg"), tr("打开(&O)"), this);
openAction->setShortcut(QKeySequence::Open);
图标应使用清晰、稳定的资源路径,并为图标含义提供文字或工具提示。不要只依赖颜色,也不要把一长串低频命令都塞进工具栏。菜单负责"找得到全部功能",工具栏负责"最快到达高频功能"。
四、菜单栏:按任务分类,而不是按类名堆放
menuBar() 返回主窗口的菜单栏,addMenu() 创建一级菜单,addAction() 将命令放入菜单。
cpp
QMenu *fileMenu = menuBar()->addMenu(tr("文件(&F)"));
fileMenu->addAction(m_newAction);
fileMenu->addAction(m_saveAction);
fileMenu->addSeparator();
fileMenu->addAction(tr("退出(&X)"), this, &QWidget::close);
入门阶段可以使用一套稳定的分类:
| 菜单 | 通常包含的命令 |
|---|---|
| 文件 | 新建、打开、保存、导入、导出、退出 |
| 编辑 | 撤销、重做、剪切、复制、粘贴、查找 |
| 视图 | 显示或隐藏工具栏、状态栏、停靠面板 |
| 工具 / 设置 | 配置、连接、执行工具任务 |
| 帮助 | 文档、关于、检查更新 |
菜单项不宜按照 C++ 类名组织。用户要完成的是"保存""查看""配置"这些任务,而不是理解你的代码目录。
五、工具栏:只保留高频、可识别的操作
addToolBar() 会创建并放入一个工具栏:
cpp
QToolBar *editToolBar = addToolBar(tr("编辑"));
editToolBar->addAction(undoAction);
editToolBar->addAction(redoAction);
editToolBar->addSeparator();
editToolBar->addAction(findAction);
工具栏可以停靠在上、下、左、右边缘,也可以让用户拖动。对内部工具或需要用户自由布置的复杂软件,可保留默认可移动行为;对流程固定、希望界面稳定的新手示例,setMovable(false) 更容易控制体验。
如果图标本身不够直观,可以在窗口级别设置按钮风格:
cpp
setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
但这不是越多文字越好。窄窗口下工具栏会变得拥挤。更实用的做法是:只给最常用的命令保留工具栏入口,其他功能仍放在菜单中。
六、状态栏:区分短消息与长期状态
状态栏适合告诉用户"刚刚发生了什么"和"程序现在处于什么状态",但不适合承载需要用户决定的错误。需要用户确认、修复或选择的异常,应在 QMessageBox 或专门的错误页。
showMessage() 适合短消息,第二个参数是超时毫秒数:
cpp
statusBar()->showMessage(tr("文件保存成功"), 3000);
addPermanentWidget() 适合持续可见的信息,例如网络连接、当前用户、行列号或模式。状态变化时保留控件指针并更新它:
cpp
// 成员:QLabel *m_connectionLabel = nullptr;
m_connectionLabel->setText(connected ? tr("已连接") : tr("未连接"));
可以用 addWidget() 放置普通状态项,用 addPermanentWidget() 放置靠右且不被临时消息遮挡的项目。一个简单判断是:用户是否需要在操作完成几秒后继续看到它?需要,就放永久区;不需要,就用临时消息。
七、QMainWindow 内部大致做了什么
初学阶段无需阅读 Qt 私有源码,但理解它的工作方式能避免误用。
QMainWindow 继承自 QWidget,却不把自己当作通用空白容器。它的内部布局会根据窗口大小、工具栏停靠位置、状态栏高度和停靠面板状态,计算每个区域的几何位置;中心控件拿到剩余空间。addToolBar()、setCentralWidget()、setStatusBar() 这些接口本质上是在登记对应区域,由主窗口在窗口大小变化时统一重新布局。
QAction 则是命令对象。菜单和工具栏并不复制一份"保存逻辑",而是各自创建同一个动作的可视化表示。动作的 enabled、checked、文本、图标和快捷键变化后,各入口会一起更新。
这也解释了两个设计结论:
- 不要给
QMainWindow自己再安装普通布局; - 不要把"保存"写成两个独立的按钮逻辑,先建立一个
QAction。
八、内置框架的边界:什么时候不该硬用 QMainWindow
QMainWindow 很适合菜单驱动的传统桌面工具,例如配置工具、IDE 类软件、工业监控客户端和编辑器。但它不是所有窗口的默认答案。
| 场景 | 更合适的选择 | 原因 |
|---|---|---|
| 登录框、设置页、一次性确认 | QDialog |
生命周期和交互目标都不是主应用外壳 |
| 一个简单嵌入式小面板 | QWidget |
不需要菜单、工具栏、停靠与状态栏的固定区域 |
| 高度品牌化、无边框、区域完全自定义的窗口 | QWidget + 自定义布局 |
QMainWindow 的停靠和区域规则会限制布局 |
| 动画、触摸和声明式界面优先的产品 | Qt Quick / QML | Widgets 的传统控件框架不是最佳主路线 |
内置结构的"限制"并非缺点本身。它用约定换来了成熟的停靠、状态栏、工具栏和平台行为。只有当产品布局确实不符合这种桌面外壳时,才应该自己搭建外壳。
九、需要完全自定义时,怎样搭一个轻量外壳
若窗口不需要停靠区,而是要严格控制每一行位置,可以使用普通 QWidget 作为顶层窗口,并用布局显式放置 QMenuBar、QToolBar、内容区和 QStatusBar:
cpp
#include <QMenuBar>
#include <QToolBar>
#include <QTextEdit>
#include <QStatusBar>
#include <QVBoxLayout>
class CustomShell : public QWidget
{
public:
explicit CustomShell(QWidget *parent = nullptr)
: QWidget(parent)
{
auto *layout = new QVBoxLayout(this);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
auto *menuBar = new QMenuBar(this);
auto *toolBar = new QToolBar(this);
auto *content = new QTextEdit(this);
auto *statusBar = new QStatusBar(this);
layout->addWidget(menuBar);
layout->addWidget(toolBar);
layout->addWidget(content, 1);
layout->addWidget(statusBar);
}
};
// 在main函数直接调用
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
// MainWindow w;
// w.show();
CustomShell cw;
cw.show();
return a.exec();
}
这段代码能搭出视觉结构,但需要自己承担更多职责:工具栏是否允许移动、用户布局怎样保存、停靠面板如何实现、窗口状态如何恢复。它适用于刻意简化或完全自定义的壳,不是为了"看起来更底层"而替换 QMainWindow。
QWidget + QVBoxLayout 拼出来的只是"长得像 QMainWindow",并不会自动获得 QMainWindow 提供的工具栏停靠、Dock 管理、窗口状态保存等能力。
十、从一个窗口到一个应用:业务代码放在哪里
刚开始时,把槽函数写在 MainWindow 中完全合理。随着"保存"涉及校验、文件格式、最近文件列表、网络同步或权限判断,窗口类会迅速膨胀。此时应把业务逐步移到专门的应用服务或控制器中,让窗口只负责收集输入和显示结果。

图 2:依赖从界面层向下。窗口可以调用应用层,但领域模型不应该反过来依赖 QMainWindow 或 QAction。
例如,先把动作连接到一个语义明确的槽:
cpp
connect(m_saveAction, &QAction::triggered,
this, &MainWindow::saveDocument);
当保存逻辑复杂后,槽函数只做编排:
cpp
void MainWindow::saveDocument()
{
const SaveResult result = m_documentController->save(currentDocument());
if (result.ok) {
statusBar()->showMessage(tr("已保存"), 2000);
} else {
statusBar()->showMessage(tr("保存失败:%1").arg(result.message), 5000);
//如果业务层需要返回错误原因,不建议直接让业务层返回 UI 文本
//可以返回错误码或结构化错误信息,由 UI 层决定最终显示文字。
}
}
不要为了这个目标立刻创建十几个"Manager"类。可靠的演进方式是:当一个槽函数开始同时处理界面、业务规则和外部资源时,再把可独立命名和测试的部分提取出去。
十一、常见问题清单
11.1 为什么菜单和工具栏的"保存"状态不一致?
通常是因为菜单和工具栏分别创建了两个独立的 QAction,然后分别维护它们的状态。让两处都调用 addAction(m_saveAction),并只维护这一个动作的 enabled 状态。
11.2 为什么中心控件没有铺满窗口?
确认调用的是 setCentralWidget(widget),而不是只 new 了控件。若中心控件本身还包含多个子控件,为它创建布局,并把布局设置到中心控件上。
11.3 为什么状态栏消息一闪而过?
showMessage(text, timeout) 到时会自动清除。连接状态、当前用户等长期信息应使用 addPermanentWidget() 中的 QLabel 或其他小控件。
11.4 可以在 QMainWindow 里直接创建 QDockWidget 吗?
可以。创建后通过 addDockWidget(Qt::LeftDockWidgetArea, dock) 添加。它很适合日志、对象树和属性编辑器,但在入门主线中先把菜单、工具栏、中心区和状态栏用熟,再引入可拖动面板。
11.5 为什么关闭窗口后程序没有退出?
通常是仍有其他顶层窗口,或代码改动了默认的关闭行为。一般桌面程序保持 QApplication 的默认 quitOnLastWindowClosed 行为即可;不要在每个按钮里强行调用 app.quit()。
十二、总结
这一篇真正要掌握的是三件事:
QMainWindow为传统桌面应用提供菜单栏、工具栏、中心区、停靠区与状态栏,中心内容通过setCentralWidget()放入;QAction是用户命令的统一载体,菜单、工具栏、快捷键和右键菜单可以复用同一个QAction;- 小项目可以先在
MainWindow写槽函数,复杂后把业务和外部访问向下拆分,保持窗口类专注于界面。
下一篇会进入对话框系统。主窗口中的"打开""保存""错误提示"等命令,都会通过模态与非模态对话框补上完整交互。
下一篇预告:《对话框系统全解析------模态/非模态、QMessageBox、QFileDialog》
口后程序没有退出?
通常是仍有其他顶层窗口,或代码改动了默认的关闭行为。一般桌面程序保持 QApplication 的默认 quitOnLastWindowClosed 行为即可;不要在每个按钮里强行调用 app.quit()。
十二、总结
这一篇真正要掌握的是三件事:
QMainWindow为传统桌面应用提供菜单栏、工具栏、中心区、停靠区与状态栏,中心内容通过setCentralWidget()放入;QAction是用户命令的统一载体,菜单、工具栏、快捷键和右键菜单可以复用同一个QAction;- 小项目可以先在
MainWindow写槽函数,复杂后把业务和外部访问向下拆分,保持窗口类专注于界面。
下一篇会进入对话框系统。主窗口中的"打开""保存""错误提示"等命令,都会通过模态与非模态对话框补上完整交互。
下一篇预告:《对话框系统全解析------模态/非模态、QMessageBox、QFileDialog》