Qt按钮类03 QToolButton——大丙+Gemini

Qt按钮类03 QToolButton——大丙+Gemini

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

QToolButton 是 Qt 框架中专为工具栏(QToolBar)和快捷操作面板设计的高阶命令按钮控件。虽然它与 QPushButton 同属于基类 QAbstractButton 的子类,但在外观渲染机制、图文拓扑排版、内存模型以及级联菜单的托管控制上存在本质差异。

级联菜单其实就是“子菜单”(Submenu)。当点击或把鼠标悬停在某个菜单项上时,该菜单项旁边会弹出一个新的菜单,这就是级联。

1. QPushButton 与 QToolButton 对比

				 ┌───────────────┐
                 │    QWidget    │
                 └───────┬───────┘
                         │
                 ┌───────┴───────┐
                 │QAbstractButton│
                 └───────┬───────┘
                         │
         ┌───────────────┴───────────────┐
         │                               │
 ┌───────┴───────┐               ┌───────┴───────┐
 │ QPushButton   │               │  QToolButton  │
 └───────────────┘               └───────────────┘
 (标准重量级命令按钮)             (增强型高阶工具按钮)
核心技术维度 QPushButton (标准命令按钮) QToolButton (高阶工具按钮)
空间占用与外观表现 重量级控件。具备固定的边框边距(Margin/Padding),默认占用较大的视觉空间。 轻量级控件。天然支持自动悬浮属性(Auto-Raise),未触发时背景透明,极易紧凑地嵌入工具栏。
图文拓扑排版能力 仅支持基础的水平排版(文字在图标右侧)。若要实现“文字在图标下方”,必须编写 QSS 样式表或重写 paintEvent 原生内置 Qt::ToolButtonStyle 状态机。无需任何附加代码,即可自由实现文字在图标下方、文字在图标右侧、纯文字、纯图标等全方位排版。
对象树生命周期模型 调用 setMenu() 后,按钮底层会自动托管该菜单的所有权(改变其 parent 并挂载到对象树上)。 调用 setMenu() 后,按钮完全不改变该菜单的所有权(不挂载对象树),必须由开发者显式指定父对象。
级联菜单控制流 菜单弹出模式固定唯一。点击按钮即以模态形式弹窗,且强行使常规的 clicked() 信号失效 内置三大 ToolButtonPopupMode 策略。支持延时长按弹出、主体与箭头区分裂弹出、即时弹出,信号控制自由度极高。

2. 核心 API 与属性控制

QToolButton 操作的大部分基础函数(如 setText(), setIcon())均从父类 QAbstractButton 继承,自身则通过以下高阶 API 扩展了图文排版与视觉展现控制。

2.1 构造函数

// 工具按钮通常直接嵌入工具栏或通过布局管理器托管,因此构造函数仅提供父对象接口
explicit QToolButton(QWidget *parent = nullptr);

2.2 图文外观布局控制(ToolButtonStyle)

图标/文本布局

// [槽函数] 控制按钮内部图标与文本的相对排版拓扑关系
[slot] void setToolButtonStyle(Qt::ToolButtonStyle style);

// 返回当前工具按钮的图标显示模式
Qt::ToolButtonStyle toolButtonStyle() const;

布局样式枚举值 Qt::ToolButtonStyle 说明:

  • Qt::ToolButtonIconOnly:单图标模式。仅展示图标,彻底隐藏文本(默认状态)。
  • Qt::ToolButtonTextOnly:单文本模式。仅展示文本,彻底隐藏图标。
  • Qt::ToolButtonTextBesideIcon:水平并排模式。文本展示在图标的正右侧。
  • Qt::ToolButtonTextUnderIcon:垂直并列模式。文本展示在图标的正下方(常用于功能区大图标按钮)。
  • Qt::ToolButtonFollowStyle:跟随全局或宿主工具栏的默认样式。

2.3 自动悬浮属性(Auto-Raise)

// 配置工具按钮是否启用自动浮起属性
void setAutoRaise(bool enable);

