QT入门第六天:菜单栏工具栏状态栏详解 | 零基础学QT

QT入门第六天:菜单栏工具栏状态栏详解 | 零基础学QT

前言

前五天我们学习了环境搭建、信号与槽、常用控件、布局管理器、对话框。今天我们来学习菜单栏、工具栏、状态栏,这三个是QMainWindow的标配。

你有没有发现,几乎所有的桌面软件都有这些东西:

  • 最上面一排:文件、编辑、帮助... → 这就是菜单栏
  • 菜单栏下面一排图标按钮:打开、保存、撤销... → 这就是工具栏
  • 最下面一排小字:显示行数、就绪... → 这就是状态栏

比如记事本、Word、浏览器、Photoshop,都是这个结构。QT的QMainWindow已经帮我们把这些都准备好了,直接用就行。

今天我们学习:

  • QMenuBar 菜单栏
  • QToolBar 工具栏
  • QStatusBar 状态栏
  • QAction 动作(菜单和工具栏共用的概念)

一、QAction 动作

在讲菜单和工具栏之前,先搞清楚一个重要的概念:QAction(动作)

1.1 什么是QAction

QAction就是一个"动作",它代表了用户可以执行的一个操作,比如"打开文件"、"保存"、"复制"、"粘贴"。

为什么要有QAction这个东西呢?因为同一个操作,可能出现在多个地方:

  • 在菜单栏里有一个"保存"菜单项
  • 在工具栏里有一个"保存"按钮
  • 还有快捷键 Ctrl+S

这三个地方触发的是同一个操作,如果分别写三份代码,那就太麻烦了。

QAction就是来解决这个问题的:创建一个QAction,然后把它同时加到菜单和工具栏里,它们共用同一个triggered信号。

💡 理解:QAction就像一个"命令",菜单和工具栏按钮只是这个命令的不同入口。

1.2 QAction的常用属性

cpp 复制代码
QAction *action = new QAction("保存", this);

// 设置图标
action->setIcon(QIcon(":/icons/save.png"));

// 设置快捷键
action->setShortcut(QKeySequence::Save);  // 标准快捷键 Ctrl+S
// 或者自定义
action->setShortcut(QKeySequence("Ctrl+S"));

// 设置提示文字(鼠标悬停时显示)
action->setStatusTip("保存文件");

// 设置是否可选中(复选框效果)
action->setCheckable(true);

// 设置是否可用
action->setEnabled(true);

1.3 触发信号

cpp 复制代码
// 点击/触发时发出triggered信号
connect(action, &QAction::triggered, this, [](){
    qDebug() << "执行保存操作";
});

二、QMenuBar 菜单栏

2.1 什么是菜单栏

菜单栏就是窗口最上面那一排菜单,比如"文件"、"编辑"、"帮助"。

QMainWindow自带一个菜单栏,用menuBar()就能拿到。

2.2 基本用法

cpp 复制代码
// 获取菜单栏(QMainWindow自带)
QMenuBar *menuBar = this->menuBar();

// 添加菜单
QMenu *fileMenu = menuBar->addMenu("文件(&F)");
QMenu *editMenu = menuBar->addMenu("编辑(&E)");
QMenu *helpMenu = menuBar->addMenu("帮助(&H)");

// 给菜单添加菜单项(QAction)
QAction *openAct = fileMenu->addAction("打开(&O)");
QAction *saveAct = fileMenu->addAction("保存(&S)");
fileMenu->addSeparator();  // 加分隔线
QAction *exitAct = fileMenu->addAction("退出(&X)");

// 连接信号
connect(openAct, &QAction::triggered, this, [](){
    qDebug() << "打开文件";
});
connect(saveAct, &QAction::triggered, this, [](){
    qDebug() << "保存文件";
});
connect(exitAct, &QAction::triggered, this, &QWidget::close);

2.3 快捷键 & 助记符

你可能注意到了,菜单名里有&F&E这样的写法,这叫助记符

按 Alt + F 就能打开"文件"菜单,按 Alt + E 打开"编辑"菜单。

