Qt入门06 Qt中的基础窗口类——大丙+Gemini

Qt入门06 Qt中的基础窗口类——大丙+Gemini

https://www.bilibili.com/video/BV1Jp4y167R9

主要内容包括: 窗口类的基类QWidget, 对话框基类QDialog, 带菜单栏工具栏状态栏的QMainWindow, 消息对话框QMessageBox, 文件对话框QFileDialog, 字体对话框QFontDialog, 颜色对话框QColorDialog, 输入型对话框QInputDialog, 进度条对话框QProgressDialog, 资源文件。

1. QWidget

QWidget类是所有窗口类的父类(控件类是也属于窗口类), 并且QWidget类的父类的QObject, 也就意味着所有的窗口类对象只要指定了父对象, 都可以实现内存资源的自动回收。

![image-20260525151759698](Qt入门06 Qt中的基础窗口类.assets/image-20260525151759698.png)

1.1 QWidget常用API

这些API在所有窗口类内中都有,所有窗口类都是QWidget类的子类,所以所有窗口类都继承了QWidget的全部API

1.1.1 设置父对象

指定父对象不仅决定了窗口的层级显示关系,更是 Qt 内存自动管理的关键。若未指定父对象,parentWidget() 将返回 nullptr,此时该窗口通常作为顶层窗口显示。

// 构造函数
QWidget::QWidget(QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());

// 公共成员函数
// 给当前窗口重新设置父对象
void QWidget::setParent(QWidget *parent);
void QWidget::setParent(QWidget *parent, Qt::WindowFlags f);
// 获取当前窗口的父对象, 没有父对象返回 nullptr
QWidget *QWidget::parentWidget() const;

示例代码:

// 在构造函数的初始化列表中,将传入的 parent 指针传递给父类 QMainWindow
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    // setupUi 内部会自动将各种控件的父对象设置为 this (当前窗口)
    ui->setupUi(this);
}

1.1.2 窗口几何信息

  • frameGeometry():包含系统边框和标题栏(通常用于获取整个窗口在屏幕上的实际物理占位)。
  • geometry():仅指客户区(不含边框,通常用于内部控件的排版)。
//------------- 窗口几何信息 -------------
// 得到相对于当前窗口父窗口的几何信息, 边框也被计算在内
QRect QWidget::frameGeometry() const;
// 得到相对于当前窗口父窗口的几何信息, 不包括边框
const QRect &geometry() const;
// 设置当前窗口的几何信息(位置和尺寸信息), 不包括边框
void setGeometry(int x, int y, int w, int h);
void setGeometry(const QRect &);
    
// 移动窗口, 重新设置窗口的位置
void move(int x, int y);
void move(const QPoint &);

示例代码:

void MainWindow::on_moveBtn_clicked()
{
    // 获取当前包含边框的几何信息,确保移动时整体坐标准确
    QRect rect = this->frameGeometry();
    // 在当前坐标(左上点坐标)基础上位移
    this->move(rect.topLeft() + QPoint(10, 20));
}

1.1.3 窗口尺寸

窗口尺寸控制主要分为:当前尺寸获取、最大/最小边界限制,以及固定尺寸锁定。

💡 核心答疑:尺寸设置的陷阱与底层逻辑

  • 尺寸是否包含标题栏?setFixedSize()resize() 以及获取宽高的 width()/height() 函数,操作的全部是客户区,绝不包含系统的标题栏和边框。
  • setGeometry 会突破 maximumSize 吗? 绝对不会。如果你预先设置了最大尺寸,即使后续使用 setGeometry 传入了一个极大的随机宽高,Qt 底层也会自动将其拦截并强行截断在设定的最大尺寸以内,保证窗口不会越界膨胀。
  • setFixedSize 到底做了什么? 它在底层等同于把最大尺寸和最小尺寸锁死成了同一个值。开启后,用户无法拖拽改变大小,且后续调用 setGeometry 尝试改变宽高的操作会失效(但改变 x, y 坐标的位移依然生效)。
//------------- 窗口尺寸 -------------
// 获取当前窗口的尺寸信息
QSize size() const
// 重新设置窗口的尺寸信息
void resize(int w, int h);
void resize(const QSize &);

// 获取与设置极值尺寸
QSize maximumSize() const;
QSize minimumSize() const;
void setMaximumSize(const QSize &);
void setMaximumSize(int maxw, int maxh);
void setMinimumSize(const QSize &);
void setMinimumSize(int minw, int minh);

// 设置固定尺寸
void QWidget::setFixedSize(const QSize &s);
void QWidget::setFixedSize(int w, int h);

// 单独获取与设置宽/高 (包含各自的极值限制函数)
int height() const;
int minimumHeight() const;
int maximumHeight() const;
void QWidget::setFixedHeight(int h);
void setMaximumHeight(int maxh);
void setMinimumHeight(int minh);

int width() const;
int minimumWidth() const;
int maximumWidth() const;
void QWidget::setFixedWidth(int w);
void setMaximumWidth(int maxw);
void setMinimumWidth(int minw);

示例代码:

// 1. 初始化时的尺寸设置
this->setMaximumSize(1600, 1000); 
this->setMinimumSize(100, 100); 
// this->setFixedSize(800, 500); // 提示:一旦开启此项,后续宽高修改将失效

// 2. 测试随机改变几何信息
void MainWindow::on_geometryBtn_clicked()
{
    int x = 100 + rand() % 500;
    int y = 100 + rand() % 500;
    
    // 即使这里加了 1000,底层的 maximumSize(1600, 1000) 也会自动执行拦截防御
    int width = this->width() + rand() % 1000;
    int height = this->height() + rand() % 1000;

    this->setGeometry(x, y, width, height);
}

1.1.4 窗口标题和图标

提供了获取和重新设置窗口左上角的图标和标题文本的功能。图标通常需要依靠 QIcon 对象配合文件路径来实例化。

//------------- 窗口图标 -------------
// 得到当前窗口的图标
QIcon windowIcon() const;
// 构造图标对象, 参数为图片的路径
QIcon::QIcon(const QString &fileName);
// 设置当前窗口的图标
void setWindowIcon(const QIcon &icon);

//------------- 窗口标题 -------------
// 得到当前窗口的标题
QString windowTitle() const;
// 设置当前窗口的标题
void setWindowTitle(const QString &);

示例代码:

void MainWindow::on_modifyBtn_clicked()
{
    this->setWindowTitle("How are you?");
    this->setWindowIcon(QIcon("C:/Users/ROG/Pictures/002.jpg"));
}

1.1.5 信号

Qt 在处理状态改变时的信号发射机制非常严谨,为了避免不必要的重绘,底层做了性能优化。

💡 核心答疑:信号触发机制的差异与右键菜单

  • 为什么重复设置标题,信号只触发一次? 当执行 setWindowTitle 时,Qt 底层会进行字符串比对优化。如果检测到新标题与当前标题内容一模一样,会直接 return 丢弃操作,从而拦截windowTitleChanged 信号的重复发射。
  • 为什么重复设置图标,信号每次都触发? 执行 QIcon("...002.jpg") 实质上是在内存中新建了一个对象。尽管图片本身没变,但传入的是全新的对象实例,Qt 认定状态已被刷新,因此每次都会触发 windowIconChanged 信号。
  • menu.addAction("文本") 返回的是什么? QMenu 是容器,addAction 在底层会自动帮你 new 一个带有该文本的 QAction(菜单项动作)对象,并返回它的指针,以便后续绑定信号槽。
// 窗口图标发生变化, 发射此信号
[signal] void QWidget::windowIconChanged(const QIcon &icon);
// 窗口标题发生变化, 发射此信号
[signal] void QWidget::windowTitleChanged(const QString &title);

// 窗口的右键菜单策略 contextMenuPolicy() 参数设置为 Qt::CustomContextMenu 时启用
// 核心注意:信号携带的 pos 参数是窗口内的相对坐标,不是全局屏幕坐标
[signal] void QWidget::customContextMenuRequested(const QPoint &pos);

示例代码:

// 监听标题和图标修改的信号
connect(this, &QMainWindow::windowTitleChanged, this, [=](const QString& title) {
    qDebug() << "new title: " << title;
});
connect(this, &QMainWindow::windowIconChanged, this, [=](const QIcon& icon) {
    qDebug() << "The image of this window has been changed!";
});

// 设置右键菜单策略并响应信号
this->setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, &MainWindow::customContextMenuRequested, this, []() {
    QMenu menu;
    // addAction 内部创建并返回了 QAction 指针对象
    menu.addAction("nameless");
    menu.addAction("Reimu");
    
    // exec() 需要的是屏幕全局坐标,必须通过 QCursor::pos() 获取
    menu.exec(QCursor::pos());
});

1.1.6 槽函数

这是 QWidget 预置的槽函数,主要用于控制窗口的显示层级(隐藏、显示、关闭、最小化、全屏等)以及交互状态(启用/禁用)。

  • 窗口状态说明: 被禁用的窗口(setEnabled(false))外观会变灰,且系统会阻断其接收用户的键盘和鼠标事件。
//------------- 窗口显示 -------------
[slot] bool QWidget::close();         // 关闭当前窗口
[slot] void QWidget::hide();          // 隐藏当前窗口
[slot] void QWidget::show();          // 显示当前创建以及其子窗口
[slot] void QWidget::showFullScreen();// 全屏显示当前窗口 (Windows有效)
[slot] void QWidget::showMaximized(); // 窗口最大化显示 (Windows有效)
[slot] void QWidget::showMinimized(); // 窗口最小化显示 (Windows有效)
[slot] void QWidget::showNormal();    // 将窗口恢复为最大化/最小化之前的状态

//------------- 窗口状态 -------------
bool QWidget::isEnabled() const;      // 判断窗口是否可用 (非槽函数)

[slot] void QWidget::setEnabled(bool);// 设置可用状态 (true->可用, false->不可用)
[slot] void QWidget::setDisabled(bool disable); // 与 setEnabled 逻辑相反
[slot] virtual void QWidget::setVisible(bool visible); // 设置窗口是否可见

示例代码:

// 在程序的入口 main 函数中,调用 QWidget 自带的 show() 槽函数来渲染界面
int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    MainWindow w;
    w.show(); 
    return a.exec();
}

1.2 补充:UI “转到槽”底层机制与信号槽自动连接

1.2.1 核心驱动:QMetaObject::connectSlotsByName

在 Qt Designer 中通过右键“转到槽”生成的槽函数,无需手动编写 connect 语句即可响应事件。此特性的核心触发点位于 UI 编译器 (uic) 自动生成的 ui_xxx.h 头文件中。

ui->setupUi(this) 函数执行的末尾,隐式调用了以下关键代码:

QMetaObject::connectSlotsByName(MainWindow);

该静态函数是整个自动连接机制的引擎,它依赖 Qt 的元对象系统 (Meta-Object System),在程序运行时动态完成信号与槽的绑定。

1.2.2 隐式连接的命名约定 (Naming Convention)

connectSlotsByName 能够成功建立连接的唯一依据是槽函数的字符串命名规范。系统通过词法解析来推断开发者的连接意图。

标准格式要求:on_<objectName>_<signalName>()

  • on_:标识此函数为自动连接槽函数的固定前缀。
  • <objectName>:UI 设计器中分配给控件的唯一对象名称(如 btnLogin)。
  • <signalName>:控件类中声明的具体信号名称(如 clicked)。

1.2.3 信号的来源与元对象编译器 (MOC) 映射

在 UI 自动生成代码中未显式看到信号函数,涉及 Qt 底层的类的封装与编译机制:

  1. 信号的继承与预定义:标准 UI 控件的信号(如 clicked(), pressed())均在其基类(如 QAbstractButton)中通过 signals: 关键字预先声明。
  2. MOC 预编译处理:信号本质上是仅有声明而无开发者自定义实现的 C++ 成员函数。只要类声明中包含 Q_OBJECT 宏,Qt 的 MOC 工具会在 C++ 真正编译前,自动生成信号的底层实现代码。
  3. 元数据表记录:MOC 同时会将该类所有的信号和槽的字符串名称、参数类型及其对应的内存索引(Index),构建成一张完整的元数据映射表(通常输出在 moc_xxx.cpp 文件中)。

1.2.4 自动连接的完整执行流程

当程序执行到 connectSlotsByName 时,底层系统按以下逻辑逐级执行:

  1. 遍历反射:通过元对象系统,遍历当前窗口类(如 MainWindow)元数据表中的所有槽函数名称。
  2. 字符串拆解:当扫描到以 on_ 开头的槽函数时,按标准格式解析出期望的目标控件名 <objectName> 和信号名 <signalName>
  3. 对象树检索:在当前窗口的对象树(Object Tree)中递归查找,依据 <objectName> 获取对应的实际控件实例指针(逻辑类似于 this->findChild<QObject*>("objectName"))。
  4. 验证寻址:查阅该控件实例的元对象表,验证其是否存在名为 <signalName> 的信号,并获取该信号的索引编号。
  5. 底层绑定:条件全部满足后,底层直接调用基于元对象索引的 connect 重载版本,将发送者(控件指针)、信号索引、接收者(当前窗口指针)与槽函数索引硬性绑定。

1.2.5工程实践优劣势及规范考量

在大型或正规的 Qt 工程开发中,对于“转到槽”机制的评估如下:

  • 核心劣势(脆性依赖):自动连接过度依赖字符串匹配。若在 UI 设计器中修改了控件的 objectName,C++ 源码中的槽函数名不会自动同步变更。这将导致 connectSlotsByName 在运行时直接略过该函数,且在编译期不会产生任何报错警告(静默失效),增加排查成本。
  • 逻辑隐蔽性:大量使用自动连接会导致代码中缺乏集中的事件路由管理。阅读代码时,无法在构造函数或初始化模块中直观地看到系统整体的信号流向。
  • 业界推荐规范:现代 C++ 客户端架构中,普遍倾向于弃用依赖字符串机制的隐式连接,转而要求开发者在代码中显式编写 Qt 5+ 风格的函数指针 connect。显式连接能够在编译阶段进行严格的类型安全检查和有效性校验,大幅提升代码的健壮性与可维护性。

2. QDialog

QDialog 是 Qt 框架中用于实现对话框的基类,它继承自 QWidget。对话框通常用于执行短期任务、提示关键信息或与用户进行简短的交互(如登录界面、设置面板、文件选择等)。

