QML SwipeView:标签导航 与 Loader 懒加载

上一篇主要介绍了 SwipeView 的滑动翻页和指示器样式调整。这篇解决"页面多了怎么管理"的问题。

一是把 SwipeView 和 TabBar 绑定在一起,将页签点击与页面滑动操作连接起来,做成 App 常见的"顶部标签 + 内容页"结构;二是给每个页面套上 Loader,用日志区实时观察页面创建与销毁,确认懒加载真的只建了该建的页面。

  • SwipeView 与 TabBar 联动 --- 页签点击和手势滑动双向同步
  • SwipeView 与 Loader --- 按需创建页面,日志区验证懒加载过程

SwipeView 与 TabBar

数据源用一个 ListModel 统一管理,TabBar 和 SwipeView 各自用 Repeater 遍历同一份数据生成页签和页面。点页签切页面,滑动页面页签也跟着走,两边永远一致。

演示代码

qml 复制代码
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

FadeInAnimation {
    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 15

        // ... 省略标题组件 TitleSeparator ...

        ListModel {
            id: listModel
            ListElement { demoText: "Page1"; demoDes: "This is Page1" }
            ListElement { demoText: "Page2"; demoDes: "This is Page2" }
            ListElement { demoText: "Page3"; demoDes: "This is Page3" }
        }

        TabBar {
            id: tabBar
            Layout.fillWidth: true
            currentIndex: swipeView.currentIndex
            Repeater {
                model: listModel
                delegate: TabButton {
                    text: demoText
                    onClicked: swipeView.currentIndex = index
                }
            }
        }

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

            Repeater {
                model: listModel
                delegate: CustomRect {
                    description: demoDes
                }
            }
        }
    }
}

关键逻辑解析

同步的关键是 currentIndex,方向是"手势滑动驱动页签" 。tabBar.currentIndex 直接绑到 swipeView.currentIndex:滑动翻页时 SwipeView 先改自己的 currentIndex,TabBar 收到变化自动高亮对应页签------这是 TabBar 作为"跟随方"的绑定。

点页签时反向写回 swipeView.currentIndex 。TabButton.onClicked 里执行 swipeView.currentIndex = index,SwipeView 翻到对应页,页签高亮又由上一行绑定同步回来。两个方向凑齐,双向联动成立,而且没有双向绑定造成的循环隐患:只有一处绑定(TabBar ← SwipeView),点击是命令式赋值。

一个 ListModel 喂两个 Repeater,保证页签和页面数量天然一致 。TabBar 的 delegate 取 demoText 当页签文字,SwipeView 的 delegate 取 demoDes 当页面内容。增删数据只需改 listModel,页签和页面同步增减,不会出现"页签 3 个、页面 2 个"的错位。

注意这个 demo 没有用 interactive 指示器,页签本身就是导航。TabBar 自带选中态高亮和均分宽度,比 PageIndicator 更适合"多、需要文字说明"的页面;指示器适合"少、只有圆点"的场景。

SwipeView 与 Loader

5 个页面由一个 Repeater 按 model: 5 生成,但每个页面都包在 Loader 里,只有"当前页和左右相邻页"才真正实例化。底部日志区实时打印每页的 created/destroyed,滑动时能看到页面按需创建、滑远后销毁,而不是一次建满 5 个。

演示代码

qml 复制代码
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

FadeInAnimation {
    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 15

        // ... 省略标题组件 TitleSeparator ...

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

            currentIndex: indicator.currentIndex
            Repeater {
                model: 5
                Loader {
                    active: SwipeView.isCurrentItem || SwipeView.isNextItem || SwipeView.isPreviousItem
                    sourceComponent: CustomRect {
                        description: "This is CustomRect" + (index+1)
                        Component.onCompleted: logArea.message.append("created: CustomRect" + index)
                        Component.onDestruction: logArea.message.append("destroyed: CustomRect" + index)
                    }
                }
            }
        }

        Rectangle {
            Layout.fillWidth: true
            Layout.minimumHeight: 100
            Layout.maximumHeight: 100
            Layout.margins: 10
            border.width: 1
            border.color: "#ccc"
            radius: 6

            LogArea {
                id: logArea
                anchors.fill: parent
            }
        }

        CustomPageIndicator {
            id: indicator
            Layout.alignment: Qt.AlignHCenter

            count: swipeView.count
            currentIndex: swipeView.currentIndex
            visible: count > 1
        }
    }
}

