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:
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 个关键点:
setDefaultAction:把文字和图标统一交给QAction管理,后续换肤时只需改 Action 的图标即可。setProperty("mainwindow_toolbutton", true):自定义属性,让 styles.qss 里的*[mainwindow_toolbutton="true"]选择器能精确命中这些按钮。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 在按下/选中时自动从 QIcon 的 Active / 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 是精髓:黑色只覆盖在原图已有像素 的地方,透明区域保持透明,所以得到的是图标本身变暗而不是整个方形变黑。把这两张加深图填回 QIcon 的 Active(按下)和 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。如果让它们也等比缩放,图标和文字会被拉变形(比如窗口变宽时按钮变胖),反而难看。保持固定尺寸 + 居中/左对齐是更稳妥的选择。
源码下载地址