// 获取当前按钮是否处于自动浮起状态
bool autoRaise() const;
  • 运行机理:当设置为 true 时,该按钮在初始状态下将处于“无边框且背景完全透明”的扁平状态。只有当鼠标光标悬停(Hover)在按钮区域上方时,系统才会动态渲染出激活的边框与背景,用以大幅降低密集工具栏的视觉疲劳度。

3. 按钮设置图标与文本的三种方式

3.1 方式一:基础多态配置(来自 QAbstractButton 继承)

normalBtn->setText("博丽灵梦");
normalBtn->setIcon(QIcon(":/imgs/reimu.png"));
normalBtn->setIconSize(QSize(50, 50));
normalBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);

3.2 方式二:高内聚动作关联(DefaultAction 机制)

在大型工程中,同一个操作(如“保存”)常共存于菜单、右键、快捷栏。通过引入 QAction 进行一体化联动:

[slot] void QToolButton::setDefaultAction(QAction *action);
  • 技术优势:工具按钮会自动拉取(Pull)该 Action 的文本、图标、自锁状态机(Checkable),并且将触发信号自动转接。
  • 信号重定向:绑定 DefaultAction 后,按钮的点击拦截应当从传统的 clicked() 信号转移到系统的 triggered(QAction*) 信号或直接在 QAction 内部拦截。
QAction* act = new QAction(normalBtn);
act->setIcon(QIcon(":/imgs/remilia.png"));
act->setText("蕾米莉亚·斯卡雷特");

normalBtn->setDefaultAction(act);
normalBtn->setIconSize(QSize(300, 300));
normalBtn->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);

3.3 方式三:内建矢量箭头直接渲染(ArrowType 机制)

无需任何外部图标素材,直接调用系统图形库在按钮表面绘制抗锯齿的矢量箭头(一旦启用,原设定的 QIcon 图标将被强行覆盖):

void setArrowType(Qt::ArrowType type);
箭头枚举值 Qt::ArrowType
  • Qt::NoArrow:无箭头(默认状态)。
  • Qt::UpArrow / Qt::DownArrow:自适应绘制向上 / 向下的矢量箭头。
  • Qt::LeftArrow / Qt::RightArrow:自适应绘制向左 / 向右的矢量箭头。

4. 级联菜单管理与三种弹出方式判定

QToolButton 允许通过 setPopupMode 函数精细化控制级联菜单的触发时机。这是它与 QPushButton 在控制流上的核心差异。

4.1 菜单配置函数

// 将给定的菜单与此工具按钮相关联。
void setMenu(QMenu *menu);

4.2 弹出模式控制

// 设置弹出菜单的触发行为模式
void setPopupMode(QToolButton::ToolButtonPopupMode mode);

QToolButton::ToolButtonPopupMode 枚举定义了以下三种互斥的控制流向:

  1. QToolButton::DelayedPopup(延时弹出模式)
    • 执行流表现:常规短促点击按钮,菜单不会弹出,系统正常发射 clicked() 信号。只有当用户按下鼠标并长按一段时间后,内置的定时器超时,关联的菜单才会呼出。
    • 核心细节:长按触发菜单弹出的那一刻,按钮本身的 clicked() 信号将被拦截屏蔽,不会发射
  2. QToolButton::MenuButtonPopup(分裂箭头模式)
    • 执行流表现:按钮的物理表面会被划分成两个完全独立的交互区域。左侧为主体点击区,右侧会额外渲染出一个内建的下拉小箭头
    • 核心细节:两条执行流互不干扰。点击左侧主体区域,只发射 clicked() 信号,绝对不弹出菜单;点击右侧小箭头,只弹出菜单,绝对不发射 clicked() 信号
  3. QToolButton::InstantPopup(即时弹出模式)
    • 执行流表现:按钮的常规点击动作被底层完全重定向。无论是轻点还是长按按钮的任意几何表面,菜单都会立刻、无延时地模态弹出
    • 核心细节:在这种模式下,按钮自身的常规点击响应被无条件剥夺,clicked() 信号彻底失效,永远不会被发射

5. 示例代码