页面组件的日志往 logArea 上追加(LogArea 是基于 TextArea 的自制滚动日志区),指示器复用项目里的 CustomPageIndicator------一个封装好 delegate、开了 interactive 的 PageIndicator。

关键逻辑解析

active 三选一是懒加载的开关 。SwipeView.isCurrentItem、isNextItem、isPreviousItem 是 SwipeView 给每个页面子项提供的附加属性,分别表示"我是当前页 / 我是下一页 / 我是上一页"。三个条件用 || 连起来:只要页面处在当前、左邻或右邻位置,Loader.active 为 true,组件才被创建;滑到更远的位置条件变 false,页面被销毁。这是上一篇提到懒加载写法的完整落地。

为什么是"当前 ± 1"而不是只建当前页?滑动动画过程中,手指拖到一半屏幕同时露着两页------如果只预建当前页,拖出下一页的瞬间才去创建,会出现白屏闪烁。保留左右邻页,动画随时有内容可画;而隔了两三页的远端页面滑过去还需要时间,没必要提前占内存。

日志区把"创建/销毁"变成可观察的过程 。页面在 Component.onCompleted 和 Component.onDestruction 里各打一条日志,滑到第 3 页时控制台会看到前两页陆续 destroyed、后两页陆续 created------懒加载到底省了多少实例,一眼可见。

currentIndex: indicator.currentIndex 让指示器成为唯一输入源 。这里没让 SwipeView 自己管页码,而是指示器点哪页、SwipeView 就翻到哪页;用户手势滑动时 currentIndex 变化又回写给指示器高亮,和上一篇"自定义指示器"的双向同步是同一套路。

TabBar 方案和 Loader 方案怎么选

TabBar 联动 Loader 懒加载
导航形式 文字页签 圆点指示器
页面管理 常驻,全部创建 只建当前页 ± 相邻页
适合场景 页签少、内容轻、要导航可见 页多、单页重、省启动开销
内容来源 ListModel 驱动,可增删 Repeater + Loader

两者不冲突:想让"重页面"也有文字页签导航,就把 TabBar 联动和 Loader 懒加载拼在一起------页签驱动 currentIndex,页面用 active 条件按需创建。

运行验证

  1. Qt Creator 打开 qml_swipeview/CMakeLists.txt,按 Ctrl+R 运行;
  2. 点「SwipeView 与 TabBar」,点 Page1/2/3 页签或直接滑动内容区,观察两者始终同步;
  3. 点「SwipeView 与 Loader」,连续滑动页面,看日志区滚动输出 created/destroyed,再点底部圆点跳页,观察跳远页时中间页的创建销毁顺序。

扩展复用方向

  • 想让 TabBar 方案也省内存,把 SwipeView 里的 delegate 换成 Loader + active 三条件即可,TabBar 联动逻辑不用动;
  • 页面是外部 .qml 文件时,把 sourceComponent 换成 source,就能做成"按文件名懒加载页面"的通用宿主;
  • 日志区的做法可以推广到任何想看组件生命周期的调试场景------Component.onCompleted/onDestruction 打点即可。

已验证环境:

相关推荐
xcyxiner10 小时前
DicomViewer24 修复编译bug(window test 失败)
前端·qt
sycmancia17 小时前
Qml——Rectangle元素
qt
sycmancia19 小时前
Qml——Item元素
qt
励志不掉头发的内向程序员21 小时前
【从零写一个CAD 04】中键拖动平移:抓住一个点,让它一直待在鼠标底下
开发语言·c++·qt·学习·系统架构·计算机外设
zhanghaha13141 天前
Qt 中的类是什么?保姆级详解(新手版)
qt
看看我呀看看我1 天前
基于Qt的虚拟键盘
c++·qt·qt5·qt6.3
kaixin_learn_qt_ing2 天前
Qt抽屉窗体实现demo
开发语言·qt
huanbailu2 天前
Qt 跨线程信号槽:connect 返回 true 但槽函数不执行的坑
开发语言·c++·qt·信号槽·跨线程·元类型
Quz2 天前
QML SplitView:可拖拽分隔面板与自定义 Handle
qt
Quz2 天前
QML SplitView 进阶:嵌套布局与状态持久化
qt