在使用 QDialog 时,首先需要理解其两种显示机制:

  1. 模态对话框 (Modal Dialog):对话框打开时会阻塞同一应用程序中其他窗口的输入。用户必须先处理并关闭该对话框,才能继续操作其他窗口。
  2. 非模态对话框 (Modeless Dialog):对话框打开后,不影响用户与其他窗口进行交互。

2.1 常用API

2.1.1 QDialog构造函数

explicit QDialog::QDialog(QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());
  • explicit:防止隐式类型转换。
  • parent:父窗口指针。若指定了父窗口,对话框在显示时通常会居中于父窗口上方,且在父窗口被销毁时,该对话框也会自动释放。
  • f:窗口状态标志,可用于定制窗口外观(例如:去掉标题栏的问号按钮、设置窗口置顶等)。

2.1.2 模态状态控制函数(公共槽函数 / 虚函数)

这些函数用于控制对话框的退出方式,并通过返回值向调用者传递用户的操作结果。它们都是虚函数(virtual),允许派生类重写其行为。

  • virtual int QDialog::exec();

    • 作用:以模态方式显示对话框。调用后会进入局部的事件循环,阻塞当前线程后续代码的执行,直到对话框被隐藏(如调用了 done()accept()reject())。
    • 返回值:返回一个整数(通常为 10,即枚举值 QDialog::Accepted == 1QDialog::Rejected == 0),代表用户的最终选择。
  • virtual void QDialog::done(int r);

    • 作用: 这是控制对话框退出的核心底层函数。 它会隐藏对话框(调用 hide()),结束 exec() 的局部事件循环,并将 exec() 的返回值直接设置为你传入的整数 r
    • 注意: 它会自动发射 finished(r) 信号。如果传入的 rQDialog::AcceptedQDialog::Rejected,底层还会额外发射对应的 accepted()rejected() 信号。
  • virtual void QDialog::accept();

    • 原理:便捷封装函数。底层直接调用 done(QDialog::Accepted)
    • 作用:隐藏模态窗口,解除 exec() 的阻塞,并将 exec() 的返回值设置为 QDialog::Accepted(整数 1)。通常绑定到“确定”或“保存”按钮。
    • 发射信号:accepted()finished(1)
  • virtual void QDialog::reject();

    • 原理:便捷封装函数。底层直接调用 done(QDialog::Rejected)
    • 作用:隐藏模态窗口,解除 exec() 的阻塞,并将 exec() 的返回值设置为 QDialog::Rejected(整数 0)。通常绑定到“取消”或“关闭”按钮。
    • 发射信号:rejected()finished(1)

⚠️ 核心注意:

调用 accept(), reject()done() 本质上是调用了 hide() 使窗口不可见,它们绝对不会触发 closeEvent(),也不会自动释放对话框的内存(除非设置了 setAttribute(Qt::WA_DeleteOnClose) 属性)。

2.1.3 关联信号 (Signals)

当对话框改变状态并退出时,Qt 底层会自动发射以下相应的信号(即自动调用以下信号函数),便于主窗口或其他组件进行监听,无需手动 emit

  • void QDialog::finished(int result);

    • 触发条件: 无论对话框以何种方式通过 done() 机制关闭,均会发射此信号。
    • 会传递参数:参数 result 为最终的返回状态码(即传入 done() 的参数)。
  • void QDialog::accepted();

    • 触发条件: 当调用 accept()done(QDialog::Accepted) 后自动发射。表示用户确认了操作。
    • 无参数
  • void QDialog::rejected();

    • 触发条件: 当调用 reject()done(QDialog::Rejected) 后自动发射。表示用户取消了操作。
    • 无参数

补充:模态状态控制函数 调用 关联信号 的情况

调用的控制函数 底层实际等价操作 必然发射的信号 附带发射的信号 (触发条件) exec() 解除阻塞后的返回值
accept() done(QDialog::Accepted) finished(1) accepted() QDialog::Accepted (整数 1)
reject() done(QDialog::Rejected) finished(0) rejected() QDialog::Rejected (整数 0)
done(r) 隐藏窗口,结束事件循环 finished(r) r=1accepted() r=0rejected() 传入的自定义整数 r

2.2 示例代码:

场景描述: 主窗口(MainWindow)有一个按钮 modalDlgBtn,点击后模态弹出一个自定义对话框(MyDialog)。 MyDialog 包含三个按钮:acceptBtnrejectBtndoneBtn。点击任意按钮都会关闭对话框,随后主窗口打印出 exec() 的返回值以及触发了哪个信号。

1. 对话框实现 (MyDialog.cpp):

#include "mydialog.h"
#include "ui_mydialog.h"

MyDialog::MyDialog(QWidget *parent) :
    QDialog(parent),
    ui(new Ui::MyDialog)
{
    ui->setupUi(this);
        
    // 绑定内部按钮,点击后调用不同的函数隐藏窗口并设置状态码
    // 点击acceptBtn
    connect(ui->acceptBtn, &QPushButton::clicked, this, &MyDialog::accept);
    // 点击rejectBtn
    connect(ui->rejectBtn, &QPushButton::clicked, this, &MyDialog::reject);
    // 点击doneBtn
    connect(ui->doneBtn, &QPushButton::clicked, this, [this]()
    {
        this->done(10);
    });
}

MyDialog::~MyDialog()
{
    delete ui;
}

2. 主窗口实现 (mainwindow.cpp):

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "mydialog.h"
#include <QDebug>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    connect(ui->modalDlgBtn, &QPushButton::clicked, this, [=]() // [=] 隐式捕获了 this
    {
        // 1. 在栈上实例化对话框(无需手动 delete)
        MyDialog dlg;

        // 2. 绑定信号槽,且必须在 exec() 执行前绑定!
        // 如果放在 exec() 之后,代码会被阻塞,等窗口关闭、信号发射完了才会执行到 connect,将无法接收任何信号。
        // 2.1 监听 finished 信号(任何方式关闭都会触发)
        connect(&dlg, &MyDialog::finished, this, [](int ret)
        {
            qDebug() << "Signal triggered: finished with result:" << ret;
        });
        // 2.2 监听 accepted 信号(仅 accept 触发)
        connect(&dlg, &MyDialog::accepted, this, []()
        {
            qDebug() << "Signal triggered: accepted (该信号无参数)";
        });
        // 2.3 监听 rejected 信号(仅 reject 触发)
        connect(&dlg, &MyDialog::rejected, this, []()
        {
            qDebug() << "Signal triggered: rejected (该信号无参数)";
        });
		// 3. 模态显示对话框,代码在此处阻塞,直到对话框关闭
        int execReturn = dlg.exec();

		// 4. 对话框关闭,阻塞解除,继续向下执行
        qDebug() << "exec() return value:" << execReturn << "(与 finished 信号接收到的参数一致)";
    });
}

MainWindow::~MainWindow()
{
    delete ui;
}

3. QDialog的子类

3.1 QMessageBox

QMessageBoxQDialog 的直接子类,专门用于在应用程序中弹出一个模态的标准化提示框,向用户展示警告、错误、简短提示,或者引导用户进行简单的分支选择(如“是/否”、“保存/取消”)。

在日常工程开发中,我们极少去 new 一个 QMessageBox 对象,而是绝大多数情况直接调用其封装好的静态方法(Static Methods),这能大幅提升开发效率。

补充:关于消息框的图标区分

  1. 窗口图标 (Window Icon)

    • 位置: 在系统的标题栏上(紧挨着标题文字的左边)。

    • 层级: 属于系统边框层(非客户区)。是由 Windows/macOS 操作系统的窗口管理器来渲染的。

    • 相关 API: setWindowIcon(QIcon("图片地址"))

      • 在函数中调用系统接口修改窗口图标,Qt自身是用户态程序无法直接修改。
  2. 提示图标 / 消息图标 (Message Icon / Icon Pixmap)

    • 位置: 在对话框正文里面(紧挨着提示文字的左边)。
    • 层级: 属于窗口的客户区(Client Area)。这是 Qt 框架自己用代码画出来的一个控件(本质上是一个专门用来显示图片的 QLabel)。
    • 相关 API: setIcon(QMessageBox::Information)setIconPixmap(QPixmap("..."))

3.1.1 QMessageBox常用API(静态函数群)

Qt 提供了五种最常用的静态信息框,它们在内部均会自动实例化对象并开启局部事件循环(等同于隐式调用了 exec()),从而阻塞当前线程。随着函数作用域结束,栈上的对象会被自动释放,开发者无需担心内存泄漏,也无需手动 delete

通用参数模型:

  • parent: 父窗口指针(通常传 this,确保对话框居中于主界面且共享生命周期)。
  • title: 弹窗左上角的标题文本。
  • text: 弹窗正文的核心提示文本。
  • buttons: (部分函数才有)需要渲染的按钮组合,使用 | 拼接(按位或)。
  • defaultButton: (部分函数才有)默认高亮的按钮,用户直接按 Enter 键触发。
1. 关于对话框 (about)
// about对话框 (无标准图标,通常用于展示版本、版权信息,无返回值)
[static] void QMessageBox::about(QWidget *parent, const QString &title, const QString &text);
  • 视觉特征: 底部只有系统强制提供的一个“OK”按钮。

    • 不会显示系统级别的标准状态图标(如警告、错误);
    • 不返回任何用户的操作结果(返回值为 void)。
  • 图标联动机制: about 窗口图标和消息图标显示完全依赖于父窗口。

    • 如果 parent 窗口设置了 WindowIcon,该对话框会自动提取并克隆,此时它的窗口图标和消息图标都会变成父对象的窗口图标;
    • 如果传了 nullptr 或父对象无图标,则该对话框没有任何图标。
  • 工程应用场景: 用于展示纯静态信息。如软件的“关于我们”、版本号、版权声明或免责条款。

示例代码:

// 执行后当前函数代码被阻塞,直到用户点击 OK 关闭窗口后继续运行
QMessageBox::about(this, "关于系统", "XXX信息管理系统 v2.0 \n开发者:nameless");
2. 提示信息对话框 (Information)

用于向用户传达正常的、非破坏性的操作结果。

[static] QMessageBox::StandardButton QMessageBox::information(
           QWidget *parent, const QString &title, const QString &text, 
           QMessageBox::StandardButtons buttons = Ok,
           QMessageBox::StandardButton defaultButton = NoButton);
  • 视觉特征: 默认带有一个蓝色的 ℹ️ (Info) 消息图标,语气平和。窗口图标跟随父对象。

  • 工程应用场景: 业务流程顺利结束后的反馈。例如“下载完成”、“文件导出成功”、“账号注册成功”。

示例代码:

// 大多数情况下,直接使用默认的 OK 按钮即可,不需要传后两个参数
QMessageBox::information(this, "操作成功", "您的个人文件XXX已成功到上传!");
3. 警告提示对话框 (Warning)

用于告知用户当前情况有些异常,或者接下来的操作存在一定风险。

[static] QMessageBox::StandardButton QMessageBox::warning(
           QWidget *parent, const QString &title, const QString &text, 
           QMessageBox::StandardButtons buttons = Ok,
           QMessageBox::StandardButton defaultButton = NoButton);
  • 视觉特征: 默认带有一个黄色的 ⚠️ (Warning) 三角感叹号消息图标。窗口图标跟随父对象。

  • 工程应用场景: 非致命错误或风险提示。例如“磁盘空间不足”、“您有未保存的修改,直接退出将丢失数据”、“网络连接不稳定”。

示例代码:

// 结合多按钮让用户确认风险,并接收返回值
int ret = QMessageBox::warning(this, "警告", "当前网络不稳定,继续上传可能损坏数据。是否继续?", 
                               QMessageBox::Yes | QMessageBox::No, 
                               QMessageBox::No); // 默认焦点在 No 上,防止误触回车

switch(ret)
{
    case QMessageBox::Yes:
        qDebug() << "用户点击yes";
        break;
    case QMessageBox::No:
        qDebug() << "用户点击no";
        break;
    default:
        break;
}
4. 严重错误对话框 (Critical)

用于通报系统级、阻断性的严重错误。

[static] QMessageBox::StandardButton QMessageBox::critical(
           QWidget *parent, const QString &title, const QString &text, 
           QMessageBox::StandardButtons buttons = Ok,
           QMessageBox::StandardButton defaultButton = NoButton);
  • 视觉特征: 默认带有一个红色的 ❌ (Critical/Error) 停止消息图标。视觉冲击力最强。窗口图标跟随父对象。

  • 工程应用场景: 程序无法继续执行的致命崩溃。例如“数据库连接失败”、“找不到核心配置文件”、“内存分配失败”。

示例代码:

QMessageBox::critical(this, "致命错误", "缺失核心组件 XXX,程序即将强制退出!");
// 弹出此框后,通常代码逻辑会紧接着执行退出程序的操作
5. 提问/抉择对话框 (Question)

专门用于与用户进行交互式提问,它的核心价值在于其返回值,用来决定程序下一步的代码走向。

[static] QMessageBox::StandardButton QMessageBox::question(
            QWidget *parent, const QString &title, const QString &text, 
            QMessageBox::StandardButtons buttons = StandardButtons(Yes | No), 
            QMessageBox::StandardButton defaultButton = NoButton);

视觉特征: 默认带有一个蓝色的 ? (Question) 问号消息图标。窗口图标跟随父对象。默认按钮是 YesNo

工程应用场景: 需要用户做二选一/三选一的抉择。例如“是否确认删除该记录?”、“是否覆盖原文件?”。必须接收返回值用来决定程序下一步的代码走向。

示例代码:

// 必须接收返回值来进行条件分支
int userChoice = QMessageBox::question(this, "删除确认", "是否确认删除该员工信息?",
                                       QMessageBox::Yes | QMessageBox::No,
                                       QMessageBox::No); // 安全起见,默认聚焦在 No 上
if(userChoice == QMessageBox::Yes) {
    qDebug() << "user 选择了 Yes, 执行数据库删除逻辑...";
} else {
    qDebug() << "user 选择了 No, 取消操作...";
}
补充:如果直接单击叉(X)关闭窗口