💡 小技巧:&后面的字母就是助记符,按Alt+那个字母就能快速打开菜单。

2.4 子菜单

菜单里面还可以再套菜单,叫子菜单:

cpp 复制代码
QMenu *fileMenu = menuBar->addMenu("文件");

// 新建子菜单
QMenu *newMenu = fileMenu->addMenu("新建");
newMenu->addAction("文本文档");
newMenu->addAction("图片");
newMenu->addAction("文件夹");

fileMenu->addAction("打开");
fileMenu->addAction("保存");

这样"文件"菜单里就有一个"新建"子菜单,鼠标移过去会展开。

2.5 带图标的菜单项

cpp 复制代码
QAction *openAct = new QAction(QIcon(":/icons/open.png"), "打开", this);
fileMenu->addAction(openAct);

2.6 可选中的菜单项

有些菜单项是开关性质的,比如"显示工具栏"、"自动换行":

cpp 复制代码
QAction *wrapAct = editMenu->addAction("自动换行");
wrapAct->setCheckable(true);  // 设为可选中
wrapAct->setChecked(true);    // 默认选中

connect(wrapAct, &QAction::toggled, this, [](bool checked){
    qDebug() << "自动换行:" << checked;
});

注意:可选中的动作,信号用toggled(bool)而不是triggered(),因为它会告诉你当前是选中还是取消。

三、QToolBar 工具栏

3.1 什么是工具栏

工具栏就是菜单栏下面那一排图标按钮,把常用的操作放在这里,用户点一下就能用,不用去菜单里找。

3.2 基本用法

cpp 复制代码
// 创建工具栏
QToolBar *toolBar = addToolBar("工具栏");

// 添加动作(和菜单共用同一个QAction)
toolBar->addAction(openAct);
toolBar->addAction(saveAct);
toolBar->addSeparator();  // 分隔线
toolBar->addAction(copyAct);
toolBar->addAction(pasteAct);

看到了吗?直接addAction把之前创建的QAction加进去就行,菜单和工具栏共用同一个动作,不用再写一遍逻辑!

3.3 工具栏的位置

工具栏默认在顶部,但用户可以拖动它,放到左边、右边、底部,甚至拖出来变成浮动窗口。

cpp 复制代码
// 设置允许停靠的位置
toolBar->setAllowedAreas(Qt::TopToolBarArea | Qt::BottomToolBarArea);

// 设置初始位置
addToolBar(Qt::TopToolBarArea, toolBar);  // 顶部
// addToolBar(Qt::LeftToolBarArea, toolBar);  // 左边
// addToolBar(Qt::RightToolBarArea, toolBar); // 右边
// addToolBar(Qt::BottomToolBarArea, toolBar); // 底部

// 禁止拖动,固定位置
toolBar->setMovable(false);

3.4 图标大小

cpp 复制代码
toolBar->setIconSize(QSize(32, 32));  // 设置图标大小

3.5 文字显示方式

默认只显示图标,也可以设置显示文字,或者图标+文字:

cpp 复制代码
// 只显示图标(默认)
toolBar->setToolButtonStyle(Qt::ToolButtonIconOnly);

// 只显示文字
toolBar->setToolButtonStyle(Qt::ToolButtonTextOnly);

// 文字在图标旁边
toolBar->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);

// 文字在图标下面
toolBar->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);

3.6 往工具栏加其他控件

工具栏不只能加按钮,还能加其他控件,比如下拉框、输入框:

cpp 复制代码
// 加一个标签
toolBar->addWidget(new QLabel("字体:"));

// 加一个下拉框
QComboBox *fontCombo = new QComboBox();
fontCombo->addItems(QStringList() << "宋体" << "微软雅黑" << "Arial");
toolBar->addWidget(fontCombo);

四、QStatusBar 状态栏

4.1 什么是状态栏

状态栏就是窗口最下面那一条,用来显示状态信息,比如"就绪"、"第3行第5列"、"已保存"之类的。

QMainWindow也自带状态栏,用statusBar()就能拿到。

4.2 显示临时消息

