20、Qt主窗口与菜单:QMainWindow、QMenuBar、QToolBar、QStatusBar、QDockWidget、QAction

说实话,Qt 里最常用的窗口框架就是 QMainWindow。你想想看,我们平时用的那些桌面软件——记事本、画图、甚至 Qt Creator 自己——它们的窗口结构其实都差不多:上面有菜单栏,下面有状态栏,中间是干活的地方,四周还能停靠各种小面板。这套布局,就是 QMainWindow 给我们搭好的骨架。

我个人习惯,每次新建一个 Qt 项目,第一件事就是把主窗口的骨架搭起来。别急着写业务逻辑,先把菜单、工具栏、状态栏这些基础设施摆好。为什么?因为后面加功能的时候,你只需要往骨架上挂肉就行了,代码结构会清晰很多。

核心要点:QMainWindow 本身就是一个布局容器,它内置了菜单栏区、工具栏区、停靠窗口区、状态栏区和中央部件区。你不需要手动管理这些区域的位置,Qt 的布局系统会自动处理。

20.1 QMainWindow 的整体布局

先看一张我画的布局图,帮你快速建立直观印象:

QMainWindow 布局结构 QMenuBar(菜单栏) QToolBar(工具栏) QDockWidget (左侧) Central Widget(中央部件) QDockWidget (右侧) QStatusBar(状态栏)

看到没?整个窗口被分成了五个区域。中间那个最大的紫色区域是中央部件,你主要的工作界面就放那里。四周的橙色区域是可选的停靠窗口,你可以拖来拖去。最上面是菜单栏和工具栏,最下面是状态栏。

小技巧:中央部件必须设置,否则 QMainWindow 会给你一个占位的空白区域。哪怕你只想显示一个 QLabel,也得把它 setCentralWidget 进去。

20.2 QMenuBar 与 QAction

菜单栏是用户和程序交互的第一道门。我见过不少新手直接把菜单项的文字写死,结果后面要国际化的时候,改得想哭。其实 Qt 的菜单系统设计得很巧妙——它把「动作」和「显示」分开了。

这个「动作」就是 QAction。你可以把 QAction 理解成一个命令的载体:它包含了图标、文字、快捷键、提示信息,甚至还能关联一个槽函数。然后菜单栏和工具栏都可以共用同一个 QAction。

// 创建一个动作
QAction *openAction = new QAction(QIcon(":/icons/open.png"), "打开(&O)", this);
openAction->setShortcut(QKeySequence::Open);
openAction->setStatusTip("打开一个文件");

// 添加到菜单
QMenu *fileMenu = menuBar()->addMenu("文件(&F)");
fileMenu->addAction(openAction);

// 添加到工具栏
QToolBar *fileToolBar = addToolBar("文件");
fileToolBar->addAction(openAction);

// 连接信号槽
connect(openAction, &QAction::triggered, this, &MainWindow::openFile);

这段代码里,openAction 只创建了一次,但既出现在菜单里,又出现在工具栏里。你想想看,如果以后要改快捷键,只需要改一处。这就是 QAction 的价值所在。

注意:菜单文本里的 &O 表示 Alt+O 可以快速激活这个菜单项。这个下划线在 Windows 上会显示出来,但在 macOS 上默认不显示。我曾经因为这个被测试提过 bug,后来才知道 macOS 的菜单行为跟 Windows 不一样。

20.3 QToolBar 工具栏

工具栏说白了就是菜单的快捷方式集合。用户懒得点菜单,直接点工具栏上的图标按钮。但工具栏有个坑——它默认是可以拖动的。用户可以把工具栏拖到窗口的左边、右边,甚至悬浮起来。

我个人建议,如果你的工具栏不希望被拖动,可以设置:

QToolBar *toolbar = addToolBar("编辑");
toolbar->setMovable(false);      // 禁止拖动
toolbar->setFloatable(false);    // 禁止悬浮

不过话说回来,大部分用户其实不介意工具栏能不能拖动。我一般只在工具栏包含关键操作时才会锁定它,比如保存按钮——万一用户不小心把工具栏拖没了,找不到保存按钮,那可就麻烦了。

20.4 QStatusBar 状态栏

状态栏是窗口底部那条窄窄的条,用来显示一些临时信息。比如「文件已保存」、「正在加载...」、「第 3 行,第 12 列」之类的。

Qt 的状态栏有两种用法:

  • 临时消息:调用 showMessage("消息内容", 3000),3 秒后自动消失。
  • 永久部件:addWidget()addPermanentWidget() 添加一个 QLabel 或其他控件,一直显示。