直接点击窗口右上角的叉(X),或者在键盘上按下 Esc 键,代表着用户想要中断、逃离或放弃当前的操作。

  • 语义: 它代表一种反向操作(在 Qt 底层被称为 RejectRole 拒绝角色)。
  • Qt 是怎么做的: Qt 会在你提供的按钮组合中,自动去寻找一个具有“取消”或“否定”语义的按钮。
    • 如果你的按钮里有 QMessageBox::Cancel,点击叉就会返回 Cancel
    • 如果有 QMessageBox::No,点击叉就会返回 No
    • 如果没有任何带有取消语义的按钮,强制关闭可能返回特定的拒绝状态码,开发者必须在 switchif-else 的最后加上 defaultelse 来做安全兜底。

3.1.2 按钮组合机制与底层类型安全

在上述 API 中,控制按钮的核心在于 StandardButton(单数)和 StandardButtons(复数)。这两者在 C++ 层面承担着截然不同的职责。

1. 按钮的真面目:StandardButton 枚举

诸如 YesNoSaveCancel,它们并非变量,而是枚举(enum)常量。在 Qt 底层源码中,系统利用十六进制数为每个按钮分配了一个具有“唯一二进制位特征”的数字代号(即二的幂次)。

  • 例如:QMessageBox::Yes 的底层值是 0x00004000
  • 由于它们属于 QMessageBox 类的内部枚举,在使用时必须携带作用域前缀(如 QMessageBox::Yes),不能简写。

StandardButton 枚举的简化定义(Qt底层代码中):

class QMessageBox {
public:
    // 定义一个名为 StandardButton 的枚举类型
    enum StandardButton {
        NoButton           = 0x00000000,
        Ok                 = 0x00000400,
        Save               = 0x00000800,
        Yes                = 0x00004000,  // <--- 这就是你看到的 Yes!本质上它是数字 16384
        No                 = 0x00010000,  // <--- 这就是你看到的 No!本质上它是数字 65536
        Cancel             = 0x00400000
        // ... 后面还有很多
    };
};

枚举值的使用补充:

写法 状态 为什么?
QMessageBox::Ok 正确 传统枚举成员暴露在父类作用域中,最常见的 Qt 风格写法。
QMessageBox::StandardButton::Ok 正确 C++11 新增的特性,允许带上枚举名以增加代码清晰度。
StandardButton::Ok 报错 StandardButton 属于 QMessageBox 的内部类型,外部无法直接访问,缺失了类作用域。

多按钮组合机制:按位「或运算」 |

当我们需要在一个提示框中同时显示多个按钮时,不能使用逗号或逻辑或(||),而必须使用 C++ 的按位或运算符(|

按位或的作用是将多个枚举常量的二进制位进行合并。例如 QMessageBox::Save | QMessageBox::Cancel 会生成一个新的整数值,该值同时包含了两个按钮的二进制特征位。Qt 底层拿到这个整数后,通过按位与(&)运算逐个比对,探测到哪些位是 1,就在界面上绘制出对应的按钮。

示例:QMessageBox::Yes | QMessageBox::No

  • Yes = 0x00004000 (二进制: 0000 0000 0000 0000 0100 0000 0000 0000)

  • No = 0x00010000 (二进制: 0000 0000 0000 0001 0000 0000 0000 0000)

因为函数的参数只能接收一个值(buttons 参数),但我们需要传递多个按钮。Qt 的做法就是用 | 把它们缝合在一起:

0000 0000 0000 0000 0100 0000 0000 0000  (Yes 的值)
| 0000 0000 0000 0001 0000 0000 0000 0000  (No  的值)
------------------------------------------------------
0000 0000 0000 0001 0100 0000 0000 0000  (合并后的新值)

通过按位或,我们得到了一个全新的整数。这个整数同时包含了 YesNo 的二进制特征。

2. 类型安全包装盒:StandardButtons

将多个枚举值按位或相加后,在编译器眼中,它退化成了一个普通的整数。为了防止开发者误传任意的无效数字(例如随意传入一个 999 导致程序崩溃),Qt 引入了 StandardButtonsStandardButtons 本质上是一个类(是 QFlags<QMessageBox::StandardButton> 模板类的类型别名)。

  • 旧版本标准写法: StandardButtons(QMessageBox::Yes | QMessageBox::No),这实际上是在调用类的构造函数,将拼接好的整数包装成一个受 Qt 保护和认可的安全对象。
  • 现代 Qt 的隐式转换: 在 Qt 5 / Qt 6 中,由于框架对 QFlags 进行了运算符重载优化,开发者可以直接写 QMessageBox::Yes | QMessageBox::No。编译器会在底层自动隐式调用构造函数,套上 StandardButtons 这个“安全包装盒”。

3.1.3 补充:对话框的国际化(汉化)

QMessageBox 默认渲染的按钮文本为纯英文(如 OK, Cancel, Yes, No)。在标准工程中,不建议为了修改按钮文字而去自定义控件,而是应该在程序的入口处(main.cpp),通过 QTranslator 加载 Qt 官方提供的 qt_zh_CN.qm 翻译文件,注入到应用程序对象中,从而实现所有标准对话框的自动全局汉化。

3.1.4 QMessageBox示例代码

mainwindow的UI中添加5个按钮,分别测试上述5个静态函数

mainwindow.cpp的构造函数

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    this->setWindowIcon(QIcon("C:/Users/ROG/Pictures/001.png"));

    // 信号槽:点击对应按钮后弹出对应要测试的东西
    // 1. about
    connect(ui->aboutBtn, &QPushButton::clicked, this, [this](){
        QMessageBox::about(this, "关于系统",
                          "XXX信息管理系统 v2.0 \n开发者:nameless");
    });

    // 2. information
    connect(ui->informationBtn, &QPushButton::clicked, this, [this]()
    {
        QMessageBox::information(this, "操作成功",
                                 "您的个人文件XXX已成功到上传!",
                                 QMessageBox::Ok,
                                 QMessageBox::NoButton);
    });

    // 3. warning
    connect(ui->warningBtn, &QPushButton::clicked, this, [this]()
    {
        int ret = QMessageBox::warning(this, "警告",
                             "当前网络不稳定,继续上传可能损坏数据。是否继续?",
                             QMessageBox::Yes | QMessageBox::No,
                             QMessageBox::No);

        switch(ret)
        {
        case QMessageBox::Yes:
            qDebug() << "用户点击yes";
            break;
        case QMessageBox::No:
            qDebug() << "用户点击no";
            break;
        default:
            break;
        }
    });

    // 4. critical
    connect(ui->criticalBtn, &QPushButton::clicked, this, [this]()
    {
        QMessageBox::critical(this, "致命错误",
                              "缺失核心组件 XXX,程序即将强制退出!",
                              QMessageBox::Ok,
                              QMessageBox::NoButton);
    });

    // 5. question
    connect(ui->questionBtn, &QPushButton::clicked, this, [this]()
    {
        int ret = QMessageBox::question(this, "保存数据e",
                                        "是否在退出前保存数据?",
                                       QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel,
                                        QMessageBox::Cancel);

        // 基于用户的不同抉择,执行完全不同的底层代码分支
            switch (ret)
            {
                case QMessageBox::Save:
                    qDebug() << "执行:保存数据 -> 关闭";
                    break;
                case QMessageBox::Discard:
                    qDebug() << "执行:丢弃数据 -> 直接关闭";
                    break;
                case QMessageBox::Cancel:
                    qDebug() << "执行:取消操作 -> 继续编辑";
                    break;
                default:
                    break;
            }
    });
}

3.2 QFileDialog

QFileDialogQDialog 的直接子类,提供了一个允许用户选择文件或目录的系统级标准对话框。

QMessageBox 类似,在日常工程开发中,我们绝大多数情况不需要手动实例化 QFileDialog 对象,而是直接调用其封装好的静态方法。这些静态方法会阻塞当前线程(开启局部事件循环),并在用户完成选择后,直接返回用户所选的文件或目录的绝对路径字符串

3.2.1 通用参数与返回值解析

在调用 QFileDialog 的常用静态函数之前,需统一明确其通用参数模型与返回值机制。