cpp 复制代码
// 获取状态栏
QStatusBar *statusBar = this->statusBar();

// 显示消息,3秒后自动消失
statusBar->showMessage("文件已保存", 3000);

// 一直显示,直到被新消息覆盖或清除
statusBar->showMessage("就绪");

// 清除消息
statusBar->clearMessage();

4.3 永久部件

临时消息会变,但有些信息需要一直显示在状态栏上,比如当前时间、光标位置,这些叫"永久部件"。

永久部件显示在状态栏的最右边,不会被临时消息覆盖。

cpp 复制代码
// 添加永久部件
QLabel *posLabel = new QLabel("行: 1  列: 1");
statusBar->addPermanentWidget(posLabel);

// 更新内容
posLabel->setText("行: 10  列: 5");

4.4 普通部件

除了永久部件,还可以加普通部件,显示在左边:

cpp 复制代码
QLabel *statusLabel = new QLabel("就绪");
statusBar->addWidget(statusLabel);

4.5 进度条

状态栏里放进度条也很常见,比如下载进度:

cpp 复制代码
QProgressBar *progressBar = new QProgressBar();
progressBar->setRange(0, 100);
progressBar->setValue(50);
progressBar->setFixedWidth(150);
statusBar->addPermanentWidget(progressBar);

五、综合实战:完善记事本

我们来把第五天的记事本完善一下,加上菜单栏、工具栏、状态栏。

5.1 完整代码

cpp 复制代码
#include "mainwindow.h"
#include <QMenuBar>
#include <QToolBar>
#include <QStatusBar>
#include <QAction>
#include <QTextEdit>
#include <QFileDialog>
#include <QMessageBox>
#include <QFile>
#include <QTextStream>
#include <QLabel>
#include <QComboBox>
#include <QFontComboBox>
#include <QSpinBox>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    setWindowTitle("简易记事本");
    resize(800, 600);

    // 文本编辑区
    m_textEdit = new QTextEdit(this);
    setCentralWidget(m_textEdit);

    // ===== 创建动作 =====
    createActions();

    // ===== 创建菜单栏 =====
    createMenus();

    // ===== 创建工具栏 =====
    createToolBars();

    // ===== 创建状态栏 =====
    createStatusBar();

    // 初始状态
    m_statusLabel->setText("就绪");
    m_posLabel->setText("行: 1  列: 1");

    // 监听光标位置变化
    connect(m_textEdit, &QTextEdit::cursorPositionChanged, 
            this, &MainWindow::updateCursorPos);

    // 监听文本变化,标记未保存
    connect(m_textEdit, &QTextEdit::textChanged, this, [=](){
        if (!windowTitle().endsWith("*")) {
            setWindowTitle(windowTitle() + "*");
        }
    });
}

