云计算百科
云计算领域专业知识百科平台

Qt 分类导航区的实现

Qt 分类导航区的实现

flyfish

Qt Creator 17.0.1 Desktop Qt 6.9.3 MSVC2022 64bit Windows 平台

完整代码在文末

可执行文件下载

请添加图片描述 在这里插入图片描述 ErFenSS 顶部 5 个分类按钮

ErFenSS 顶栏那一排分类按钮(All / Folders / Pictures / Audio / Videos)视觉效果:图标在上、文字在下,悬停时图标轻轻浮起、背景微微发黑,按下时图标下陷、颜色加深,选中态有一层从上到下的暗色渐变,而且还能跟随主题色自动适配。

一、UI 基础:5 个 QToolButton 的静态布局

在 mainwindow.ui 中,5 个按钮以绝对几何排布在 topWidget 的 y=30 以下,每个 90×90,横向间距 20px:

<widget class="QToolButton" name="tbAll"> <x>20</x> <y>30</y> <width>90</width> <height>90</height> </widget>
<widget class="QToolButton" name="tbFolders"> <x>110</x> <y>30</y> <width>90</width> <height>90</height> </widget>
<widget class="QToolButton" name="tbPictures"> <x>200</x> <y>30</y> <width>90</width> <height>90</height> </widget>
<widget class="QToolButton" name="tbAudio"> <x>290</x> <y>30</y> <width>90</width> <height>90</height> </widget>
<widget class="QToolButton" name="tbVideos"> <x>380</x> <y>30</y> <width>90</width> <height>90</height> </widget>

选用 QToolButton 而不是 QPushButton,是因为它原生支持图标在上、文字在下的 Qt::ToolButtonTextUnderIcon 样式,省去自绘。

在这里插入图片描述

二、按钮初始化:图标 + 文字 + 属性标记

入口是 setupCategoryNav() mainwindow.cpp,它调用 [optionToolButtons() 给 5 个按钮设置图标和文字:

optionToolButton(ui->tbAll, QIcon(":/tab/images/tab/all.png"), "All");
optionToolButton(ui->tbFolders, QIcon(":/tab/images/tab/folder.png"), "Folders");
optionToolButton(ui->tbPictures, QIcon(":/tab/images/tab/picture.png"),"Pictures");
optionToolButton(ui->tbAudio, QIcon(":/tab/images/tab/audio.png"), "Audio");
optionToolButton(ui->tbVideos, QIcon(":/tab/images/tab/video.png"), "Videos");

真正干活的是 optionToolButton()

void MainWindow::optionToolButton (QToolButton * tb, QIcon icon, QString text) {
QAction *pAction = new QAction(this);
pAction->setText(text);
pAction->setIcon(icon);
tb->setIconSize(QSize(50, 50)); // 图标 50×50
tb->setDefaultAction(pAction); // 文字+图标绑到 Action
tb->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); // 图标在上、文字在下
tb->setProperty ("mainwindow_toolbutton", true); // 打上属性标记供 QSS 选择
tb->setToolTip(""); // 禁止 QToolButton 自动把 action 文本弹成 tooltip 黑框
}