// 临时消息
statusBar()->showMessage("文件加载完成", 2000);

// 永久部件
QLabel *statusLabel = new QLabel("就绪");
statusBar()->addPermanentWidget(statusLabel);

注意 addPermanentWidgetaddWidget 的区别:前者从右往左排,后者从左往右排。我一般把重要的状态信息(比如连接状态、用户信息)放在右边永久区域,把操作反馈放在左边临时区域。

20.5 QDockWidget 停靠窗口

停靠窗口是 Qt 里比较灵活的一个组件。你可以把它想象成一个可以自由移动的小面板。比如 Qt Creator 里的「项目文件列表」、「输出窗口」、「调试变量监视」——这些都是 QDockWidget。

创建停靠窗口的步骤很简单:

QDockWidget *dock = new QDockWidget("文件浏览器", this);
dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);

QTreeWidget *tree = new QTreeWidget;
dock->setWidget(tree);

addDockWidget(Qt::LeftDockWidgetArea, dock);

这里有个细节:setAllowedAreas 限制了停靠窗口可以停靠的位置。如果你不设置,默认四个方向都能停。我曾经做过一个项目,用户把调试窗口拖到了菜单栏上面,结果界面乱成一团。从那以后,我习惯给每个停靠窗口都明确设置允许区域。

经验之谈:QDockWidget 的标题栏默认有一个关闭按钮。如果你不希望用户关掉这个面板,可以调用 dock->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable),把关闭功能去掉。

20.6 综合示例:搭建一个简易文本编辑器

说了这么多,不如直接看一个完整的例子。下面是一个简易文本编辑器的主窗口代码,把上面讲到的组件都用上了:

#include <QMainWindow>
#include <QMenuBar>
#include <QToolBar>
#include <QStatusBar>
#include <QDockWidget>
#include <QAction>
#include <QTextEdit>
#include <QListWidget>

class TextEditor : public QMainWindow {
    Q_OBJECT
public:
    TextEditor(QWidget *parent = nullptr) : QMainWindow(parent) {
        // 1. 中央部件
        QTextEdit *editor = new QTextEdit(this);
        setCentralWidget(editor);

        // 2. 菜单栏
        QMenu *fileMenu = menuBar()->addMenu("文件(&F)");
        
        QAction *newAct = new QAction("新建(&N)", this);
        newAct->setShortcut(QKeySequence::New);
        fileMenu->addAction(newAct);

        QAction *openAct = new QAction("打开(&O)", this);
        openAct->setShortcut(QKeySequence::Open);
        fileMenu->addAction(openAct);

        // 3. 工具栏
        QToolBar *toolbar = addToolBar("文件操作");
        toolbar->addAction(newAct);
        toolbar->addAction(openAct);

        // 4. 状态栏
        statusBar()->showMessage("就绪");

        // 5. 停靠窗口
        QDockWidget *dock = new QDockWidget("文件列表", this);
        dock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);
        
        QListWidget *fileList = new QListWidget;
        fileList->addItem("文档1.txt");
        fileList->addItem("文档2.txt");
        dock->setWidget(fileList);
        
        addDockWidget(Qt::RightDockWidgetArea, dock);
    }
};

这段代码虽然简单,但五脏俱全。你运行一下就能看到:上面有菜单栏和工具栏,中间是编辑区,右边有个文件列表面板,底部显示「就绪」。这就是一个标准的主窗口骨架。

补充说明:实际项目中,菜单和工具栏的动作通常会封装在一个独立的函数里,比如 createActions()createMenus()createToolBars()。这样构造函数就不会太臃肿,代码也更容易维护。

20.7 避坑指南

最后分享几个我踩过的坑:

  • QMainWindow 不能嵌套:千万别在一个 QMainWindow 里再放一个 QMainWindow。如果你需要多文档界面,用 QMdiArea。
  • 工具栏图标大小:不同平台的图标风格不一样。Windows 上 24x24 合适,macOS 上 32x32 更协调。可以用 setIconSize(QSize(24,24)) 统一设置。
  • 状态栏更新频率:不要在循环里频繁调用 showMessage,否则界面会卡顿。如果需要实时更新,用 QTimer 控制频率。
  • 停靠窗口的 restoreState:如果你保存了窗口布局,记得在程序启动时调用 restoreState()。我曾经忘记这步,结果用户每次打开程序,停靠窗口都回到默认位置,被吐槽了好久。

嗯,以上就是 QMainWindow 及其周边组件的核心内容。这些东西看起来零散,但组合起来就是一套完整的主窗口框架。你只要把骨架搭好了,后面加功能就像往抽屉里放东西一样简单。