mainwindow.cpp构造函数:

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    // 配置顶级视窗物理尺寸
    this->resize(1600, 1000);

    // 纯代码架构:手动构建中心承载组件
    QWidget* centralWidget = new QWidget(this);
    this->setCentralWidget(centralWidget);

    // 实例化全局水平布局管理器
    QHBoxLayout* centralLayout = new QHBoxLayout(centralWidget);

// =========================================================================
// 1. 双稳态自锁状态机(setCheckable)与基本样式
// =========================================================================
    // 1.1 灵梦按钮:按下塞钱,按起来没有钱
    // 规范:即使后续会加入布局,纯代码下显式指定父对象或在布局后交接是好习惯
    QToolButton* reimuBtn = new QToolButton(this);
    reimuBtn->setText("博丽灵梦");
    reimuBtn->setIcon(QIcon(":/touhou/reimu.png"));
    reimuBtn->setIconSize(QSize(100, 100));
    reimuBtn->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); // 文字在图标下方
    reimuBtn->setCheckable(true); // 开启双稳态

    // 现代 C++ 规范:显式捕获 this,避免 C++20 中 [=] 隐式捕获 this 的警告
    connect(reimuBtn, &QToolButton::toggled, this, [this](bool checked)
    {
        if(checked) qDebug() << "【博丽神社】: 塞钱箱收到香油钱!灵梦正在认真退治妖怪中...";
        else qDebug() << "【博丽神社】: 没有香油钱,灵梦继续喝茶无视异变。";
    });

    // 1.2 魔理沙按钮:按下DAZE
    QToolButton* marisaBtn = new QToolButton(this);
    marisaBtn->setText("雾雨魔理沙");
    marisaBtn->setIcon(QIcon(":/touhou/marisa.png"));
    marisaBtn->setIconSize(QSize(100, 100));
    marisaBtn->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); // 文字在图标右侧

    connect(marisaBtn, &QToolButton::clicked, this, [this]()
    {
        qDebug() << "【魔理沙】: Master Spark 全功率释放 DA☆ZE!";
    });

// =========================================================================
// 2. DefaultAction 联动托管机制(Action 状态同步驱动按钮外观)
// =========================================================================
    // 2.1 妖梦按钮:全面托管给 QAction
    QToolButton* youmuBtn = new QToolButton();
    youmuBtn->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);

    // 实例化全局动作流,指定默认按钮文本,显式挂载到 this 上防泄漏
    QAction* actYoumu = new QAction(QIcon(":/touhou/youmu.png"), "执行冥界庭院修剪任务", this);
    actYoumu->setCheckable(true); // 让 Action 本身具备双稳态自锁属性,而不是ToolButton本身

    // 【核心订正】:通过 setDefaultAction 托管后,按钮的外观、状态完全同步该 Action
    youmuBtn->setDefaultAction(actYoumu);
    youmuBtn->setIconSize(QSize(100, 100)); // 注意:Size 仍需由 Button 自身控制

    // 错误:connect(youmuBtn, &QToolButton::toggled, this, [actYoumu](bool checked)
    // 【架构纠正】:被托管后,应当监听 QAction 的信号,而不是 QToolButton 的信号!
    // 这样能确保无论是点击按钮,还是通过快捷键触发该 Action,逻辑都能完美收拢。
    connect(actYoumu, &QAction::toggled, this, [actYoumu](bool checked)
    {
        if(checked)
        {
            qDebug() << "【白玉楼命令拦截】: 妖梦拔出楼观剑与白楼剑!";
            actYoumu->setText("『人妖名剑·现世斩』!");
        }
        else
            actYoumu->setText("执行冥界庭院修剪任务");
    });