// 创建所有动作
void MainWindow::createActions()
{
    // 文件操作
    m_newAct = new QAction("新建(&N)", this);
    m_newAct->setShortcut(QKeySequence::New);
    m_newAct->setStatusTip("新建一个文件");
    connect(m_newAct, &QAction::triggered, this, &MainWindow::newFile);

    m_openAct = new QAction("打开(&O)...", this);
    m_openAct->setShortcut(QKeySequence::Open);
    m_openAct->setStatusTip("打开一个文件");
    connect(m_openAct, &QAction::triggered, this, &MainWindow::openFile);

    m_saveAct = new QAction("保存(&S)", this);
    m_saveAct->setShortcut(QKeySequence::Save);
    m_saveAct->setStatusTip("保存文件");
    connect(m_saveAct, &QAction::triggered, this, &MainWindow::saveFile);

    m_saveAsAct = new QAction("另存为(&A)...", this);
    m_saveAsAct->setShortcut(QKeySequence::SaveAs);
    m_saveAsAct->setStatusTip("另存为新文件");
    connect(m_saveAsAct, &QAction::triggered, this, &MainWindow::saveAsFile);

    m_exitAct = new QAction("退出(&X)", this);
    m_exitAct->setShortcut(QKeySequence::Quit);
    m_exitAct->setStatusTip("退出程序");
    connect(m_exitAct, &QAction::triggered, this, &QWidget::close);

    // 编辑操作
    m_undoAct = new QAction("撤销(&U)", this);
    m_undoAct->setShortcut(QKeySequence::Undo);
    m_undoAct->setStatusTip("撤销上一步操作");
    connect(m_undoAct, &QAction::triggered, m_textEdit, &QTextEdit::undo);

    m_redoAct = new QAction("重做(&R)", this);
    m_redoAct->setShortcut(QKeySequence::Redo);
    m_redoAct->setStatusTip("重做刚才撤销的操作");
    connect(m_redoAct, &QAction::triggered, m_textEdit, &QTextEdit::redo);

    m_cutAct = new QAction("剪切(&T)", this);
    m_cutAct->setShortcut(QKeySequence::Cut);
    m_cutAct->setStatusTip("剪切选中的内容");
    connect(m_cutAct, &QAction::triggered, m_textEdit, &QTextEdit::cut);

    m_copyAct = new QAction("复制(&C)", this);
    m_copyAct->setShortcut(QKeySequence::Copy);
    m_copyAct->setStatusTip("复制选中的内容");
    connect(m_copyAct, &QAction::triggered, m_textEdit, &QTextEdit::copy);

    m_pasteAct = new QAction("粘贴(&P)", this);
    m_pasteAct->setShortcut(QKeySequence::Paste);
    m_pasteAct->setStatusTip("粘贴剪贴板内容");
    connect(m_pasteAct, &QAction::triggered, m_textEdit, &QTextEdit::paste);

    m_selectAllAct = new QAction("全选(&A)", this);
    m_selectAllAct->setShortcut(QKeySequence::SelectAll);
    m_selectAllAct->setStatusTip("选中所有内容");
    connect(m_selectAllAct, &QAction::triggered, m_textEdit, &QTextEdit::selectAll);

    // 视图
    m_wrapAct = new QAction("自动换行(&W)", this);
    m_wrapAct->setCheckable(true);
    m_wrapAct->setChecked(true);
    m_wrapAct->setStatusTip("切换自动换行");
    connect(m_wrapAct, &QAction::toggled, this, [=](bool checked){
        m_textEdit->setLineWrapMode(checked ? QTextEdit::WidgetWidth : QTextEdit::NoWrap);
    });

    // 帮助
    m_aboutAct = new QAction("关于(&A)...", this);
    m_aboutAct->setStatusTip("关于本软件");
    connect(m_aboutAct, &QAction::triggered, this, &MainWindow::showAbout);
}

// 创建菜单栏
void MainWindow::createMenus()
{
    QMenuBar *menuBar = this->menuBar();

    // 文件菜单
    QMenu *fileMenu = menuBar->addMenu("文件(&F)");
    fileMenu->addAction(m_newAct);
    fileMenu->addAction(m_openAct);
    fileMenu->addAction(m_saveAct);
    fileMenu->addAction(m_saveAsAct);
    fileMenu->addSeparator();
    fileMenu->addAction(m_exitAct);

    // 编辑菜单
    QMenu *editMenu = menuBar->addMenu("编辑(&E)");
    editMenu->addAction(m_undoAct);
    editMenu->addAction(m_redoAct);
    editMenu->addSeparator();
    editMenu->addAction(m_cutAct);
    editMenu->addAction(m_copyAct);
    editMenu->addAction(m_pasteAct);
    editMenu->addSeparator();
    editMenu->addAction(m_selectAllAct);

    // 视图菜单
    QMenu *viewMenu = menuBar->addMenu("视图(&V)");
    viewMenu->addAction(m_wrapAct);

    // 帮助菜单
    QMenu *helpMenu = menuBar->addMenu("帮助(&H)");
    helpMenu->addAction(m_aboutAct);
}

