QML TabBar组件与容器索引的绑定

62 阅读5分钟

TabBar 组件只能处理当前选第几个的需求,真正展示内容要靠容器、布局等组件。Qt 里有两类容器能够和 TabBar 配对,分别是 StackLayout(用索引控制页面)和 SwipeView(支持手势滑动)。

这篇分别介绍它们的关联用法:

  • TabBar + StackLayout — 点击标签即时切换内容页
  • TabBar + SwipeView — 标签和页面 currentIndex 双向绑定,点击、滑动都能切换页面

TabBar 与 StackLayout:点击标签切换页面

5.gif

三个标签对应三块纯色内容区,点标签内容立刻切换,底部显示"当前索引 / 总页数"。

演示代码

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

            TabButton {
                text: "页面1"
                width: implicitWidth
            }
            TabButton {
                text: "页面2"
                width: implicitWidth
            }
            TabButton {
                text: "页面3"
                width: implicitWidth
            }
        }

        StackLayout {
            id: stackLayout
            Layout.fillWidth: true
            Layout.fillHeight: true
            currentIndex: tabBar.currentIndex

            Rectangle {
                color: "#3498db"
                Text {
                    anchors.centerIn: parent
                    text: "页面 1"
                    color: "#fff"
                    font.pointSize: 20
                    font.bold: true
                }
            }

            Rectangle {
                color: "#e74c3c"
                Text {
                    anchors.centerIn: parent
                    text: "页面 2"
                    color: "#fff"
                    font.pointSize: 20
                    font.bold: true
                }
            }

            Rectangle {
                color: "#2ecc71"
                Text {
                    anchors.centerIn: parent
                    text: "页面 3"
                    color: "#fff"
                    font.pointSize: 20
                    font.bold: true
                }
            }
        }

        Text {
            text: "当前索引: " + tabBar.currentIndex + " / 总页数: " + stackLayout.count
            font.pointSize: 11
            color: "#333"
        }
    }
}

关键逻辑解析

联动只有一行:currentIndex: tabBar.currentIndex。把 StackLayout 的 currentIndex 绑到 TabBar 上,点标签 → TabBar 的索引变 → StackLayout 显示对应那一页。TabBar 自身不需要知道内容区存在,这一行绑定是它们唯一的连接点。

StackLayout 的页面全部常驻。三个 Rectangle 从一开始就都创建好了,切换只是"显示第几个",不是"创建第几个"。所以页面里的输入框、滚动位置这些内部状态切换后原样保留,代价是所有页面占用的内存、初始化开销一直存在。

stackLayout.count 直接可读。页面数就是 StackLayout 的子项数,运行时也能查。底部文字把 tabBar.currentIndex 和 count 拼在一起,一眼看出"第几页 / 共几页"。

按钮设 width: implicitWidth,标签按内容宽排列。不给宽度 TabBar 会把按钮拉满均分;这里显式设成内容宽度,三个标签靠左紧凑排列,突出"这是页内切换而非全宽导航"的用法。

TabBar 与 SwipeView:双向联动

6.gif

四个标签,下方四个页面。点标签可以换页,按住页面左右滑也可以换页——两个操作都同步到对方的选中状态。

演示代码

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
            currentIndex: swipeView.currentIndex

            Repeater {
                model: ["首页", "发现", "消息", "我的"]
                TabButton {
                    text: modelData
                    ToolTip.visible: hovered
                    ToolTip.text: "索引: " + TabBar.index
                }
            }
        }

        SwipeView {
            id: swipeView
            Layout.fillWidth: true
            Layout.fillHeight: true
            clip: true

            // 使用 currentIndex 进行双向绑定
            currentIndex: tabBar.currentIndex

            Repeater {
                model: ["#3498db", "#e74c3c", "#2ecc71", "#f39c12"]
                Rectangle {
                    color: modelData
                    Text {
                        anchors.centerIn: parent
                        text: "页面 " + (index + 1)
                        color: "#fff"
                        font.pointSize: 18
                        font.bold: true
                    }
                }
            }
        }

        Text {
            text: "当前索引: " + swipeView.currentIndex + " / 总页数: " + swipeView.count
            font.pointSize: 11
            color: "#333"
        }
    }
}

关键逻辑解析

双向同步 = 两个 currentIndex 互相绑定。tabBar.currentIndex: swipeView.currentIndex 和 swipeView.currentIndex: tabBar.currentIndex 同时存在:点标签时,TabBar 索引变化传到 SwipeView 完成翻页;滑动页面时,SwipeView 索引变化传回 TabBar 让高亮跟着走。这是 Qt 文档里推荐的"two-way binding"写法,注意要点是不要在事件里直接给 currentIndex 赋值,赋值会切断绑定,改动要交给绑定系统完成。

clip: true 是滑动的标配。SwipeView 翻页是横向滑动过渡,翻页瞬间两个页面同时出现在可视区,不裁剪会把旁边的页面画出来,观感就是"露馅"。

标签数据用字符串数组驱动。Repeater 的 model 直接给 ["首页", "发现", "消息", "我的"],delegate 里用 modelData 取当前项当文字——想加标签,往数组里加一个字符串就行。

TabBar.index 是官方附加属性,不是笔误。TabBar 会给它的每个 TabButton 附加 index、position、tabBar 三个只读属性,delegate 里写 "索引: " + TabBar.index 读到的就是"这个按钮是第几个"。示例把它接进 ToolTip,鼠标悬停标签时显示当前索引,是演示附加属性最直观的方式。

ToolTip 挂在控件上是附加式用法。ToolTip.visible: hovered + ToolTip.text: ... 直接写在 TabButton 上,不用实例化 ToolTip 组件,悬停自动弹出,移动端没有鼠标则天然不触发。

StackLayout 还是 SwipeView:看要不要手势和动画

StackLayoutSwipeView
切换方式点标签即时跳点标签 / 手势滑动
切换动画无,直接显示有,页面滑入滑出
页面生命周期全部常驻相邻页预加载,仍保留状态
适合表单、设置项这类"别丢状态"的页内容流、需要手势翻页的浏览场景
联动 TabBar单向绑定即可需双向绑定

判断标准很简单:要不要滑动翻页的体验。纯工具型页面用 StackLayout 干净利落;偏浏览型(图片流、文章列表)用 SwipeView,配合手势更顺手。固定数量页面用这两者都行,标签本身要动态增删的话,回到上一篇的 Repeater + ListModel 方案,把内容区的 Repeater model 也接同一份数据即可。

运行验证

  1. Qt Creator 打开 qml_tabbar/CMakeLists.txt,按 Ctrl+R 运行;
  2. 左侧点「与 StackLayout」,点三个标签看内容即时切换、底部索引联动;
  3. 点「与 SwipeView」,先点标签换页,再按住页面左右拖拽换页,观察标签高亮始终跟着当前页;鼠标悬停标签可看到索引提示。

扩展复用方向

  • 想在 StackLayout 切换时也带点过渡,可以给页面加 visible 与透明度状态,或改用 StackView 的 push/pop 做带动画的层级导航;
  • SwipeView 每个页面顶部配一个 PageIndicator 小圆点,滑动位置一目了然,是引导页、轮播的现成组合;
  • 双向绑定出问题时,先检查是不是有代码在 onClicked 里直接写过 currentIndex = x——那会切断绑定,改成用 tabBar.setCurrentIndex(x) 这类方法赋值。

已验证环境: