默认 TabBar 样式不讨喜,到了实际的项目中多半要改样式。选中态要圆角背景、颜色要有过渡动画;标签数量和内容还得跟着数据动态增删。
这篇介绍两个简单的例子:
- 自定义 TabButton 样式 --- 重写原始组件的
contentItem和background属性,选中标签显示蓝底白字、带颜色过度动画 - 动态添加/删除标签 --- 用
ListModel管理标签和页面,增删数据行,界面能够自动同步
自定义 TabButton 样式

三个圆角胶囊按钮:没选中是灰字透明底,选中变蓝底白字,颜色变化带过渡效果。
演示代码
qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
FadeInAnimation {
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 15
// ... 省略标题组件 TitleSeparator ...
TabBar {
id: tabBar
Layout.fillWidth: true
background: Rectangle {
color: "#f5f5f5"
radius: 4
}
TabButton {
text: "首页"
width: implicitWidth + 20
contentItem: Text {
text: parent.text
font.pixelSize: 14
color: parent.checked ? "#fff" : "#666"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
implicitWidth: 60
implicitHeight: 36
color: parent.checked ? "#3498db" : "transparent"
radius: 4
Behavior on color {
ColorAnimation { duration: 150 }
}
}
}
// ... 省略结构相同的「发现」「我的」两个 TabButton ...
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
color: Qt.rgba(0.95, 0.95, 0.95, 1)
radius: 4
Text {
anchors.centerIn: parent
text: "自定义样式:圆角背景 + 颜色动画"
font.pointSize: 11
color: "#666"
}
}
}
}
关键逻辑解析
自定义 TabButton 从两部分下手:contentItem 管文字/图标,background 管底子 。QML 控件"内容与背景分离",想换样子不用改控件逻辑,只需替换这两块。示例里 contentItem 用一个 Text 显示标签文字,background 用一个圆角 Rectangle 当底。
选中态直接读 parent.checked 。TabButton 是 AbstractButton,自带 checked 属性,选中时为真。内容文字的 color: parent.checked ? "#fff" : "#666"、背景的 color: parent.checked ? "#3498db" : "transparent" 都挂在它上面,点选切换时样式跟着状态自动变,不用自己监听信号。
Behavior on color 让颜色切换"滑过去" 。选中不是瞬间跳色,而是 150ms 的 ColorAnimation 渐变。Behavior 是 QML 里给属性变化加动画的标准手法------checked 一翻转,color 从透明到蓝色就带上了过渡。
width: implicitWidth + 20 保留内容宽度。TabBar 默认把按钮均分占满,这里显式设宽度让按钮只比文字宽一点,三个胶囊并排靠左,不再拉伸满行。
implicitWidth/implicitHeight 定义背景的最小尺寸。background 里给 60×36 的隐式尺寸,保证 TabButton 即使文字很短也有可点的面积;设置的是隐式值而不是硬宽高,布局系统仍有缩放余地。
动态添加/删除标签

