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 个按钮以绝对几何排布在 topWidgety=30 以下,每个 90×90,横向间距 20px:

xml 复制代码
<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 个按钮设置图标和文字:

cpp 复制代码
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()

cpp 复制代码
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 个关键点:

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

三、QSS 基础样式:白字 + 透明 + 状态渐变

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

css 复制代码
/* 基础:白字、透明背景、无边框 */
*[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 切换选中/未选中:

cpp 复制代码
// 选中态:顶透明 → 底 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() 末尾:

cpp 复制代码
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);
}

构造函数里:

cpp 复制代码
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 在按下/选中时自动从 QIconActive / Selected 模式取深色位图。这个项目不新增任何 PNG,而是在运行时合成:

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

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

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

cpp 复制代码
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)

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

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

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

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

cpp 复制代码
// 顶部分类按钮保持设计时的原始几何
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

相关推荐
zhanghaha13141 小时前
Python进阶教程:27_subprocess 模块 零基础超详细教程
开发语言·python
兄弟加油,别颓废了。1 小时前
bugku题目
开发语言·前端·python
zh_xuan1 小时前
c++ string_view
开发语言·c++
阿里嘎多学长1 小时前
2026-09-05 GitHub 热点项目精选
开发语言·程序员·github·代码托管
古少侠1 小时前
用 DS随心转 把 AI 画的 Mermaid 流程图转成 Word 里的高清图
开发语言·c#·word
万年咸鱼1 小时前
Java BufferedOutputStream 详解:原理、用法与性能优化
java·开发语言·性能优化
万年咸鱼1 小时前
Java BufferedInputStream 详解:原理、用法与实战
java·开发语言·python
weixin199701080161 小时前
[特殊字符]《二手ERP × 闲鱼消息驱动架构:正向+逆向交易消息如何驱动WMS出库与回传》(附Python源码)
开发语言·python·架构
galaxy_strive2 小时前
Qt C++插件化编写项目(1)
开发语言·c++·qt