// =========================================================================
// 3. MenuButtonPopup(分裂箭头模式)与信号解耦测试
// =========================================================================
    // 3. 红魔馆按钮
    QToolButton* hongmoguanBtn = new QToolButton();
    hongmoguanBtn->setText("红魔馆势力范围");
    hongmoguanBtn->setIcon(QIcon(":/touhou/hongmoguan.png"));
    hongmoguanBtn->setIconSize(QSize(200, 120));
    hongmoguanBtn->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);

    // 实例化 QMenu,传入 this 挂载到窗口对象树上,防止内存泄漏
    QMenu* menu = new QMenu(this);
    QAction *actSakuya  = menu->addAction(QIcon(":/touhou/sakuya.png"), "十六夜咲夜");
    QAction* actRemilia = menu->addAction(QIcon(":/touhou/remilia.png"), "蕾米莉亚·斯卡蕾特");
    QAction *actFlandre    = menu->addAction(QIcon(":/touhou/flandre.png"), "芙兰朵露·斯卡蕾特");

    hongmoguanBtn->setMenu(menu);
    hongmoguanBtn->setPopupMode(QToolButton::MenuButtonPopup); // 分裂箭头模式

    // 分裂箭头模式下特性:点击左侧主体发射 clicked 信号;点击右侧小箭头弹出菜单
    connect(hongmoguanBtn, &QToolButton::clicked, this, [=]()
    {
        qDebug() << "【红魔馆正门】: 触发了主体点击!红美铃在门前打瞌睡被抓到了。";
    });

    // 拦截菜单内不同 Action 的触发信号(也可以通过 menu->triggered 统一收拢)
    connect(actSakuya, &QAction::triggered, this, [=]()
    {
        qDebug() << "【完美与潇洒的女仆】: 『幻葬·夜雾幻影』!时间停止流动,并丢出了一堆飞刀。";
    });
    connect(actRemilia, &QAction::triggered, this, [=]()
    {
        qDebug() << "【红魔之王】: 命运的红线已被操纵。神枪『冈格尼尔』(Gungnir) 蓄势待发。";
    });
    connect(actFlandre, &QAction::triggered, this, [=]()
    {
        qDebug() << "【恶魔之妹】: 破坏之眼已被捕获!『莱瓦汀』(Lævateinn) 准备进行疯狂的弹幕游戏。";
    });

// =========================================================================
// 4. InstantPopup(即时弹出模式,点击主体直接出菜单,不发射 clicked 信号)
// =========================================================================
    // 4.1 幽幽子按钮
    QToolButton *yuyukoBtn = new QToolButton();
    yuyukoBtn->setText("西行寺幽幽子");
    yuyukoBtn->setIcon(QIcon(":/touhou/yuyuko.png"));
    yuyukoBtn->setIconSize(QSize(100, 100));
    yuyukoBtn->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);

    QMenu *ghostMenu = new QMenu(this);
    QAction *actEat = ghostMenu->addAction("吃光白玉楼的三色团子");
    QAction *actDead = ghostMenu->addAction("引发西行妖开花(反魂蝶)");

    yuyukoBtn->setMenu(ghostMenu);
    yuyukoBtn->setPopupMode(QToolButton::InstantPopup); // 即时弹出模式

    // 注意:在 InstantPopup 模式下,连接 clicked 信号是无效的,因为点击主体会直接弹菜单
    connect(yuyukoBtn, &QToolButton::clicked, this, [=]() {
        qDebug() << "【⚠️ 警告】: 这行代码永远不会执行,因为 InstantPopup 拦截并屏蔽了 clicked 信号!";
    });

    connect(actEat, &QAction::triggered, this, [=]()
    {
        qDebug() << "【白玉楼日常】: 幽幽子大人吃光了所有的点心,妖梦又要去采购了。";
    });
    connect(actDead, &QAction::triggered, this, [=]()
    {
        qDebug() << "【西行妖下】: 『死蝶浮月』... 华丽的弹幕之中隐藏着冥界主宰死生程度的能力。";
    });


// =========================================================================
// 布局与内存托管
// =========================================================================
    centralLayout->addStretch();
    centralLayout->addWidget(reimuBtn);
    centralLayout->addWidget(marisaBtn);
    centralLayout->addWidget(youmuBtn);
    centralLayout->addWidget(hongmoguanBtn);
    centralLayout->addWidget(yuyukoBtn);
    centralLayout->addStretch();
}
posted @ 2026-06-13 14:49  学习笔记草稿存放账号  阅读(17)  评论(0)    收藏  举报