通用参数模型:

  • parent: 父窗口指针(通常传 this,确保对话框居中于主界面且共享生命周期)。
  • caption: 弹窗左上角的标题文本。
    • caption /ˈkæpʃn/ n. 说明文字,标题,字幕 vt. 加说明文字
  • dir: 对话框打开时的默认初始目录路径(如 "C:/Users/nameless/Desktop""d:\\temp")。
  • filter: 文件过滤器,用于限制对话框中显示的文件类型。
    • 基本格式(一种后缀名)"自定义描述文字 (*.后缀名)"
    • 多种后缀名,直接用空格 做分隔符,如:``"自定义描述文字 (*.后缀1 *.后缀2)"`
      • 示例:All Files (*.png *.jpg *.jpeg *.txt)
    • 多重过滤器:如果需要指定多个不同类型的过滤器,必须使用双分号 ;; 进行分隔。
      • 示例"Images (*.png *.jpg);;Text files (*.txt)"
  • selectedFilter: 这是一个 QString * 指针(输出参数)。
    • 传入时:指定默认高亮选中的是哪个过滤器。
    • 返回时:将用户在下拉框中实际选中的那个过滤器字符串覆盖写入此指针指向的内存。如果业务逻辑不关心用户选了哪个过滤器,直接传 nullptr
  • options: 对话框的附加行为属性,这是一个枚举标志位(QFlags),即枚举类型。通常使用默认值即可,极少需要人为干预。

返回值的本质(极其重要):

QFileDialog 的静态方法返回的是 QStringQStringList,它代表的是文件在硬盘上的真实绝对路径,而绝对不是某种系统状态码(如 "Ok" 或 "Cancel")。

  • 有效操作:用户选中了图片文件 001.png 并点击“打开/保存”,函数直接返回完整的绝对路径字符串:"D:/temp/001.png"
  • 中断/逃离操作:用户点击了窗口右上角的叉(X)、按下了 Esc 键,或者点击了“取消”按钮。此时,系统既不会崩溃,而是直接返回一个空字符串"")。
  • 工程防呆机制:每次调用获取到路径后,必须第一步检查该字符串是否为空(.isEmpty()。如果为空,说明用户放弃了操作,代码应该直接 return 结束后续逻辑,否则将空路径传给文件 I/O 类会导致程序异常。

3.2.2 核心静态函数群

根据业务需求(选目录、选单文件、选多文件、保存文件),以下是 4 个最常用的静态函数及其实际应用。

1. 获取已存在的目录路径 (Directory)

用于让用户选择一个已存在的文件夹,返回该文件夹的绝对路径。

[static] QString QFileDialog::getExistingDirectory(
                  QWidget *parent = nullptr, 
                  const QString &caption = QString(), 
                  const QString &dir = QString(), 
                  QFileDialog::Options options = ShowDirsOnly);
  • 返回值QString,包含文件夹名的绝对路径。如果用户点击了“取消”或直接关闭窗口,返回空字符串("")。

示例代码:

QString directoryPath = QFileDialog::getExistingDirectory(this, "getExistingDirectory caption", "D:\\tmp");

if(directoryPath.isEmpty())
    QMessageBox::information(this, "info", "用户没有选择文件夹!");
else
    QMessageBox::information(this, "info", "用户选择的文件夹:" + directoryPath);

2. 获取单个文件路径 (Open File)

用于让用户在系统中选择一个已存在的文件。

[static] QString QFileDialog::getOpenFileName(
                  QWidget *parent = nullptr, 
                  const QString &caption = QString(), 
                  const QString &dir = QString(), 
                  const QString &filter = QString(), 
                  QString *selectedFilter = nullptr, 
                  QFileDialog::Options options = Options());
  • 返回值QString,包含文件名的绝对路径。如果用户点击了“取消”或直接关闭窗口,返回空字符串("")。

示例代码:

QString filePath = QFileDialog::getOpenFileName(this, "getOpenFileName caption",
                                                "D://tmp",
                                                "All files (*.png *.jpg *.jpeg *.txt);;Images (*.png *.jpg *.jpeg);;Text files (*.txt)");

if(filePath.isEmpty())
    QMessageBox::information(this, "info", "用户没有选择文件", QMessageBox::Ok);
else
    QMessageBox::information(this, "info", "用户选择的文件" + filePath, QMessageBox::Ok);

3. 获取多个文件路径 (Open Files)

允许用户按住 CtrlShift键在对话框中多选文件。

[static] QStringList QFileDialog::getOpenFileNames(
                  QWidget *parent = nullptr, 
                  const QString &caption = QString(), 
                  const QString &dir = QString(), 
                  const QString &filter = QString(), 
                  QString *selectedFilter = nullptr, 
                  QFileDialog::Options options = Options());
  • 返回值QStringList(即 QList<QString>)。返回一个包含多个文件绝对路径的字符串列表。可以通过 .size() 获取选中数量,通过循环遍历提取每一个路径。

示例代码:

QStringList files = QFileDialog::getOpenFileNames(this, "getOpenFileNames caption",
                                                  "d:\\tmp",
                                                  "全部类型(*.jpg *.png *.jpeg *.txt)");

if(!files.size())
    QMessageBox::information(this, "info", "用户没有选择文件");
else
{
    QString filesPath;
    for(auto& file : files)
        filesPath += (file + '\n');
    QMessageBox::information(this, "info", "用户选择文件包括: \n" + filesPath);
}

4. 获取保存文件路径 (Save File)

用于引导用户选择一个目录,并输入一个预期保存的文件名。

[static] QString QFileDialog::getSaveFileName(
                  QWidget *parent = nullptr, 
                  const QString &caption = QString(), 
                  const QString &dir = QString(), 
                  const QString &filter = QString(), 
                  QString *selectedFilter = nullptr, 
                  QFileDialog::Options options = Options());

注意事项与底层逻辑:

  • 此函数仅仅是获取一个表示路径的字符串,它在底层绝对不会替你创建文件。开发者需要拿到这个返回的字符串后,自行配合 QFileQTextStream / QDataStream 等类去执行真实的 I/O 写入操作。

  • 在这里,filter 参数除了过滤显示外,还是系统给文件名自动添加后缀的依据。如果不写或者写 XXX(*),系统则不自动添加后缀,需要开发者通过代码或要求用户自行输入后缀。

示例代码:

#if 0 // 系统自动添加文件后缀名
QString file1 = QFileDialog::getSaveFileName(this, "getSaveFileName1 caption",
                                             "d:\\tmp",
                                             "自定义后缀(*);;png图片(*.png);;txt文本(*.txt)");
qDebug() << file1;
#else // 自己写文件后缀名
QString file2 = QFileDialog::getSaveFileName(this, "getSaveFileName1 caption", "d:\\tmp");
qDebug() << file2;
#endif

3.3 QFont和QFontDialog

在 Qt 框架中,字体的处理涉及两个核心概念:数据封装用户交互。Qt 将字体的所有底层属性(字号、粗细、字体族等)统一封装在 QFont 数据类中;而为用户提供可视化选择界面的,则是 QDialog 的子类 QFontDialog

学习本节的逻辑主线是:先理解如何通过代码使用 QFont 定义和应用字体,再学习如何通过 QFontDialog 界面让用户去动态生成一个 QFont 对象。

3.3.1 QFont 字体属性类

QFont 不是一个 UI 控件,而是一个纯粹的数据类。它包含了渲染文本所需的所有排版特征。

1. QFont核心构造函数

开发者可以在实例化 QFont 对象时,直接通过构造函数初始化字体的核心特征。

QFont::QFont(); // 无参构造

QFont::QFont(const QString &family, int pointSize = -1, int weight = -1, bool italic = false);

参数说明:

  1. const QString &family字体族名称

    • 告诉 Qt 你想使用哪种基础字体的参数。

    • 匹配机制:Qt 有一套非常智能的字体匹配算法(Font Matching Algorithm)。如果你指定的字体(比如 "Microsoft YaHei")在用户的操作系统中不存在,Qt 不会让程序崩溃,而是会自动在系统中寻找一种字符集、外观最相近的替代字体来渲染。

    • 传入空字符串:如果你传入一个空字符串 "",Qt 会直接使用当前应用程序的默认字体(通常由 QApplication::setFont() 全局设置,或者跟随操作系统主题)。

  2. int pointSize = -1 (字号大小 / 磅值)

    控制字体在屏幕上的物理显示大小。

    • 正整数(如 9, 12, 16):指定具体的磅值(1 磅 = 1/72 英寸)。因为是物理单位,它会自动适应不同分辨率(DPI)的屏幕。同一磅值的字,在 1080P 和 4K 屏幕上看起来物理尺寸是一样大的,非常适合做跨平台的高分屏适配。

    • 特殊值 -1(默认):告诉 Qt “我不想硬编码字号,请帮我直接使用系统或父窗口的默认字号”。

      注意pointSize 绝对不能填 0 或其他负数,否则 Qt 会将其视为非法输入并强制回退到默认字号。

  3. int weight = -1 (字重 / 粗细程度)

    控制字体的笔画粗细。

    • 数据类型int(但强烈建议使用 QFont::Weight 枚举类型,以保证代码可读性)。
    • 范围说明(Qt 5):有效范围是 0 ~ 99。数字越大,字体越粗。-1 表示使用默认粗细。

    常用枚举QFont::Weight的对照表(Qt5)

    Qt 5 枚举名称 对应数值 适用场景 / 说明
    QFont::Light 25 细体,适合大标题或需要现代感的设计
    QFont::Normal 50 正常粗细,正文默认值
    QFont::DemiBold 63 半粗体,比正常稍微加重一点
    QFont::Bold 75 粗体,常用于强调文字或按钮文本
    QFont::Black 87 黑体(极粗),视觉冲击力最强

    💡 框架升级提示:如果你未来升级到 Qt 6,Qt 为了与现代网页 CSS 和 OpenType 字体标准对齐,将字重的范围改为了 1 ~ 1000(例如 Qt 6 中 QFont::Normal 变成了 400,QFont::Bold 变成了 700)。不过枚举的名称没变,所以坚持使用枚举(如 QFont::Bold)会让你的代码在升级时完全不受影响。

  4. bool italic = false (是否倾斜)

    控制文本是否以斜体显示。

    • false(默认值):正体显示。
    • true:斜体显示。如果该字体本身没有专门设计的斜体字形(Italic variant),Qt 的底层渲染引擎会通过数学变换,将正体字强行拉斜(Oblique)来模拟斜体效果。
2. QFont属性的 Set / Get 方法

QFont 提供了极其规整的 API 用于动态修改或获取字体属性。

注意区分 PointSize 与 PixelSize:pointSize基于物理尺寸的单位(跨平台显示比例更一致,工程推荐),而 pixelSize绝对像素大小(受不同显示器分辨率 DPI 影响较大)。

// 设置属性 (Setters)
void QFont::setFamily(const QString &family);
void QFont::setPointSize(int pointSize);       // 推荐:设置字号(磅)
void QFont::setPixelSize(int pixelSize);       // 设置绝对像素大小
void QFont::setWeight(int weight);             // 粗细 (0~99)
void QFont::setBold(bool enable);              // 快捷开关:加粗 (等效于 weight 置为 75)
void QFont::setItalic(bool enable);            // 快捷开关:倾斜

// 获取属性 (Getters,命名规律为去掉 set 前缀)
QString QFont::family() const;
int QFont::pointSize() const;
int QFont::pixelSize() const;
int QFont::weight() const;
bool QFont::bold() const;
bool QFont::italic() const;
3. 字体的应用作用域

创建并配置好 QFont 对象后,需要将其应用到具体的界面元素上。Qt 的字体系统具有继承机制:如果在父节点设置了字体,子节点默认继承该字体(除非子节点单独设置了其他字体)。

// 1. 窗口级作用域 (QWidget 类)
// 仅对当前窗口及其包含的子控件生效
void QWidget::setFont(const QFont &);
const QWidget::QFont& font() const;

// 2. 全局应用程序级作用域 (QApplication 类)
// 作用于当前进程内的所有窗口和控件,通常在 main.cpp 中初始化时调用
[static] void QApplication::setFont(const QFont &font, const char *className = nullptr);
[static] QFont QApplication::font();

3.3.2 QFontDialog 类的静态 API

QFontDialog 提供了一个标准的系统级对话框,供用户挑选字体。与 QFileDialog 类似,我们绝大多数场景直接调用其静态方法 getFont(),该方法会阻塞线程并弹出界面。

/*
参数解析:
  - ok: 极其重要的状态输出参数。传入一个 bool 变量的地址。若用户点击“确定”,该地址的值被底层写为 true;若点击“取消”或关闭窗口,被写为 false。
  - initial: 对话框弹出时,默认高亮选中的初始字体设置。
  - parent: 父窗口指针(管理生命周期与居中显示)。
  - title: 字体对话框的标题栏文字。
  - options: 附加选项,通常保持默认无需修改。
*/
[static] QFont QFontDialog::getFont(
        bool *ok, 
        const QFont &initial, 
        QWidget *parent = nullptr, 
        const QString &title = QString(), 
        QFontDialog::FontDialogOptions options = FontDialogOptions());

// 简易重载版本(不指定初始字体和标题)
[static] QFont QFontDialog::getFont(bool *ok, QWidget *parent = nullptr);

底层逻辑与状态校验:

由于 getFont() 函数的返回值类型被固定为 QFont(返回用户选中的字体对象),这就带来了一个工程问题:如果用户点击了“取消”或叉掉了窗口,函数该返回什么?

它依然必须返回一个合法的 QFont 对象(通常是系统默认字体或你传入的初始字体)。为了区分用户到底是点击了“确定”还是“取消”,Qt 引入了 bool *ok 这个指针作为传出参数(输出参数)

3.3.3 QFontDialog示例代码

在实际开发中,调用 QFontDialog 获取字体后,必须第一步校验 ok 的值。这就是该模块的防呆机制,防止用“取消”操作返回的无效或默认字体去覆盖掉用户界面原有的正常字体。

示例代码(MainWindow的构造函数中绑定信号槽):

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    connect(ui->qFontDialogTestBtn, &QPushButton::clicked, this, [this]()
    {
        // 1. 准备一个状态接收变量
        bool isOk;

        // 2. 定义初始字体(如:微软雅黑,12磅,加粗),作为对话框的默认显示
        QFont defaultFont("Microsoft YaHei", 12, QFont::Bold);

        // 3. 调用静态方法,阻塞弹出对话框
        QFont selectedFont = QFontDialog::getFont(
                     &isOk,          // 传入指针,接收用户操作状态
                     defaultFont,    // 初始状态
                     this,           // 父窗口
                     "请选择系统字体"  // 窗口标题
                 );

        // 4. 严谨的校验与分支逻辑
        if(isOk)
        {
            QMessageBox::information(this, "Font Information",
                                     "选择" + selectedFont.family() + "字体成功,设置成功");
            // 设置后,mainwindow及其所有子控件的字体都会同步改变(继承机制)
            this->setFont(selectedFont);
        }
        else
        {
            QMessageBox::warning(this, "Font Warning",
                                 "设置字体失败,请重试!");
        }
    });
}

3.4 QColor和QColorDialog

主要内容包括:纯数据封装类 QColor 的核心机制与属性控制,以及系统标准对话框 QColorDialog 的静态调用与状态校验机制。

3.4.1 QColor颜色属性类

QFont 类似,QColor 并不是一个 UI 控件,而是一个纯粹的数据类。它在 Qt 框架中专门用于封装和管理颜色属性。

Qt 的颜色系统主要基于 RGBA 颜色模型构建,各通道取值范围均为 0 ~ 255(即 8-bit 无符号整数):

  • R (Red):红色分量
  • G (Green):绿色分量
  • B (Blue):蓝色分量
  • A (Alpha):透明度通道(默认值为 255,即完全不透明;0 表示完全透明)

QColor常用API:

1. QColor构造与初始化

除了直接传入 RGB 值,Qt 还预置了 Qt::GlobalColor 枚举(如 Qt::red, Qt::white, Qt::transparent 等),便于快速构造常用基础颜色。

// 1. 无参构造 (注意:默认构造出的 QColor 对象是一个非法/无效的颜色)
QColor::QColor();

// 2. 使用 Qt 预置全局颜色枚举构造
QColor::QColor(Qt::GlobalColor color);

// 3. 使用 RGBA 数值构造 (alpha 默认 255)
QColor::QColor(int r, int g, int b, int a = 255);

Qt::GlobalColor枚举补充:

Qt::GlobalColor 是 Qt 预定义的一组基本颜色枚举,用于免去开发者手敲 RGB 值的麻烦。它的底层本质只是一些普通的整数标识,当你使用它们时,Qt 会自动将其作为索引,去底层预设的颜色数组中查表,转换为真实的十六进制颜色值。

颜色系 枚举常量示例 对应的十六进制值/说明
基础无色系 Qt::black, Qt::white, Qt::transparent 分别对应 #000000, #FFFFFF, 以及纯透明状态
灰度系 Qt::gray, Qt::lightGray, Qt::darkGray 分别对应 #A0A0A4, #C0C0C0, #808080
标准亮色 Qt::red, Qt::green, Qt::blue RGB 极值,如 #FF0000, #00FF00, #0000FF
标准暗色 Qt::darkRed, Qt::darkGreen 亮色前加 dark,亮度值通常减半,如 #800000
2. 属性设置(Setters)
// ------------- 属性设置 -------------
void QColor::setRed(int red);      // 单独修改红色分量
void QColor::setGreen(int green);  // 单独修改绿色分量
void QColor::setBlue(int blue);    // 单独修改蓝色分量
void QColor::setAlpha(int alpha);  // 单独修改透明度
// 一次性重新设置 RGBA
void QColor::setRgb(int r, int g, int b, int a = 255);
3. 获取属性(getters)
// ------------- 属性获取 -------------
int QColor::red() const;
int QColor::green() const;
int QColor::blue() const;
int QColor::alpha() const;

// 批量获取:通过指针将当前颜色的 RGBA 值回写到传入的变量中
void QColor::getRgb(int *r, int *g, int *b, int *a = nullptr) const;

3.4.2 QColorDialog 颜色选择对话框

QColorDialog 继承自 QDialog。在工程实战中,通常避免显式实例化该类对象,而是直接调用其静态方法,以阻塞当前线程的方式唤起系统级颜色选择面板。

1. QColorDialog核心静态函数:getColor

该函数会挂起当前线程,弹出标准系统调色板,接收用户交互输入,并将其封装为 QColor 对象返回。

[static] QColor QColorDialog::getColor(
        const QColor &initial = Qt::white, 
        QWidget *parent = nullptr, 
        const QString &title = QString(), 
        QColorDialog::ColorDialogOptions options = ColorDialogOptions());

参数解析:

  • initial: 弹窗初始化时默认高亮锚定的颜色(缺省为 Qt::white)。
  • parent: 指定父窗口对象,用于接管生命周期管理并确保对话框居中显示。
  • title: 自定义对话框顶部标题栏文本。
  • options: 附加行为枚举选项(例如 QColorDialog::ShowAlphaChannel 用于强制显示透明度调节滑块)。
2. 右上角的叉(X)或者“取消”按钮 的 防呆校验机制 (isValid)

QFileDialog 中用户取消操作返回空字符串的逻辑不同,若用户在此面板点击右上角关闭按钮或“取消”按键,底层将返回一个无效的 QColor 对象

工程规范:在接收返回值后,严禁直接将其应用于绘图系统或样式表渲染。必须优先调用 color.isValid() 执行合法性校验,以避免引发 UI 渲染异常或底层断言崩溃。

3.4.3 示例代码:

UI 文件包含两个 QLabel (colorTextLabel 用于文本输出,showColorLabel 用于色块展示) 以及一个触发按钮 (QcolorDialogTestBtn)。

mainwindow构造函数

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QColorDialog>
#include <QVariant> // 理论可用,但在字符串拼接场景下推荐使用 QString::arg()

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    connect(ui->QcolorDialogTestBtn, &QPushButton::clicked, this, [this]()
    {
        // 阻塞线程:唤起调色板,设置默认色为白,显式开启 Alpha 通道支持
        QColor clr = QColorDialog::getColor(Qt::white, this, "nameless - choose color", QColorDialog::ShowAlphaChannel);

        // 核心步骤:合法性校验
        if(clr.isValid())
        {
            // 优化:采用 QString::arg() 替代繁琐的 QVariant 强转拼接,提升运行效率与可读性
            QString colorText = QString("| R: %1 | G: %2 | B: %3 | ALP: %4 |")
                                    .arg(clr.red())
                                    .arg(clr.green())
                                    .arg(clr.blue())
                                    .arg(clr.alpha());
            ui->colorTextLabel->setText(colorText);

            // 修正:默认的 clr.name() 仅返回 #RRGGBB,会丢失 Alpha 通道数据
            // 需传入 QColor::HexArgb 参数,以生成符合 QSS 规范且包含透明度的 #AARRGGBB 字符串
            QString qss = QString("background-color: %1;").arg(clr.name(QColor::HexArgb));
            ui->showColorLabel->setStyleSheet(qss);
        }
    });
}

3.5 QInputDialog

QInputDialogQDialog 的直接子类,提供了一个简单便捷的模态系统对话框,专门用于从用户处获取单一的数值、字符串或从列表中选择一项数据。

QMessageBoxQFileDialog 的使用逻辑高度一致,在日常工程开发中,我们几乎不会去显式实例化 QInputDialog 对象,而是直接调用其封装好的静态方法(Static Methods)。这些方法会阻塞当前代码执行流(开启局部事件循环),并在用户完成交互后直接返回对应类型的数据。

3.5.1 核心避坑指南与防呆机制:bool *ok 参数

  • 底层逻辑差异:对于 QFileDialog,如果用户点击“取消”或关闭窗口,函数会返回空字符串 "",我们可以通过 .isEmpty() 来判断。但是,对于 QInputDialog::getIntgetDouble,如果用户点击“取消”,函数会直接返回传入的默认值(value

  • 工程防呆要求: 如果没有 ok 参数,程序将彻底无法区分“用户手动输入了与默认值相同的数字并点击确定”和“用户直接点击了取消”。因此,必须传入 bool 变量的地址

    • 当用户点击“确定”时,底层会将该地址写入 true
    • 若用户点击“取消”或关闭窗口(Esc / 右上角叉),则写入 false
    • 结论: 获取返回值后的第一步,必须是对 ok 变量进行合法性分支校验。

3.5.2 API - 静态函数与工程代码示例

将这 5 个核心 API 分为三大类:数值输入下拉列表项选择文本输入

1. 数值输入对话框 (Int / Double)

用于获取指定范围和步长的整型或浮点型数据。由于是基于 QSpinBoxQDoubleSpinBox 渲染的,底层会自动拦截非法字符输入。

获取整型 (getInt)

// 得到一个可以输入整形数的对话框窗口, 返回对话框窗口中输入的整形数
/*
参数:
  - parent: 父窗口指针
  - title: 弹窗标题
  - label: 提示文本(描述输入内容)
  - value: 输入框中的默认初始值, 默认为 0
  - min: 允许输入的最小值
  - max: 允许输入的最大值
  - step: 步长(点击微调按钮时每次增减的量)
  - ok: 状态输出参数,接收用户是点击了确定(true)还是取消(false)
  - flags: 窗口属性标志
*/
[static] int QInputDialog::getInt(
            QWidget *parent, const QString &title, 
            const QString &label, int value = 0, 
            int min = -2147483647, int max = 2147483647, 
            int step = 1, bool *ok = nullptr, 
            Qt::WindowFlags flags = Qt::WindowFlags());

获取浮点型 (getDouble)

// 得到一个可以输入浮点数的对话框窗口, 返回对话框窗口中输入的浮点数
/*
参数与 getInt 基本一致,特有参数:
  - decimals: 浮点数的精度,即保留小数点后几位 (默认 1 位)
*/
[static] double QInputDialog::getDouble(
            QWidget *parent, const QString &title, 
            const QString &label, double value = 0, 
            double min = -2147483647, double max = 2147483647, 
            int decimals = 1, bool *ok = nullptr, 
            Qt::WindowFlags flags = Qt::WindowFlags());

示例代码

(在 UI 中增加一个名为 InputDialogTestBtn 的按钮)

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

#define GETDOUBLE

    connect(ui->InputDialogTestBtn, &QPushButton::clicked, [this]()
    {
#ifdef GETINT
        bool isOkInt;
        int retInt = QInputDialog::getInt(this, "Get Age", "Please enter your age:", 0, 0, 150, 2, &isOkInt);
        if(isOkInt)
            QMessageBox::information(this, "Show Age", "The age you entered: " + QString::number(retInt));
        else
            QMessageBox::information(this, "Show Age", "You haven't entered anything!");
#endif // GETINT

#ifdef GETDOUBLE
        bool isOkDouble;
        double retDouble = QInputDialog::getDouble(this, "Get Salary", "Please enter your salary:",
                                                    0, -10000, 10000, 2, &isOkDouble);
        if(isOkDouble)
            QMessageBox::information(this, "Show Salary", "Your salary: " + QString::number(retDouble));
        else
            QMessageBox::information(this, "Show Salary", "No salary entered!");
#endif // GETDOUBLE
    });
}
2. 下拉菜单项选择对话框 (Item)

基于 QComboBox 渲染,允许用户从预设的字符串列表中选择一项。

// 得到一个带下拉菜单的对话框窗口, 返回所选菜单项的文本内容
/*
核心参数:
  - items: QStringList类型,预设的字符串列表数据源
  - current: 默认选中的列表项索引 (0 表示第一项)
  - editable: true(允许用户不仅能选,还能在框内自己输入新文本) / false(只能从下拉列表中点选)
*/
[static] QString QInputDialog::getItem(
            QWidget *parent, const QString &title, 
            const QString &label, const QStringList &items, 
            int current = 0, bool editable = true, bool *ok = nullptr, 
            Qt::WindowFlags flags = Qt::WindowFlags(), 
            Qt::InputMethodHints inputMethodHints = Qt::ImhNone);

示例代码:

(在 UI 中增加一个名为 InputDialogTestBtn 的按钮)

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    connect(ui->InputDialogTestBtn, &QPushButton::clicked, [this]()
    {
        QStringList characters;
        characters << "Reimu" << "Marisa" << "Sakuya" << "Remilia" << "Flandre";
        
        bool isOkItem;
        QString retItem = QInputDialog::getItem(this, "Choose One", "Select your character:",
                                                 characters, 0, false, &isOkItem);
        if(isOkItem)
            QMessageBox::information(this, "Show Character", "You chose: " + retItem);
        else
            QMessageBox::information(this, "Show Character", "You haven't chosen anyone!");
    });
}
3. 文本输入对话框 (Text / MultiLineText)

用于获取用户输入的字符串。包含单行文本(基于 QLineEdit)和多行文本(基于 QTextEdit)。

单行文本输入 (getText)

// 得到一个可以输入单行信息的对话框窗口, 返回用户输入的文本字符串
/*
核心参数:
  - mode: QLineEdit::EchoMode 枚举类型,控制文本显示模式:
    - QLineEdit::Normal: 正常明文显示 (默认)
    - QLineEdit::Password: 密文掩码显示 (实心圆点或星号),常用于密码输入
    - QLineEdit::NoEcho: 不显示任何字符 (防窥视级别最高)
    - QLineEdit::PasswordEchoOnEdit: 仅在敲击键盘的瞬间明文显示,随后变为密文
  - text: 文本框中默认填充的字符串
*/
[static] QString QInputDialog::getText(
            QWidget *parent, const QString &title, const QString &label,
            QLineEdit::EchoMode mode = QLineEdit::Normal, 
            const QString &text = QString(), bool *ok = nullptr, 
            Qt::WindowFlags flags = Qt::WindowFlags(), 
            Qt::InputMethodHints inputMethodHints = Qt::ImhNone);

多行文本输入 (getMultiLineText)

// 得到一个可以输入多行数据的对话框窗口, 返回用户在窗口中输入的文本信息
// 注意:多行输入不支持 EchoMode 的密文掩码模式
[static] QString QInputDialog::getMultiLineText(
            QWidget *parent, const QString &title, const QString &label, 
            const QString &text = QString(), bool *ok = nullptr, 
            Qt::WindowFlags flags = Qt::WindowFlags(), 
            Qt::InputMethodHints inputMethodHints = Qt::ImhNone);

示例代码:

(在 UI 中增加一个名为 InputDialogTestBtn 的按钮)

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

#define MULTI

    connect(ui->InputDialogTestBtn, &QPushButton::clicked, [this]()
    {
#ifdef SINGLE
        bool isOkLine;
        QString retLine = QInputDialog::getText(this, "Full Name", "Enter your full name:",
                                                 QLineEdit::Password, "Please enter...", &isOkLine);
        if(isOkLine)
            QMessageBox::information(this, "Show Name", "Your full name: " + retLine);
        else
            QMessageBox::information(this, "Show Name", "You haven't entered anything!");
#endif // SINGLE

#ifdef MULTI
        bool isOkMulti;
        QString retMulti = QInputDialog::getMultiLineText(this, "Get Multi Text", "Enter something here:",
                                                           "...", &isOkMulti);
        if(isOkMulti)
            QMessageBox::information(this, "Show Something", retMulti);
        else
            QMessageBox::information(this, "Show Something", "You haven't entered anything!");
#endif // MULTI
    });
}

3.6 QProgressDialog

QProgressDialog 类是 QDialog 的直接子类。通过这个类,我们可以得到一个自带进度条的标准对话框窗口。这种类型的对话框通常用于需要耗时的后台操作场景,例如:文件拷贝、数据下载、模型加载等,用于向用户展示实时进度并提供中止操作的入口。

3.6.1 常用API

QProgressDialog 的 API 主要围绕文本提示、进度极值控制以及自动化行为展开。

1. 构造与初始化
// 构造函数
QProgressDialog::QProgressDialog(QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags());

QProgressDialog::QProgressDialog(const QString &labelText, const QString &cancelButtonText, 
                                 int minimum, int maximum, QWidget *parent = nullptr,
                                 Qt::WindowFlags f = Qt::WindowFlags());
/*
参数:
   - labelText: 对话框正文中显示的提示信息
   - cancelButtonText: 取消按钮上显示的文本信息 (若传入 QString() 空串,则不显示取消按钮)
   - minimum: 进度条的最小值 (通常设为 0)
   - maximum: 进度条的最大值 (通常设为 100 或文件总大小)
   - parent: 当前窗口的父对象 (管理生命周期与居中显示)
   - f: 窗口的标志位属性,通常使用默认值即可
*/
2. 文本与界面控制
// 得到与设置进度条上方的提示文本
QString QProgressDialog::labelText() const;
void QProgressDialog::setLabelText(const QString &text);

// 重新设置取消按钮显示的文本信息
[slot] void QProgressDialog::setCancelButtonText(const QString &cancelButtonText);
3. 范围与进度控制
// 得到与设置进度条的极值
int QProgressDialog::minimum() const;
void QProgressDialog::setMinimum(int minimum);

int QProgressDialog::maximum() const;
void QProgressDialog::setMaximum(int maximum);

// 快捷设置进度条范围 (最大和最小值)
[slot] void QProgressDialog::setRange(int minimum, int maximum);

// 得到与设置进度条当前的值
int QProgressDialog::value() const;
void QProgressDialog::setValue(int progress);
4. 自动化行为控制 (默认开启)
// 当 value() == maximum() 时,对话框是否自动调用 reset() 重置数据。此属性默认为 true。
bool QProgressDialog::autoReset() const;
void QProgressDialog::setAutoReset(bool reset);

// 当 value() == maximum() 时,对话框是否自动隐藏。此属性默认为 true。
bool QProgressDialog::autoClose() const;
void QProgressDialog::setAutoClose(bool close);

QProgressDialog::reset()相关补充

属性/状态 QProgressDialog::reset() 发生前 QProgressDialog::reset() 发生后
对话框在屏幕上 正在显示 隐藏起来了(从屏幕消失)
当前的进度值 达到了最大值 (100%) 内部状态归零,等待下次任务
预估剩余时间 显示“还剩 0 秒” 清空计算,下次重新算
“取消”状态 如果你按了取消,就是 true 恢复成 false(清空不良记录)
窗口标题和文字 "正在处理数据..." 完全没变,还是"正在处理数据..."
父对象(Parent) 依附于你的主窗口 完全没变,依然依附于主窗口
5. 状态获取与信号
// 判断用户是否在交互中按下了“取消”键。按下了返回 true, 否则返回 false。通常在耗时循环中通过判断此值来决定是否 abort 任务。
bool wasCanceled() const;

// 重置进度对话框:将进度归零,wasCanceled() 变为 true,且隐藏对话框。
[slot] void QProgressDialog::cancel();

// 重置进度对话框:将进度归零。如果 autoClose() 为真,对话框将被隐藏。
[slot] void QProgressDialog::reset();   

// ------------- 信号 -------------
// 当用户单击 Cancel 按钮时发射此信号。默认情况下,Qt 底层已经将该信号自动连接到了 cancel() 槽函数。
[signal] void QProgressDialog::canceled();
6. 补充:模态设置 (继承自 QWidget)
// 设置窗口的显示状态 (模态 / 非模态)
/*
参数:
    Qt::NonModal  -> 非模态 (不阻塞任何窗口)
    Qt::WindowModal -> 窗口模态 (仅阻塞其父窗口及其祖先窗口)
    Qt::ApplicationModal -> 应用模态 (阻塞当前应用程序中的所有其他窗口)
*/
void QWidget::setWindowModality(Qt::WindowModality windowModality);

setWindowModality 配合 show()exec() 的本质区别:

  • UI 阻塞 vs 线程阻塞: setWindowModality(...) 结合 show(),仅仅是在操作系统的事件分发系统(Event Dispatcher)层面,拦截了发往其他窗口的鼠标和键盘事件。它绝对不会阻塞当前 C++ 函数的执行流,代码会立刻执行 show() 后面的语句。
    • 哪个是UI阻塞,哪个是线程阻塞?我不知道
  • 双重阻塞:QDialog::exec() 是一种暴力封装。调用它时,Qt 会在底层不仅强行将窗口设为 Qt::ApplicationModal,还会开启一个局部事件循环(Local Event Loop)。这个事件循环会死死卡住当前的 C++ 线程,导致 exec() 后面的代码全部挂起,直到窗口被销毁或隐藏。

如果父对象传了 nullptr,就算是设置了 Qt::WindowModal(半模态),也不会阻塞任何窗口。

3.6.2 示例代码

场景描述: 基于定时器模拟文件拷贝的场景。

  1. 点击主窗口按钮progressBtn后,弹出进度条窗口(模态或非模态),并启动定时器;
  2. 通过定时器信号,按固定频率更新进度;
  3. 当进度达标或用户点击取消时, 关闭定时器, 关闭并析构进度对话框。

mainwindow.cpp 的构造函数:

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

// 宏定义:方便一键切换模态与非模态进行调试测试
#define IS_MODAL 1

    connect(ui->progressBtn, &QPushButton::clicked, this, [this]()
    {
        // 1. 在堆上实例化进度条对话框
        QProgressDialog* progress = new QProgressDialog(
                    "正在拷贝数据...", "取消拷贝", 0, 100, this);
        progress->setWindowTitle("请稍后"); // 继承QWidget的方法

#if IS_MODAL // 设置是否模态
        // 2. 设置模态 (阻塞父窗口,禁止用户在拷贝时进行其他操作)
        // 不能用exec,因为仍需执行构造函数后续内容
        progress->setWindowModality(Qt::WindowModal);
#endif

        // 3. 显示窗口 (非阻塞方法,保证后续代码继续执行)
        progress->show(); // 继承自QWidget

        // 4. 更新进度条(定时器模拟)
        static int value = 0;
        // int value = 0; 没有static出了构造函数value就被析构了
        QTimer* timer = new QTimer{this};
        connect(timer, &QTimer::timeout, this, [=]()
        {
            progress->setValue(value);
            value++;
            // 当value > 最大值的时候,关闭定时器
            if(value > progress->maximum())
            {
                timer->stop();
                value = 0;
                delete progress;
                delete timer;
                QMessageBox::information(this, "拷贝成功", "数据文件已同步完成!");
            }
        });

        // 5. 处理点击取消按钮的信号槽
        connect(progress, &QProgressDialog::canceled, this, [=]()
        {
            timer->stop();
            value = 0;
            delete progress;
            delete timer;
            QMessageBox::warning(this, "拷贝中止", "用户已手动取消数据同步!");
        });

        // 6. 所有信号槽注册完毕后,正式启动定时器 (每 50 毫秒触发一次)
        timer->start(50);
    });
}

注意: QMessageBox 的静态函数在底层会自动调用 exec()。所以要先执行完需要的操作,再使用QMessageBox

4. QMainWindow

QMainWindow 是 Qt 标准基础窗口中结构最复杂的窗口,其核心组成元素及数量限制如下:

  • 菜单栏 (MenuBar):最多且只能有 1 个,固定位于窗口最上方。
  • 状态栏 (StatusBar):最多且只能有 1 个,固定位于窗口最下方。
  • 工具栏 (ToolBar):可以有 n 个(多个),默认提供 1 个,可停靠于窗口的上下左右边缘。
  • 停靠窗口 (DockWidget):可以有 n 个(多个),默认不提供,可停靠于窗口的上下左右边缘。

image

4.1 菜单栏

菜单栏(QMenuBar)是窗口顶部的水平条,,用于容纳各种下拉顶级菜单。在一个 QMainWindow 窗口中,最多只能存在一个菜单栏实例。

4.1.1 通过 UI 设计器添加菜单项

在 Qt Designer(UI 设计器)中,菜单栏的层级关系为:菜单栏 (QMenuBar) -> 菜单 (QMenu) -> 动作/子菜单项 (QAction)

  1. 添加顶级菜单(QMenu):
    • 双击 UI 界面顶部的“在这里输入”(Type Here)。
    • 输入菜单名称(如:文件编辑),回车即可。
    • 注意:若部分 Qt 版本遇到无法直接在窗口输入中文的 Bug,可先在其他地方写好中文复制粘贴进去,或者在右侧属性栏中修改 title 属性。
  2. 添加子菜单项(QAction):
    • 点击已创建的顶级菜单,在展开的下拉列表中双击“在这里输入”。
    • 输入子项名称(如:新建打开),回车即可创建一个触发动作(QAction)。
  3. 通过 Action Editor(动作编辑器)管理:
    • 每在 UI 中添加一个子菜单项,Qt Creator 底部就会自动在 Action Editor 中生成一个对应的 QAction 对象。
    • 推荐做法:也可以先在 Action Editor 中点击新建 QAction,设置好对象名和文本,然后直接将它拖拽到对应的菜单下拉列表中。

4.1.2 通过代码动态添加菜单及菜单项

在代码中构建菜单栏,通常在 QMainWindow 的构造函数中进行。其核心逻辑是:获取菜单栏 -> 创建(顶级)菜单 -> 创建并添加子菜单/动作 -> 连接信号与槽

1. 获取菜单栏(QMainWindow 级API)
QMenuBar* QMainWindow::menuBar() const
  • 功能:获取主窗口的菜单栏实例。若当前窗口未初始化菜单栏,此方法将隐式创建一个新的 QMenuBar 并返回。
  • 返回值:指向当前窗口唯一菜单栏的指针(QMenuBar*)。
2. 创建添加顶级菜单(QMenuBar 级 API)
QMenu* QMenuBar::addMenu(const QString &title)
  • 功能:在菜单栏实例中添加一个新的顶级菜单(如“文件”、“编辑”)。
  • 参数 title:菜单显示的文本。通过在字母前添加 & 符号可绑定 Alt 快捷键(例如:传入 "系统(&S)",即可通过 Alt + S 展开该菜单)。
  • 返回值:返回一个 QMenu* 指针,指向新创建的顶级菜单(必须接收返回值)。
3. 创建添加子菜单/动作(QMenu 级 API)
QAction* QMenu::addAction(const QString &text)
  • 功能:在当前菜单中实例化并附加一个具体的动作项(即下拉列表中的可点击项)。
  • 参数 text:动作项的显示文本,同样支持 & 符号指定菜单展开后的单键快捷键。
  • 返回值:返回新生成的 QAction* 指针。此指针是后续绑定 triggered 信号与具体业务槽函数的核心对象。

补充:插入水平分割线

QAction* QMenu::addSeparator()
  • 功能:在当前菜单项序列的末尾插入一条水平分割线,用于功能模块的视觉隔离。
4. QAction对象(子菜单)的属性配置

1. 配置状态提示栏

void QAction::setStatusTip(const QString &statusTip)
  • 功能:配置状态栏提示。当光标悬停于该 QAction 时,底层状态栏 (QStatusBar) 将同步展示此参数传入的说明文本。

2. 指定全局系统快捷键

void QAction::setShortcut(const QKeySequence &shortcut)
  • 功能:为指定动作配置全局系统快捷键。
  • 参数 shortcut:支持标准字符串输入(如 QKeySequence("Ctrl+N"))或标准枚举值(如 QKeySequence::New)。

4.1.3 示例代码

前置条件:在 UI 设计器中已预先创建菜单 menu0(测试),并在其下创建两个子菜单项 saber_action(saber)与 dxl_action(冬雪莲)。

mainwindow.cpp 构造函数实现:

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

//一. 注册信号槽:通过UI添加的子菜单(saber_action和dxl_action)
    connect(ui->saber_action, &QAction::triggered, this, [=]()
    {
        QMessageBox::information(this, "saber", "saber: EX 咖喱棒!!!");
    });

    connect(ui->dxl_action, &QAction::triggered, this, [=]()
    {
        QMessageBox::warning(this, "warning", "冬雪莲:骂谁罕见!啊!骂谁罕见!!!");
    });

// 二. 通过代码添加的菜单和子菜单
    // 1. 获取窗口自带的菜单栏(如果不存在会自动创建)
    QMenuBar* menuBar = this->menuBar();

    // 2. 创建并添加顶级菜单(QMenu对象)
    //"&F" 和 "&E" 表示快捷键,按下 Alt + F 即可打开文件菜单
    QMenu* fileMenu = menuBar->addMenu("文件(&F)");
    QMenu* editMenu = menuBar->addMenu("编辑(&E)");

    // 3. 创建并添加子菜单(QAction对象)
    // 3.1 通过 QMenu::addAction() 直接添加,返回 QAction 指针(推荐)
    QAction* newAction = fileMenu->addAction("新建(&N)");
    editMenu->addSeparator(); // 在菜单项之间添加一条分割线
    QAction* openAction = fileMenu->addAction("打开(&O)");

    // 3.2 先独立创建 QAction 对象,再手动添加到菜单中(适用于复杂配置)
    QAction* exitAction = new QAction("EXIT", this);
    exitAction->setShortcut(QKeySequence("Ctrl+Q"));    // 设置快捷键
    exitAction->setStatusTip("退出应用程序"); // 设置状态栏提示信息
    editMenu->addAction(exitAction); // 将动作加入文件菜单

    // 4. 连接信号与槽(让菜单项具备实际功能)
    // 点击“退出”菜单项时,触发窗口的关闭槽函数
    connect(exitAction, &QAction::triggered, this, &QMainWindow::close);
}

4.2 工具栏 (QToolBar)

工具栏(QToolBar)是 QMainWindow 中用于存放常用操作快捷方式的面板。通常,工具栏中的按钮与菜单栏(QMenuBar)中的高频子项(QAction)是一一对应的。

与菜单栏、状态栏“全局唯一”的特性不同,一个 QMainWindow 中可以存在 多个 工具栏。默认情况下,系统提供 1 个,用户可将其停靠在窗口的上下左右四个边缘(ToolBar Area),或者将其拖拽出来作为独立窗口悬浮于桌面上。

4.2.1 通过 UI 设计器添加工具栏

在 Qt Designer 中,我们可以极其直观地管理工具栏及其内部元素。

1. 添加与删除工具栏:
  • 添加: 在主窗口的空白处右键单击,选择 “添加工具栏” (Add Tool Bar)。系统会在窗口顶部生成一个新的空白工具栏面板。
  • 删除: 右键点击已有的工具栏,选择 “移除工具栏” (Remove Tool Bar)
2. 向工具栏中添加动作(QAction)
  • 操作方法:在底部的 Action Editor(动作编辑器) 中,选中已经创建好的 QAction(通常是为菜单栏准备的),直接用鼠标左键按住,拖拽到 UI 界面的工具栏面板上松开即可。
  • 图标显示: 如果该 QAction 在属性中设置了 icon(图标),工具栏上默认只会显示图标;如果没有设置图标,则会显示文本(text)。
3. 补充:⚠️ UI 设计器的致命局限(防呆指南):

在 Qt Designer 中,你无法直接将左侧控件箱(Widget Box)里的普通控件(比如搜索框 QLineEdit、下拉菜单 QComboBox)直接拖拽到工具栏中!UI 设计器只支持向工具栏拖入 QAction。如果想在工具栏里嵌入普通控件,必须通过代码实现

4.2.2 通过代码动态添加工具栏

在纯代码环境下构建工具栏,核心逻辑流转分为三大步:

  1. 实例化: new 一个 QToolBar 对象。
  2. 挂载: 调用主窗口(QMainWindow)的方法,明确指明停靠边缘,并将其添加进窗口。
  3. 填充: 调用工具栏(QToolBar)的方法,向其中逐个添加 QAction,或嵌入标准的 QWidget

4.2.3 工具栏核心 API

主要分为主窗口级别和工具栏级别:

1. 挂载工具栏 (QMainWindow 级 API)

// 在主窗口指定的区域添加已创建的工具栏
/*
参数 area 枚举 (Qt::ToolBarArea):
    Qt::LeftToolBarArea   -> 停靠在左侧
    Qt::RightToolBarArea  -> 停靠在右侧
    Qt::TopToolBarArea    -> 停靠在顶部 (默认)
    Qt::BottomToolBarArea -> 停靠在底部
    Qt::AllToolBarAreas   -> 允许停靠在所有区域
*/
void QMainWindow::addToolBar(Qt::ToolBarArea area, QToolBar *toolbar);

// 简易重载:默认添加到顶部 (TopToolBarArea)
void QMainWindow::addToolBar(QToolBar *toolbar);

// 快捷创建:直接传入标题,由系统在底层 new 一个 QToolBar 并默认添加至顶部,返回其指针
QToolBar *QMainWindow::addToolBar(const QString &title);

2. 向工具栏添加具体元素 (QToolBar 级 API)

// 1. 添加已有的 QAction 对象 (通常直接复用菜单栏的 QAction)
void QToolBar::addAction(QAction *action);

// 2. 快捷创建并添加新的 QAction 对象
QAction *QToolBar::addAction(const QString &text);
QAction *QToolBar::addAction(const QIcon &icon, const QString &text);

// 3. 嵌入标准 QWidget 控件 (突破 UI 设计器的限制!)
QAction *QToolBar::addWidget(QWidget *widget);

// 4. 添加垂直/水平的分隔线 (用于视觉分组)
QAction *QToolBar::addSeparator();

4.2.4 工具栏的属性设置与底层机制

在 Qt Designer 的属性面板中,可以直观地修改工具栏的行为限制。在纯代码层面,Qt 遵循 set + 属性名(首字母大写) 的 C++ 驼峰命名规范。

1. 核心行为属性

  • movable(可移动性): 决定用户是否可以用鼠标拖动该工具栏的把手。

    • 对应 API: void QToolBar::setMovable(bool movable);
  • floatable(可悬浮性): 决定工具栏被拖拽出停靠区后,是否能以独立小窗口的形式悬浮在桌面上。

    • 对应 API: void QToolBar::setFloatable(bool floatable);
  • allowedAreas(允许停靠区域): 限制工具栏只能被拖拽到哪几个边缘(使用按位或 | 缝合枚举)。

    • 对应 API: void QToolBar::setAllowedAreas(Qt::ToolBarAreas areas);

2. 💡 核心重点机制

  • QAction 的“一处实例化,多处挂载”联动机制:

    • 使用方式: 通常不会为工具栏单独创建功能重复的 QAction,而是将同一个 QAction 指针复用于菜单栏和工具栏。

    • 底层优势: 此时,菜单和工具栏实现了完美的状态同步。无论是修改图标、禁用功能(setEnabled(false)),还是更改快捷键,多处的 UI 元素都会同时且一致地做出响应。

  • addAction 不转移内存所有权(不接管):

    • 设计逻辑(逻辑共享性): QAction 是抽象的逻辑指令(如“保存”、“复制”),Qt 提倡将其在菜单、工具栏等多个载体中共享。
    • 底层行为: 若工具栏通过 addAction 接管并独占了 QAction 的生命周期,一旦工具栏被销毁,QAction 也会随之释放,将导致菜单栏中的同名动作触发野指针崩溃。因此,addAction 绝不会改变 QAction 的父子关系。
    • 内存规范: 动态创建的 QAction 必须手动指定父对象(通常为 this),将其挂载到主窗口的对象树上。否则工具栏销毁时它不会被释放,必然导致内存泄漏。
  • addWidget 的内存所有权转移(强制接管):

    • 设计逻辑(物理排他性): QWidget(如按钮、搜索框)是物理 UI 元素,同一时刻只能归属一个容器。
    • 底层行为: 调用 QToolBar::addWidget(QWidget *widget) 时,Qt 底层会自动对传入的控件执行 setParent()。这意味着工具栏强制接管了该控件的生命周期
    • 工程禁忌: 传入的控件必须是分配在堆区(new 出来)的对象。严禁传入局部栈对象,以防函数出栈时系统自动析构局部变量,导致界面元素异常消失或引发野指针崩溃。

4.2.5 示例代码

前置条件: 在 UI 中预先创建工具栏 toolBar1,并包含子菜单项 saber_actiondxl_action

mainwindow.cpp 的构造函数:

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // ================= 一、混合构建:UI 界面与代码结合 =================
    // 1. 注册 UI 已有 Action 的信号槽
    connect(ui->saber_action, &QAction::triggered, this, [=]() {
        QMessageBox::information(this, "saber info", "saber: EX 咖喱棒!!!");
    });

    connect(ui->dxl_action, &QAction::triggered, this, [=]() {
        QMessageBox::warning(this, "冬雪莲", "骂谁罕见!啊!骂谁罕见!!!");
    });

    // 2. 突破 UI 限制:通过代码向工具栏注入 QWidget
    QLineEdit* edit = new QLineEdit(this); // 推荐规范:显式传入 this 是一种良好的防御性编程习惯
    edit->setFixedWidth(100);              // 设置固定宽度
    ui->toolBar1->addWidget(edit);         // 此时 toolBar1 强制接管 edit 的生命周期
    
    ui->toolBar1->addWidget(new QPushButton("搜索"));


    // ================= 二、纯代码动态构建 =================
    // 1. 实例化新工具栏并挂载到主窗口左侧
    QToolBar* toolBar2 = new QToolBar("toolBar2", this); // 规范:挂在主窗口对象树上
    this->addToolBar(Qt::LeftToolBarArea, toolBar2);

    // 2. 内存防呆规范应用
    // - QIcon 是隐式共享的轻量级数据类,直接传值即可,切勿 new 到堆区。
    // - QAction 必须传入 this 作为父对象,防止脱离对象树造成内存泄漏。
    QAction* sakuyaAction = new QAction(QIcon("D:/tmp/img/sakuya.png"), "十六夜咲夜", this);
    toolBar2->addAction(sakuyaAction);

    connect(sakuyaAction, &QAction::triggered, this, [=]() {
        QMessageBox::warning(this, "咲夜", "砸!瓦鲁多!时间停止吧!!!");
    });
}

4.3 状态栏 (QStatusBar)

状态栏(QStatusBar)固定驻扎在 QMainWindow 窗口的最底部,通常用于输出简短的系统状态信息、非侵入式的操作反馈,或放置极少的辅助小控件。

在一个 QMainWindow 中,最多只能存在 1 个状态栏实例。

4.3.1 核心机制:消息分级与排版逻辑

Qt 将状态栏区域划分为三种不同级别的消息/控件展示模式。理解它们的优先级与底层的排版插入机制,是精确控制状态栏界面的大前提。

消息类型 行为本质 视觉排版顺序与位置 遮挡关系与生命周期
临时消息 (Temporary) 独占式显示 覆盖整个状态栏左侧区域 优先级最高。弹出时强制隐藏“正常控件”。
构造函数未构造完成:临时消息消失后“正常控件”不会自动出现。
正常控件 (Normal) 居左排列序列 靠左区域。底层维护一个独立索引。可追加队尾,也可插队到任意中间位置。 优先级较低。弹出临时消息时会被完全遮挡。
永久控件 (Permanent) 居右排列序列 靠右区域。底层维护另一个独立索引。可追加队尾,也可在右侧组内任意插队。 独立显示层级。永远不会被临时消息遮挡,始终显示在窗口最右侧。

整体控件渲染的视觉顺序结构:

[正常0] [正常1] [正常2] ... (动态弹性的空白伸缩区) ... [永久0] [永久1] [永久2]

4.3.2 常用 API:追加与插入

1. 临时消息控制
// 在状态栏显示临时文本消息
/*
参数:
  - message: 要显示的字符串。
  - timeout: 消息驻留的毫秒数。传入 0(默认值)表示该消息会一直显示。传入 3000 则 3 秒后自动消失。
*/
[slot] void QStatusBar::showMessage(const QString &message, int timeout = 0);

// 手动强制清除当前的临时消息
[slot] void QStatusBar::clearMessage();
2. 正常控件组 API (居左序列)
// 【追加操作 / push_back】:永远将控件添加在当前“正常控件组”的最右侧末尾。
void QStatusBar::addWidget(QWidget *widget, int stretch = 0);

// 【插队操作 / insert】:将控件精准插入到指定的 index 索引位。
void QStatusBar::insertWidget(int index, QWidget *widget, int stretch = 0);

核心参数详解:

  • stretch (伸缩系数): 决定当用户拉伸主窗口变宽时,控件如何瓜分状态栏中间的“空白伸缩区”。
    • stretch = 0 (默认): 佛系占位。控件只占用刚好够用的最小必要宽度,窗口再宽也不碰多余空白(适用于文字标签、按钮)。
    • stretch > 0 (弹性霸占): 控件变为弹性体。底层会把状态栏剩余的空白空间,按所有弹性控件的 stretch 数字比例强制分配给它们。数值越大,拉伸时变宽的速度越快、抢占的空间越多(极度适用于动态拉长的 QProgressBar 进度条,或需展示长路径的文本框)。
  • index (插入索引): 仅对 insert 系列有效。传入 0 表示强制插队到该组别(正常组或永久组)的最左侧绝对队首。传入超出元素总数的值,自动退化为追加到队尾。
3. 永久控件组 API (居右序列)
// 【追加操作】:永远将控件添加在当前“永久控件组”的最右侧末尾。
void QStatusBar::addPermanentWidget(QWidget *widget, int stretch = 0);

// 【插队操作】:在永久控件组的内部序列中插队。
void QStatusBar::insertPermanentWidget(int index, QWidget *widget, int stretch = 0);

4.3.3 工程防呆与避坑指南

  1. 构造函数中临时消息导致正常控件永久消失的生命周期陷阱

    • 现象分析: 若在 QMainWindow 的构造函数中先调用 addWidget 添加控件,紧接着调用 showMessage("...", 3000),由于此时主窗口尚未执行 show() 进行屏幕渲染,底层会将正常控件的可见状态记录为 hidden。当临时消息超时消失时,底层恢复的也是初始的 hidden 状态,导致正常控件永久不可见。

    • 工程解决方案: 避免在构造函数中直接触发 showMessage。可使用 QTimer::singleShot 进行延时触发,确保窗口及内部控件完全渲染并确认为可见状态后,再显示临时消息。。

  2. QAction 不能加到状态栏里

    • 现象分析: QStatusBar 继承自 QWidget,直接调用 ui->statusbar->addAction(action) 不会引发编译错误,但状态栏底层未重写相关的 UI 渲染逻辑。因此,该 QAction 仅会被隐式注册为后台快捷键,界面上不会显示任何按钮或图标。
    • 标准解法: 必须在堆区实例化一个 QToolButton,将其与 QAction 绑定(setDefaultAction),最后把这个实体按钮塞进状态栏。

4.3.4 示例代码

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
// ==========================================
// 1. 正常控件排版与 stretch 伸缩系数测试
// ==========================================
    // stretch默认为0
    QLabel* labelA = new QLabel("[系统前置模块]", this);
    labelA->setStyleSheet("background-color: red"); // QWidget方法
    ui->statusbar->addWidget(labelA, 0);

    // stretch = 1: 弹性控件,拉伸主窗口时会自动膨胀变宽以填充剩余空间
    QLabel* dynamicLabel = new QLabel("当前运行目录:C/Program Files/test", this);
    dynamicLabel->setStyleSheet("background-color: lightblue"); // QWidget方法
    ui->statusbar->addWidget(dynamicLabel, 1);

// ==========================================
// 2. QAction 植入状态栏的工程解决方案
// ==========================================
    QAction* sxcAction = new QAction(QIcon("D:/tmp/img/sxc.png"), "咲川", this);
    connect(sxcAction, &QAction::triggered, this, [=]()
    {
        QMessageBox::warning(this, "天皇怒吼", "那你去找物管啊!你再骂!!!");
    });

    // 实例化 QToolButton 作为 QAction 的物理渲染载体
    QToolButton* actionBtn = new QToolButton(this);
    actionBtn->setDefaultAction(sxcAction);
    ui->statusbar->addWidget(actionBtn);

// ==========================================
// 3. 永久控件 (Permanent Widgets)
// ==========================================
    QLabel* versionLabel = new QLabel("Version: bk-2.0.1", this);

    // 永久控件默认靠右,绝对不会被showMessage遮挡
    ui->statusbar->addPermanentWidget(versionLabel);


// ==========================================
// 4. 解决“控件无法自动恢复”的时序陷阱
// ==========================================
    // 利用单次定时器延时 5000 毫秒。
    // 确保窗口及内部控件已完全 show 出来并确认可见状态后,再触发临时消息。
    QTimer::singleShot(5000, this, [=]()
    {
        // 弹出 3 秒的临时消息,会覆盖左侧的 labelA、dynamicLabel 和 actionBtn
        // 3 秒后,由于时序安全,被覆盖的左侧控件会自动恢复显示
        ui->statusbar->showMessage("系统初始化完成,正在连接数据库...", 3000);
    });
}

4.4 停靠窗口 (QDockWidget)

停靠窗口(QDockWidget)是一种高交互性的二级窗口容器。其核心特性在于支持动态交互:既能吸附停靠在主窗口的边缘区域,也能通过鼠标拖拽或双击标题栏脱离主窗口,转换为独立悬浮于桌面之上的浮动窗口。

QMainWindow 架构中,停靠窗口的设计规则与数量限制如下:

  • 数量限制:可以存在多个($n$ 个),默认不初始化提供。
  • 空间分布:围绕核心中央部件(Central Widget),可多层并行或重叠停靠于上、下、左、右四个特定的停靠区域(Qt::DockWidgetArea)。

4.4.1 通过UI设计器添加停靠窗口

在 Qt Designer 中,管理停靠窗口必须严格遵循其特有的“框架嵌套”层级结构:

  1. 组件置入

    在左侧工具箱的 Containers(容器) 分组中选中 Dock Widget,拖拽至主窗口边缘。当界面呈现蓝色虚线指示框时松开鼠标,即可完成物理吸附。

  2. 理解视口层级(工程核心)

    审视右侧对象树(Object Inspector),QDockWidget 的实例化并不是一个空壳,其底层默认包含一个中间承载层:

    QDockWidget (外壳框架) -> QWidget (对象名通常为 dockWidgetContents) -> 实际业务控件

    • 规范实践:外层的 QDockWidget 仅负责提供标题栏、拖拽把手及状态按钮。若需添加具体的业务控件(如按钮、文本框、表格),必须精准将其拖入内部的 dockWidgetContents 视口中
  3. 交互属性配置

    选中外层 QDockWidget,在右侧属性面板中可以直接勾选或取消 features(交互特性)和 allowedAreas(允许停靠区域),实现免代码的交互限制。

4.4.2 通过代码动态添加停靠窗口+设置停靠属性

在纯代码环境下,构建停靠窗口的标准工程流转逻辑为:实例化外壳 -> 配置外壳行为权限 -> 动态创建内部业务组件(中间QWidget+具体实现的组件) -> 外壳接管组件所有权 -> 将外壳挂载至主窗口

1. 挂载停靠窗口(QMainWindow 级 API)
void QMainWindow::addDockWidget(Qt::DockWidgetArea area, QDockWidget *dockwidget)
  • 功能:将初始化好的停靠窗口挂载到主窗口指定的初始边缘区。
  • 参数 area 枚举值 (Qt::DockWidgetArea)
    • Qt::LeftDockWidgetArea -> 停靠在核心区左侧
    • Qt::RightDockWidgetArea -> 停靠在核心区右侧
    • Qt::TopDockWidgetArea -> 停靠在核心区顶部
    • Qt::BottomDockWidgetArea -> 停靠在核心区底部
2. 配置停靠行为特性(QDockWidget 级 API)
void QDockWidget::setFeatures(QDockWidget::DockWidgetFeatures features)
  • 功能:控制停靠窗口在界面上的交互权限(是否允许关闭、移动、悬浮)。
  • 参数 features:支持使用按位或 | 缝合以下枚举值:
    • QDockWidget::DockWidgetClosable -> 允许关闭(右上角渲染出 [X] 按钮)。
    • QDockWidget::DockWidgetMovable -> 允许拖动(渲染出双排点状的拖拽把手)。
    • QDockWidget::DockWidgetFloatable -> 允许悬浮(可脱离主窗口在桌面任意漂浮)。
    • QDockWidget::VerticalTitleBar -> 奇葩外观:将标题栏旋转 90 度,改为在左侧垂直显示。
    • QDockWidget::NoDockWidgetFeatures-> 全面封印:窗口不可关闭、不可移动、不可悬浮,死死锁在原地。
3. 限制允许停靠的区域(QDockWidget 级 API)
void QDockWidget::setAllowedAreas(Qt::DockWidgetAreas areas)
  • 功能:规约窗口的乱窜行为。例如,限制某些窗口只能在左右停靠,禁止其污染上下布局。
  • 参数 areas:同样支持 | 缝合,如 Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea

4.4.3 核心内部机制与其他属性设置

1. 物理排他性与 setWidget() 的内存接管机制

停靠窗口装载内部界面通过以下 API 实现:

void QDockWidget::setWidget(QWidget *widget)
  • 设计逻辑(物理排他性)QDockWidget 的中心区域拥有绝对的独裁性,在同一时序下有且仅能容纳 1 个核心控件

  • 覆盖惩罚:在代码中若连续对同一个 QDockWidget调用两次 setWidget(),后置控件会无情解除前置控件的父子关系并将其物理隐匿,界面将有且仅显示最后一次设置的对象。

  • 底层行为与生命周期管理:当执行 setWidget(widget) 时,底层的 Qt 框架会自动触发 widget->setParent(当前QDockWidget)。至此,停靠窗口强制接管该控件的内存生命周期。外壳析构时,内部控件会自动挂钩销毁。

  • 架构范式(多控件容纳方案):若业务需求在停靠窗口中塞入多个控件,必须通过“大箱子套小盒子”的套娃哲学:先 new 一个空白的基础 QWidget 作为“核心大容器”交付给 setWidget(),后续所有业务子控件统统指定该“大容器”为父对象。

2. 状态获取属性(实时监测)
bool QDockWidget::isFloating() const;
  • 功能:实时获取当前停靠窗口的物理视觉状态,用于做动态业务逻辑判定。
  • 返回值:返回 true 代表窗口当前处于脱离主窗口的桌面悬浮状态;返回 false 代表窗口正吸附停靠在 QMainWindow 的物理边缘。
3. 核心联动信号(状态切换监听)
[signal] void QDockWidget::topLevelChanged(bool topLevel);
  • 功能:当停靠窗口在“物理吸附”与“外置悬浮”状态之间发生显式切换时,由底层布局管理器触发该信号。
  • 参数 topLevel:同步反馈当前的悬浮状态(等价于切换后的 isFloating() 逻辑值)。
    • 当窗口变为悬浮时,传入 true;重新吸附回主窗口边缘时,传入 false
  • 工程价值:这是实现高级 UI 联动的桥梁。比如当窗口脱离主窗口悬浮时,可以通过此信号触发槽函数,动态修改状态栏的文字提示,或者调整主窗口中心部件的渲染策略。

4.4.4 工程防呆与避坑指南

1. 非 QMainWindow 承载导致的特性退化
  • 现象分析:若在普通 QWidgetQDialog 窗口中硬塞一个 QDockWidget,编译可通过且能正常渲染。然而,该窗口将丧失移动、悬浮、磁铁吸附等所有高级动态交互能力,退化为一个死板的嵌入式硬方块。
  • 底层原因:QDockWidget 的拖拽把手及磁铁吸附特性,物理上 100% 依赖于 QMainWindow 内部独有的高级中央布局管理器,普通窗口不具备对接此类高级行为的底层接口。
2. 缺失 Central Widget 导致的核心区布局暴走
  • 现象分析:如果在 QMainWindow 中,你还没有调用 setCentralWidget() 指定中心部件,就直接调用 addDockWidget() 挂载了停靠窗口。程序跑起来后,该停靠窗口会因为找不到“中心参考物”而强行暴力霸占整个窗口的中央核心区域。此时它的尺寸会异常放大,且失去正常的拖拽边界。
  • 防呆规范:任何主窗口项目,必须雷打不动地先初始化 Central Widget,再挂载停靠窗口等外围组件。
3. setWidget 塞入局部栈对象的野指针崩溃**
  • 现象分析:由于 setWidget() 涉及内存所有权接管,传入的控件必须是在堆区(new 出来)的对象。严禁传入局部栈对象。
  • 错误示范QWidget localWidget; dock->setWidget(&localWidget);
  • 后果:初始化函数一旦执行结束,局部变量 localWidget 自动出栈析构,而 QDockWidget 还在试图持有它,界面会瞬间变白、变瞎,并在下一次刷新时引发野指针内存崩溃。

4.4.5 示例代码:纯 C++ 代码动态构建停靠窗口

前置条件:主窗口已正常通过 UI 设计器初始化中心部件(Central Widget)。

mainwindow.cpp 构造函数:

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    // 1. 实例化外壳对象,并指定父对象this
    QDockWidget* bocchiDock = new QDockWidget("后藤一里的避难纸箱(QDockWidget)", this);

    // 2. 设置交互行为:可关闭,可移动,可浮动
    bocchiDock->setFeatures(QDockWidget::DockWidgetClosable |
                            QDockWidget::DockWidgetMovable |
                            QDockWidget::DockWidgetFloatable);

    // 3. 设置停靠吸附区域:左侧和右侧
    bocchiDock->setAllowedAreas(Qt::LeftDockWidgetArea | Qt::RightDockWidgetArea);

    // 4. 构建内部业务界面容器(内存防呆:必须堆区 new 出来)
    // 4.1 中间的 dockContents 负责提供一个内部的画布/容器,从而可以添加多个控件
    QWidget* dockContents = new QWidget(bocchiDock);
    // 4.2 垂直布局管理器,不是一个可视化的控件,仅用于管理Widget布局
    QVBoxLayout* layout = new QVBoxLayout(dockContents);
    // 4.3 实现具体操作的按钮控件,并绑定父对象为dockContents
    QPushButton* panicBtn = new QPushButton("触发社交恐惧症", dockContents);
    layout->addWidget(panicBtn);
    dockContents->setLayout(layout); // 将布局物理绑定到容器上

    // 5. 核心关键点:外壳强制接管内部实体界面的内存所有权 (底层执行 setParent)
    bocchiDock->setWidget(dockContents);

    // 6. 将配置完成的停靠窗口挂载到主窗口的左侧停靠区
    this->addDockWidget(Qt::LeftDockWidgetArea, bocchiDock);

    // 剩余部分:注册信号槽实现功能
    connect(panicBtn, &QPushButton::clicked, this, [=]()
    {
        // 实时检测当前停靠窗口的状态
        if(bocchiDock->isFloating())
            QMessageBox::critical(this, "社恐发作",
                                  "波奇酱:外面的人类太可怕了!不要让我悬浮在外面啊啊啊!让我塞回主窗口里去!");
        else
            QMessageBox::information(this, "纸箱安全",
                                     "波奇酱:呼...吸附在边上感觉很安心。只要我不主动悬浮出去,就没有人能注意到我...");
    });
}

5. 资源管理系统(.qrc)

5.1 资源管理系统原理和底层机制

1. 什么是 .qrc 资源系统

Qt 资源系统(Qt Resource System)是一种与平台无关的资源存储机制,用于将应用程序依赖的二进制资产(如图片、音频、翻译文件等)直接嵌入到最终生成的可执行程序(.exe)中。

2. 底层编译机制(rcc)

资源文件的工作原理并非在运行时动态读取外部磁盘文件,而是分为以下三个构建阶段:

  1. 构建期解析:项目编译时,Qt 的资源编译器(rcc)会解析 .qrc 文件(本质是 XML 格式的配置文件)。
  2. 转换为 C++ 数组rcc 会将所有引入的媒体资产转化为包含原始十六进制字节数据的 C++ 源代码文件(例如 qrc_res.cpp)。
  3. 集成到二进制段:该 C++ 文件随项目一同被标准编译器编译,资产数据作为静态数据区的一部分直接打包进可执行文件的只读数据段(.rodata 段)。

3. 应用边界与内存开销

  • 核心优势:实现零依赖部署。资产与可执行文件一体化分发,彻底杜绝了因用户误删或工作路径变更导致的资源加载失败(File Not Found)异常。
  • 物理局限:由于资源被转化为静态数组,程序启动时这些数据会随程序一并载入虚拟内存空间。若引入过大的视频或数据库文件,会导致编译期极度缓慢,且程序运行时常驻内存(RSS)暴涨。
  • 适用场景:仅适用于图标(.ico)、轻量级图片(.png/.jpg)、短音效(.wav)、静态配置文件(.json/.ini)等小体积资产。

5.2 资源文件的创建与管理流程

Step0. 提前物理复制(前置条件)

  1. 目录规划:在项目目录(即 .pro.qrc 文件所在同级目录)下手动新建资产文件夹(规范命名为 imagesres)。
    • 路径约束:从语法上讲,资源可以直接裸放在项目根目录下,但工程实践中必须通过子文件夹进行分类。所有资产的物理路径必须位于 .qrc 文件的当前目录或其子目录之内。
  2. 物理复制:将所需的物理资产(如 Reimu.png)手动复制到该文件夹中。

路径约束:物理资源文件必须存放在 .qrc / .pro 文件的当前目录或其子目录之下。

Step1. 创建资源配置文件

  1. 在 Qt Creator 工程树中,右键点击项目根目录 -> 选择 "Add New..."
  2. 选择 "Qt" 分类 -> 选择 "Qt Resource File" -> 点击 "Choose..."
  3. 输入资源文件名(例如 res),确认路径位于 .pro 同级或下级,完成向导。

Step2. 配置资源编辑器

创建完成后,使用 Qt Creator 自带的资源编辑器打开生成的 res.qrc 文件:

  1. 点击 "Add" -> 选择 "Add Prefix"

    • 前缀的本质:前缀是纯粹的虚拟路径,它在操作系统的物理磁盘上不存在任何真实的文件夹。前缀的作用是在 Qt 的内存资源树中开辟一个虚拟的路由节点(类似于 C++ 中的 namespace),无需创建实际路径文件夹。

    • 虚拟根目录(/)的约束依据:在资源配置中,前缀必须以 / 开头。这是因为 Qt 在内部构建了一个独立的、类似于 UNIX 的虚拟文件系统。

      • / 在该系统中代表虚拟根目录。前缀以 / 开头,意味着在虚拟系统的根节点下建立分支。
      • 在代码中通过冒号 : 激活资源寻址时(如 :/Reimu.png),后面的 / 就是该虚拟根目录的映射。这一机制与项目工程文件(.pro)在磁盘上的物理位置完全脱钩。
    • 多级虚拟目录的行为表现:如果在资源编辑器中将前缀设置为多级结构(例如 /Touhou/img):

      • 磁盘物理层面

        项目所在的目标磁盘上不会自动创建名为 Touhouimg 的真实文件夹,也不需要创建真实文件夹。

        虚拟目录只是代码中的“路由地址”,它与磁盘上的物理目录没有任何映射关系。

      • 虚拟系统层面

        Qt 会在内存树中自动构建出两级虚拟目录。此时,若想在代码中访问该前缀下的资产(如 Reimu.png),必须使用完整的虚拟路径:":/Touhou/img/Reimu.png"

  2. 选中对应前缀 -> 点击 "Add" -> 选择 "Add Files" -> 在弹出的文件选择器中选中所需的资产(如 Reimu.png)。

    资源配置系统要求:

    被引入的物理资产,其所在的磁盘路径必须位于 .qrc 文件当前目录或其子目录之内。 通常由于 .qrc.pro 文件处于同一层级,因此也可以说资产必须位于 .pro 文件的同级目录或更深层的子目录中。

Step3. 别名设置(Alias)

应用场景:当外部文件名由于迭代发生变更,或者名字过长时,可以在编辑器属性栏为其设置 Alias(别名)。

  • 无别名时的路径公式

    • 冒号: + 资源前缀 + 物理的相对路径 + 实际文件名(带后缀)
  • 有别名时的路径公式

    • 冒号: + 资源前缀 + 别名(不带后缀)
  • 注意事项:一旦配置了别名,该资产在虚拟系统内的原文件名(通常是物理相对路径 + 实际文件名)将彻底失效,后续 C++ 代码中必须使用别名进行寻址访问。

5.3 构建约束与核心注意事项

**1. **字符集编码与编译风险

  • 严禁使用中文命名.qrc 文件名、虚拟前缀、资产物理文件名以及别名中,切勿包含任何中文字符
  • 编译报错原因:在高版本 Qt Creator 与特定底层编译器(如 MSVC)配合时,中文字符会导致 rcc 转换为 C++ 源代码时出现字符集编码解析断裂,直接引发编译期的语法错误。

2. 物理资产路径的跨目录与跨盘符限制

资源配置系统对引入的物理资产有着严格的工程约束。虽然底层编译器对同盘符下的外部文件有不同的处理机制,但工程标准要求资产必须存放在项目目录之内

  • 跨盘符引入(硬性编译失败):若项目在 D 盘,尝试引入 C 盘的资产,构建系统会直接报错。技术原因在于 rcc 编译器在 .qrc 中必须记录相对路径,而 Windows 操作系统下无法计算跨盘符的相对路径。
  • 同盘符但项目外引入(工程级隐患/反模式):若资产与项目在同一盘符但相互独立(例如项目在 D:/Proj,图片在 D:/Pics),rcc 虽然能通过 ../ 向上寻址的方式计算出相对路径并完成编译,但严禁在工程中使用
    • 原因一(破坏封装性):一旦项目文件夹的分发层级发生改变(例如改变了父目录深度),相对寻址链条即刻断裂。
    • 原因二(破坏可移植性):代码一旦迁移到其他开发人员的主机上,由于对方缺失项目外部的特定磁盘目录,构建系统将直接瘫痪。

5.4 虚拟映射关系示例表与示例代码

5.4.1虚拟映射关系示例表

物理磁盘路径(真实存在) 资源前缀(虚拟) 别名设置(Alias) 代码中的访问路径(虚拟)
D:/Project/images/Reimu.png / 未设置(默认) :/Reimu.png
D:/Project/images/Reimu.png /img 未设置(默认) :/img/Reimu.png
D:/Project/Reimu.png /Touhou/img 未设置(默认) :/Touhou/img/Reimu.png
D:/Project/Reimu.png /Touhou/img main_character :/Touhou/img/main_character

5.4.2 示例代码

资源管理文件如图所示:

image

mainwindow.cpp构造函数:

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);

    this->resize(1600, 1000);
// =========================================================================
// 示例 1:使用多级虚拟前缀 + 原文件名
// =========================================================================
    QPushButton* reimuBtn = new QPushButton(QIcon(":/Touhou/img/img/Reimu.jpg"), "博丽灵梦", this);
    reimuBtn->resize(150, 100);

    connect(reimuBtn, &QPushButton::clicked, this, [=]()
    {
        QMessageBox::information(this, "博丽神社", "又是异变吗?真是麻烦……赶紧解决完回去喝茶。\n\n对了,参拜的话记得往赛钱箱里投钱!");
    });

// =========================================================================
// 示例 2:使用多级虚拟前缀 + 别名机制
// =========================================================================
    QPushButton* sxcBtn = new QPushButton(QIcon(":/Tianhuang"), "咲川天皇", this);
    sxcBtn->resize(150, 100);
    sxcBtn->move(0, 300);

    connect(sxcBtn, &QPushButton::clicked, this, [=]()
    {
        QMessageBox::critical(this, "天皇怒吼", "你态度能不能好点嘛!你再骂!!!");
    });
}
posted @ 2026-05-25 22:01  学习笔记草稿存放账号  阅读(19)  评论(0)    收藏  举报