「添加标签」每次在末尾追加一个标签,「删除当前」删掉选中的那个(至少留一个),「清空全部」重置回单个标签,标签栏和内容页面同步增减。
演示代码
qml
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
FadeInAnimation {
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 15
// ... 省略标题组件 TitleSeparator ...
RowLayout {
Layout.fillWidth: true
spacing: 10
Button {
text: "添加标签"
onClicked: {
var newIndex = tabModel.count + 1
tabModel.append({"tabName": "标签" + newIndex, "pageColor": Qt.rgba(Math.random(), Math.random(), Math.random(), 1)})
}
}
Button {
text: "删除当前"
enabled: tabModel.count > 1
onClicked: {
if (tabBar.currentIndex >= 0 && tabModel.count > 1) {
tabModel.remove(tabBar.currentIndex)
}
}
}
Button {
text: "清空全部"
onClicked: {
tabModel.clear()
tabModel.append({"tabName": "标签1", "pageColor": "#3498db"})
}
}
}
TabBar {
id: tabBar
Layout.fillWidth: true
clip: true
Repeater {
model: ListModel {
id: tabModel
ListElement { tabName: "标签1" }
ListElement { tabName: "标签2" }
}
delegate: TabButton {
text: tabName
width: implicitWidth + 10
}
}
}
StackLayout {
Layout.fillWidth: true
Layout.fillHeight: true
currentIndex: tabBar.currentIndex
Repeater {
model: tabModel
delegate: Rectangle {
color: "#3498db"
Text {
anchors.centerIn: parent
text: tabName
color: "#fff"
font.pointSize: 18
font.bold: true
}
}
}
}
Text {
text: "标签数量: " + tabModel.count
font.pointSize: 11
color: "#666"
}
}
}
关键逻辑解析
标签和数据用 ListModel 一份数据驱动两份 UI 。同一个 tabModel 同时喂给 TabBar 里的 Repeater(生成 TabButton)和 StackLayout 里的 Repeater(生成内容页),append/remove/clear 改数据,两边的界面自动增删------这是"标签和页面数量永远一致"的保证,不会出现标签 5 个页面 3 个的错位。
新增标签名按现有数量取名 。var newIndex = tabModel.count + 1,再 append({"tabName": "标签" + newIndex, ...}),每次追加的标签自然递增,无需外部计数。
删除前两重保护 。enabled: tabModel.count > 1 让"只剩一个标签"时按钮直接置灰;onClicked 里再判一次 count > 1,双保险防止把最后一个也删掉,保证标签栏不为空。
删掉当前标签后,currentIndex 由容器自己收拾 。tabModel.remove(tabBar.currentIndex) 只负责删数据行;TabBar 检测到按钮少了,会自动把选中索引收敛到合法范围(比如删了第 2 个,选中会落到新的第 1 个),内容页跟着联动,不需要手动修正。
标签多了会横向滚动,所以要 clip: true 。按钮总宽超出 TabBar 可视区后会自动变成可滑动,clip 保证滚动过程中按钮不会越界画到内容区外面。
模型里预留了 pageColor 字段,页面配色可以更活 。append 时传了随机颜色、清空时传了蓝色,说明数据层已经支持每页不同色;当前 demo 内容页 Rectangle 把颜色写死成 #3498db,想真正"一页一色",把 delegate 里的 color 换成 pageColor 即可。
自定义样式和动态数据怎么结合
| 需求 | 手段 |
|---|---|
| 换标签外观 | 重写 contentItem + background,状态用 parent.checked |
| 加选中过渡 | background 里 Behavior on color + ColorAnimation |
| 不让按钮均分 | 设显式 width(implicitWidth + N) |
| 标签随数据增删 | Repeater + ListModel,改数据即改界面 |
| 删标签的边界 | 靠 count > 1 判空,currentIndex 交给容器收敛 |
| 标签多了可滑动 | 自动 Flickable + clip: true |
自定义只动"皮",动态增删只动"数据",两层可以叠着用:数据驱动出一排按钮,按钮再套上自定义 contentItem/background,就是一套能增删、能换肤的标签栏。
运行验证
- Qt Creator 打开
qml_tabbar/CMakeLists.txt,按Ctrl+R运行; - 左侧点「自定义按钮」,点三个胶囊按钮看蓝底白字与颜色渐变;
- 点「动态标签」,反复点「添加标签 / 删除当前」,观察标签栏和内容页同步增减、数量文字实时变化;点「清空全部」回到单标签。
扩展复用方向
- 把自定义
TabButton抽成单独 QML 文件(比如PillTabButton.qml),三个按钮引用同一组件,样式只维护一处; - 标签数量多时让 TabBar 支持关闭按钮(每个 TabButton 右上角加个 ×,点击从 model 里
remove(index)),做"浏览器标签页"体验; - 配合
SwipeView而不是 StackLayout,动态标签加滑动翻页,内容页仍由同一个tabModel驱动。
已验证环境:
- Qt 版本:Qt 6.8.2 / Qt 6.11.1
- 操作系统:Windows 11
- GitHub:QML-Minimal-Demos/qml_tabbar