这里有 3 个关键点:

  • setDefaultAction:把文字和图标统一交给 QAction 管理,后续换肤时只需改 Action 的图标即可。
  • setProperty("mainwindow_toolbutton", true):自定义属性,让 styles.qss 里的 *[mainwindow_toolbutton="true"] 选择器能精确命中这些按钮。
  • setToolTip(""):QToolButton 默认会把 action 文本作为 tooltip,鼠标悬停时弹一个黑框,这里显式清空避免干扰视觉。
  • 三、QSS 基础样式:白字 + 透明 + 状态渐变

    styles.qss 定义了按钮的三态视觉:

    /* 基础:白字、透明背景、无边框 */
    *[mainwindow_toolbutton="true"] {
    border: none;
    background-color: transparent;
    color: #FFFFFF;
    padding: 0px;
    }

    /* hover:从顶到底微微发黑(rgba 18) */
    *[mainwindow_toolbutton="true"]:hover {
    background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1,
    stop:0 rgba(0, 0, 0, 0),
    stop:1 rgba(0, 0, 0, 18));
    }

    /* pressed:更深的黑色渐变(rgba 46) */
    *[mainwindow_toolbutton="true"]:pressed {
    background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1,
    stop:0 rgba(0, 0, 0, 0),
    stop:1 rgba(0, 0, 0, 46));
    }

    :hover 和 :pressed 是 Qt 自动跟踪鼠标状态的伪状态,不需要写任何 C++ 代码就能触发。顶部 stop:0 设为完全透明,让渐变从标题栏背景色自然过渡到底部的暗色,视觉上像一层阴影」浮在按钮上。

    四、选中态:两套 QSS 切换 + 属性联动

    点击分类按钮会调用 setTabBarIndex(),它用两套内联 QSS 切换选中/未选中:

    // 选中态:顶透明 → 底 rgba(0,0,0,45) 的暗色渐变
    const QString ssSelected =
    "QToolButton{background-color:qlineargradient(x1:0,y1:0,x2:0,y2:1,"
    "stop:0 rgba(0,0,0,0),stop:1 rgba(0,0,0,45));"
    "color:#FFFFFF;border:none;}"
    "QToolButton:hover{…stop:1 rgba(0,0,0,55)…}"
    "QToolButton:pressed{…stop:1 rgba(0,0,0,70)…}";

    // 未选中态:透明背景 + 轻微 hover 加深
    const QString ssNormal =
    "QToolButton{background-color:transparent;color:#FFFFFF;border:none;}"
    "QToolButton:hover{…stop:1 rgba(0,0,0,18)…}"
    "QToolButton:pressed{…stop:1 rgba(0,0,0,46)…}";

    QToolButton *tabs[5] = { ui->tbAll, ui->tbFolders, ui->tbPictures, ui->tbAudio, ui->tbVideos };
    for (int i = 0; i < 5; ++i) {
    tabs[i]->setStyleSheet(i == index ? ssSelected : ssNormal);
    tabs[i]->setProperty("mainwindow_toolbutton_selected", (i == index));
    updateStyle(tabs[i]); // unpolish → polish → update 强制重刷
    }

    几个值得注意的设计:

    用数组统一管理:5 个按钮放进 tabs[5],循环赋值,避免 5 段重复代码。 内联 QSS 而非属性选择器:选中态直接 setStyleSheet,优先级高于外部 styles.qss,确保选中视觉不被基础样式覆盖。 updateStyle() 三板斧:style()->unpolish(w) 清掉旧样式缓存 → polish(w) 重新应用 → update() 触发重绘。这是 Qt 动态改样式后必须做的,否则可能看不到变化。 选中态的 hover/pressed 也单独定义:选中按钮在 hover 时渐变更深(55 vs 45),pressed 时最深(70),保证选中状态下交互反馈依然清晰。

    五、CatButtonEffect —— 图标缓动 + 加深态合成

    上面 QSS 只做了背景色的状态变化,真正让按钮活起来」的是 CatButtonEffect 这个内部类。它为每个分类按钮做两件事:图标尺寸缓动和运行时合成加深态图标。

    5.1 安装与初始化

    在 setupCategoryNav() 末尾:

    qDeleteAll(m_catEffects);
    m_catEffects.clear();
    for (QToolButton *tb : { ui->tbAll, ui->tbFolders, ui->tbPictures, ui->tbAudio, ui->tbVideos }) {
    auto *eff = new CatButtonEffect(tb, this);
    m_catEffects.append(eff);
    }

    构造函数里:

    m_origSize = btn->iconSize(); // 记录原始 50×50
    m_btn->setAttribute(Qt::WA_Hover, true); // 开启 hover 事件
    m_anim = new QVariantAnimation(this);
    m_anim->setEasingCurve(QEasingCurve::OutCubic); // 缓动曲线
    connect(m_anim, &QVariantAnimation::valueChanged, this,
    [this](const QVariant &v) { m_btn->setIconSize(v.toSize()); });
    m_btn->installEventFilter(this);

    5.2 运行时合成加深态图标(不增加资源文件)

    常规做法是每个图标配一张压暗 20% 的 _b.png,Qt 在按下/选中时自动从 QIcon 的 Active / Selected 模式取深色位图。这个项目不新增任何 PNG,而是在运行时合成:

    QPixmap base = cur.pixmap(sz, QIcon::Normal, QIcon::Off); // 原始 50×50 图标

    // 按下态(Active):叠加 22% 黑(alpha=56)
    QPixmap dPress = base.copy();
    { QPainter p(&dPress);
    p.setCompositionMode(QPainter::CompositionMode_SourceAtop); // 关键:只在不透明区域上色
    p.fillRect(dPress.rect(), QColor(0, 0, 0, 56)); }

    // 选中态(Selected):叠加 15% 黑(alpha=38)
    QPixmap dSel = base.copy();
    { QPainter p(&dSel);
    p.setCompositionMode(QPainter::CompositionMode_SourceAtop);
    p.fillRect(dSel.rect(), QColor(0, 0, 0, 38)); }

    cur.addPixmap(dPress, QIcon::Active, QIcon::On);
    cur.addPixmap(dPress, QIcon::Active, QIcon::Off);
    cur.addPixmap(dSel, QIcon::Selected, QIcon::On);
    cur.addPixmap(dSel, QIcon::Selected, QIcon::Off);
    m_btn->setIcon(cur);

    CompositionMode_SourceAtop 是精髓:黑色只覆盖在原图已有像素的地方,透明区域保持透明,所以得到的是图标本身变暗而不是整个方形变黑。把这两张加深图填回 QIcon 的 Active(按下)和 Selected(选中)模式后,Qt 会在对应状态自动切换,无需手动管理。

    5.3 图标尺寸缓动(浮起 / 下陷)

    eventFilter 监听 4 个事件,驱动 QVariantAnimation 在图标尺寸之间插值:

    case QEvent::Enter:
    // 放大到 112%(50→56),140ms OutCubic —— 浮起反馈
    animateTo(QSize(orig*1.12, orig*1.12), 140);
    break;
    case QEvent::Leave:
    animateTo(m_origSize, 160); // 回到 50
    break;
    case QEvent::MouseButtonPress:
    // 缩到 92%(56→46),80ms —— 下陷反馈
    animateTo(QSize(orig*0.92, orig*0.92), 80);
    break;
    case QEvent::MouseButtonRelease:
    // 松手后:若仍在按钮内 → 回到 56(hover 态),否则 → 50
    const bool stillIn = m_btn->rect().contains(me->position().toPoint());
    animateTo(stillIn ? QSize(orig*1.12, orig*1.12) : m_origSize, 130);
    break;

    animateTo 每次都会 stop() 正在运行的动画再重新 start(),保证快速移动鼠标时不会叠加多个动画导致抖动。OutCubic 缓动让动画快起慢停」,手感更自然。

    5.4 三层反馈叠加

    最终一个分类按钮的交互是三层视觉同时作用的结果:

    交互背景层(QSS)图标颜色层(QIcon mode)图标尺寸层(CatButtonEffect)
    常态 透明 Normal 原色 50×50
    hover rgba(0,0,0,18) 渐变 Normal 原色 56×56(放大)
    pressed rgba(0,0,0,46) 渐变 Active 加深 22% 46×46(缩小)
    选中 rgba(0,0,0,45) 渐变 Selected 加深 15% 50×50

    六、换肤联动:选中态自动适配新背景

    在这里插入图片描述

    applyTheme() 换主题色时会重新调用 setTabBarIndex(m_currentTabIndex):

    ui->backgroundWidget->setStyleSheet("background-color:" + t.primaryColor);
    setTabBarIndex(m_currentTabIndex); // 重刷选中态渐变

    因为选中态的渐变是 rgba(0,0,0,45) 这种与背景色无关的黑色半透明,所以无论顶栏换成绿色、蓝色还是紫色,选中按钮都会自然地「压暗一层」,无需为每个主题单独写样式。这就是为什么 6 套主题下分类按钮都能保持一致的质感。

    七、缩放时的特殊处理:分类按钮不参与等比缩放

    在 applyScaling() 的最后:添加链接描述

    // 顶部分类按钮保持设计时的原始几何
    for (int i = 0; i < m_catButtons.size() && i < m_catOrigGeoms.size(); ++i) {
    QToolButton *b = m_catButtons.at(i);
    const QRect r = m_catOrigGeoms.at(i);
    b->setGeometry(r.x(), r.y(), r.width(), r.height()); // 强制恢复 90×90
    }

    窗口缩放时,标题栏宽度会拉伸、右侧按钮会右移,但5 个分类按钮始终保持 90×90。如果让它们也等比缩放,图标和文字会被拉变形(比如窗口变宽时按钮变胖),反而难看。保持固定尺寸 + 居中/左对齐是更稳妥的选择。

    源码下载地址 https://github.com/shaoshengsong/ErFenSS

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » Qt 分类导航区的实现
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!