// 创建工具栏
void MainWindow::createToolBars()
{
    // 文件工具栏
    QToolBar *fileToolBar = addToolBar("文件");
    fileToolBar->addAction(m_newAct);
    fileToolBar->addAction(m_openAct);
    fileToolBar->addAction(m_saveAct);
    fileToolBar->setMovable(false);  // 固定位置

    // 编辑工具栏
    QToolBar *editToolBar = addToolBar("编辑");
    editToolBar->addAction(m_undoAct);
    editToolBar->addAction(m_redoAct);
    editToolBar->addSeparator();
    editToolBar->addAction(m_cutAct);
    editToolBar->addAction(m_copyAct);
    editToolBar->addAction(m_pasteAct);

    // 格式工具栏(加一些控件)
    QToolBar *formatToolBar = addToolBar("格式");
    formatToolBar->addWidget(new QLabel(" 字体:"));
    
    QFontComboBox *fontCombo = new QFontComboBox();
    formatToolBar->addWidget(fontCombo);
    connect(fontCombo, &QFontComboBox::currentFontChanged, 
            m_textEdit, &QTextEdit::setFontFamily);

    formatToolBar->addWidget(new QLabel(" 字号:"));
    
    QSpinBox *sizeSpin = new QSpinBox();
    sizeSpin->setRange(8, 72);
    sizeSpin->setValue(12);
    formatToolBar->addWidget(sizeSpin);
    connect(sizeSpin, QOverload<int>::of(&QSpinBox::valueChanged), 
            m_textEdit, &QTextEdit::setFontPointSize);
}

// 创建状态栏
void MainWindow::createStatusBar()
{
    QStatusBar *statusBar = this->statusBar();

    // 左边:状态提示
    m_statusLabel = new QLabel();
    statusBar->addWidget(m_statusLabel);

    // 右边:永久显示光标位置
    m_posLabel = new QLabel();
    statusBar->addPermanentWidget(m_posLabel);
}

// 更新光标位置
void MainWindow::updateCursorPos()
{
    QTextCursor cursor = m_textEdit->textCursor();
    int line = cursor.blockNumber() + 1;
    int col = cursor.columnNumber() + 1;
    m_posLabel->setText(QString("行: %1  列: %2").arg(line).arg(col));
}

// 新建文件
void MainWindow::newFile()
{
    m_textEdit->clear();
    m_currentFile.clear();
    setWindowTitle("未命名 - 简易记事本");
    m_statusLabel->setText("新建文件");
}

// 打开文件
void MainWindow::openFile()
{
    QString fileName = QFileDialog::getOpenFileName(
        this, "打开文件", "", "文本文件 (*.txt);;所有文件 (*.*)"
    );
    if (fileName.isEmpty()) return;

    QFile file(fileName);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        QMessageBox::critical(this, "错误", "无法打开文件!");
        return;
    }

    QTextStream in(&file);
    m_textEdit->setText(in.readAll());
    file.close();

    m_currentFile = fileName;
    setWindowTitle(fileName + " - 简易记事本");
    m_statusLabel->setText("已打开:" + fileName);
}

// 保存文件
void MainWindow::saveFile()
{
    if (m_currentFile.isEmpty()) {
        saveAsFile();
        return;
    }

    QFile file(m_currentFile);
    if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
        QMessageBox::critical(this, "错误", "无法保存文件!");
        return;
    }

    QTextStream out(&file);
    out << m_textEdit->toPlainText();
    file.close();

    // 去掉标题末尾的*
    QString title = windowTitle();
    if (title.endsWith("*")) {
        title.chop(1);
        setWindowTitle(title);
    }

    m_statusLabel->setText("已保存");
}

// 另存为
void MainWindow::saveAsFile()
{
    QString fileName = QFileDialog::getSaveFileName(
        this, "另存为", "未命名.txt", "文本文件 (*.txt)"
    );
    if (fileName.isEmpty()) return;

    m_currentFile = fileName;
    saveFile();
}

// 关于对话框
void MainWindow::showAbout()
{
    QMessageBox::about(
        this,
        "关于简易记事本",
        "简易记事本 v2.0\n\n"
        "一个用QT写的简单记事本程序\n"
        "支持菜单栏、工具栏、状态栏\n\n"
        "作者:零基础学QT"
    );
}

5.2 头文件

别忘了在头文件里声明这些成员变量和函数:

