07_QMainWindow 与应用程序框架——菜单栏、工具栏、状态栏

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 只能设置一个中心控件 。如果中心区域中要放多个控件,就给这个中心控件安装 QVBoxLayoutQGridLayout 等布局。

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.hmainwindow.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 没有负责绘制按钮或菜单项,它更接近一个"命令对象"。 QMenuQToolBar 等控件只是把这个命令以不同形式展示出来。

它可以保存文本、图标、快捷键、是否可用、提示文字和触发信号;菜单项、工具栏按钮甚至右键菜单都可以展示同一个动作。

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() 如何生成可翻译资源;现在先养成习惯即可。

需要注意,&FCtrl+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 则是命令对象。菜单和工具栏并不复制一份"保存逻辑",而是各自创建同一个动作的可视化表示。动作的 enabledchecked、文本、图标和快捷键变化后,各入口会一起更新。

这也解释了两个设计结论:

  • 不要给 QMainWindow 自己再安装普通布局;
  • 不要把"保存"写成两个独立的按钮逻辑,先建立一个 QAction

八、内置框架的边界:什么时候不该硬用 QMainWindow

QMainWindow 很适合菜单驱动的传统桌面工具,例如配置工具、IDE 类软件、工业监控客户端和编辑器。但它不是所有窗口的默认答案。

场景 更合适的选择 原因
登录框、设置页、一次性确认 QDialog 生命周期和交互目标都不是主应用外壳
一个简单嵌入式小面板 QWidget 不需要菜单、工具栏、停靠与状态栏的固定区域
高度品牌化、无边框、区域完全自定义的窗口 QWidget + 自定义布局 QMainWindow 的停靠和区域规则会限制布局
动画、触摸和声明式界面优先的产品 Qt Quick / QML Widgets 的传统控件框架不是最佳主路线

内置结构的"限制"并非缺点本身。它用约定换来了成熟的停靠、状态栏、工具栏和平台行为。只有当产品布局确实不符合这种桌面外壳时,才应该自己搭建外壳。

九、需要完全自定义时,怎样搭一个轻量外壳

若窗口不需要停靠区,而是要严格控制每一行位置,可以使用普通 QWidget 作为顶层窗口,并用布局显式放置 QMenuBarQToolBar、内容区和 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:依赖从界面层向下。窗口可以调用应用层,但领域模型不应该反过来依赖 QMainWindowQAction

例如,先把动作连接到一个语义明确的槽:

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》

相关推荐
总有刁民想爱朕ha2 小时前
Python PyQt5图片批量转MP4视频工具:本地离线免费无水印,完整代码
开发语言·python·qt
_Narcissus_2 小时前
二分算法笔记及例题
数据结构·c++·笔记·算法·蓝桥杯·查找·二分算法
青少儿编程课堂4 小时前
用图形化编程做一个“少年探险闯关”小游戏:方向键控制、碰撞检测与多关卡串起完整项目
c++·python·算法·bfs·信息学竞赛
liulilittle5 小时前
长上下文的成本结构与「甜点区间」——从推理引擎的物理约束看 200K/256K/400K
c++·人工智能·ai·llm·注意力·qkv
十五年专注C++开发5 小时前
100w条数据丝滑滚动!Qt Model/View 架构实战(二)
开发语言·c++·qt
skr爱码士5 小时前
06_Qt 常用数据类型与容器:QString、QList、QVector、QMap 的性能与实现分析
开发语言·c++·qt
奶茶树6 小时前
【C++】13. C++11新特性【上】
c语言·开发语言·c++·git·github
wabs6666 小时前
关于栈【力扣1047. 删除字符串中的所有相邻重复项的思考】
数据结构·c++·算法·leetcode··代码随想录
橘色的喵7 小时前
PySide6 工业上位机的实时帧链、零拷贝与跨语言架构
c++·架构·图像·pyside