cpp 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>

class QTextEdit;
class QAction;
class QLabel;

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);

private slots:
    void newFile();
    void openFile();
    void saveFile();
    void saveAsFile();
    void showAbout();
    void updateCursorPos();

private:
    void createActions();
    void createMenus();
    void createToolBars();
    void createStatusBar();

    QTextEdit *m_textEdit;
    QString m_currentFile;
    QLabel *m_statusLabel;
    QLabel *m_posLabel;

    // 动作
    QAction *m_newAct;
    QAction *m_openAct;
    QAction *m_saveAct;
    QAction *m_saveAsAct;
    QAction *m_exitAct;
    QAction *m_undoAct;
    QAction *m_redoAct;
    QAction *m_cutAct;
    QAction *m_copyAct;
    QAction *m_pasteAct;
    QAction *m_selectAllAct;
    QAction *m_wrapAct;
    QAction *m_aboutAct;
};

#endif

5.3 运行效果

Ctrl + R 运行,你会看到一个功能完整的记事本:

  • ✅ 顶部有菜单栏(文件、编辑、视图、帮助)
  • ✅ 下面有三排工具栏(文件、编辑、格式)
  • ✅ 底部有状态栏(左边显示状态,右边显示光标位置)
  • ✅ 菜单和工具栏共用动作,点哪个都一样
  • ✅ 有快捷键(Ctrl+N新建、Ctrl+O打开、Ctrl+S保存...)
  • ✅ 鼠标悬停在按钮上,状态栏有提示
  • ✅ 修改内容后标题加*号标记未保存

是不是有点专业软件的感觉了?

六、今日总结

今天我们学习了QMainWindow的三剑客:菜单栏、工具栏、状态栏,还有QAction这个核心概念。

知识点汇总

组件 作用 常用方法
QAction 动作(菜单和工具栏共用) setIcon, setShortcut, setStatusTip, triggered/toggled
QMenuBar 菜单栏 addMenu, addAction, addSeparator
QMenu 菜单 addAction, addMenu, addSeparator
QToolBar 工具栏 addAction, addWidget, setMovable, setIconSize
QStatusBar 状态栏 showMessage, addWidget, addPermanentWidget, clearMessage

重要概念

  • QAction:一个动作可以同时加到菜单和工具栏,共用同一个信号
  • 助记符 :菜单名里的&F,按Alt+F快速打开
  • 快捷键setShortcut()设置,QKeySequence有很多标准快捷键
  • 永久部件:状态栏右边,不会被临时消息覆盖
  • 可选中动作setCheckable(true),用toggled(bool)信号

经验分享

  1. 先创建动作,再建菜单和工具栏:统一管理,代码更清晰
  2. 善用标准快捷键:QKeySequence::Save、QKeySequence::Open这些,不用自己记
  3. 状态栏提示很重要:每个动作都加setStatusTip,用户体验更好
  4. 工具栏不要太挤:常用的放工具栏,不常用的放菜单里就行
  5. 分组清晰:相关的动作放一起,用分隔线分开

七、明日预告

明天我们将学习QT的事件处理机制

什么是事件?比如:

  • 鼠标点击、移动、滚轮 → 鼠标事件
  • 键盘按下、松开 → 键盘事件
  • 窗口大小改变 → 尺寸事件
  • 窗口显示、隐藏 → 显示事件
  • 定时器到时间了 → 定时器事件

QT是基于事件驱动的,理解事件机制非常重要。我们会学习:

  • 什么是事件
  • 怎么重写事件处理函数
  • 事件的传递和忽略
  • 事件过滤器
  • 定时器事件

学会事件处理,你就能更灵活地控制程序的行为了!


📝 学习建议:菜单栏、工具栏、状态栏是做桌面软件的标配,一定要掌握。

练习建议:

  • 把今天的记事本代码敲一遍
  • 试试加一个"查找"功能到菜单和工具栏
  • 试试在状态栏加一个字数统计
  • 试试加一个"字体颜色"按钮到工具栏

掌握了这三剑客,做出来的软件就有模有样了!明天见,继续加油!💪