QML StackView:入栈、出栈、替换与批量操作

文章来源声明: 原文作者:Quz; 来源站点:掘金; 原文链接:https://juejin.cn/post/7685224015707914290; 本文基于上述来源整理/加工,觅优补充点评,仅供技术学习交流。版权归原作者所有。
觅优短评

把 StackView 的 API 按“进/退/换/批量”场景整理成对照表,depth 贯穿判断的思路很实用。适合做 QML 导航流程、向导表单的开发者当速查手册。

`StackView` 是 Qt Quick Controls 里的页面栈容器,页面一层层压进去(push),再一层层弹出来(pop),初始页在堆栈底部。这套机制和App软件中的导航返回是一回事。

这一篇用两个示例说明入栈、出栈、替换的基本操作与批量操作。

基本操作

1.gif

一个 StackView 配四个按钮:Push 压入新页、Push(无动画)再压一页、Pop 回到上一页、Replace 把当前页换掉。

演示代码

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

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

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

        StackView {
            id: stack
            initialItem: mainView
            Layout.fillWidth: true
            Layout.fillHeight: true
            spacing: 15
            clip: true
        }

        Text {
            text: "StackView Depth: " + stack.depth
            color: "#333"
            font.pointSize: 11
        }
    }

    Component {
        id: mainView

        ColumnLayout {
            spacing: 10

            property color textColor: "#333"

            Button {
                text: "Push(叠加新页面)"
                Layout.fillWidth: true
                Layout.preferredHeight: 30
                property var info: {"textColor":"green"}
                onClicked: stack.push(mainView, info)
            }

            Button {
                text: "Push(叠加新页面,无动画)"
                Layout.fillWidth: true
                Layout.preferredHeight: 30
                property var info: {"textColor":"green"}
                onClicked: stack.push(mainView, info, StackView.Immediate)
            }

            Button {
                text: "Pop(回到上一页)"
                Layout.fillWidth: true
                Layout.preferredHeight: 30
                enabled: stack.depth > 1
                onClicked: stack.pop()
            }

            // 栈长度不变,新的页面替换掉原有页面,退不回去
            Button {
                text: "Replace(替换当前页面)"
                Layout.fillWidth: true
                Layout.preferredHeight: 30
                property var info: {"textColor":"red"}
                onClicked: stack.replace(mainView, info)
            }

            Item { Layout.fillHeight: true }
        }
    }
}

关键逻辑解析

initialItem属性决定栈底是谁。StackView 一出生栈里就有 mainView(按钮页),所以 depth(栈深) 初始为 1。后续所有 Push 都叠在它上面,Pop 无论如何不会把栈底弹掉。

push(item, properties, operation) 的三个参数各管一件事:第一个是"推什么"——这里传组件 mainView,每次 push 都会新建一个页面实例;第二个是"给新页面带什么属性",{"textColor":"green"} 会按名注入到新页面的同名属性上(mainView 里声明了 property color textColor);第三个是操作方式,StackView.Immediate 表示不播转场动画、瞬间切换。replace(mainView, info) 同理,但效果是"原地换皮"——栈长度不变,回退时跳不到被替换掉的那一页。

栈底页没有上一页可回,Pop 按钮置灰避免空弹;这也说明 demo 是用 depth 做状态判断,而不是自己记一个计数器——页面压入弹出都由 StackView 管理,所以基于 depth 属性来判断永远是准的。

每次 Push 一个页面实例,按钮也带走一份。Push 压入的是 mainView 组件的副本,所以新页面里同样有这套按钮,可以继续 Push、Pop,页面就这样一层层叠下去。底部 Text 绑定 stack.depth,压一页数字加一,弹一页数字就减一,能直观看到栈的变化。

批量操作

2.gif

基本操作的 API 一次只动一层。批量场景则要"一次压 5 页、一次弹 5 页、一键回栈底"——用的还是 push/pop,只是换成批量调用,外加 pushItems 这个专为连续压栈准备的接口。

演示代码

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts

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

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

        StackView {
            id: stack
            initialItem: mainView
            Layout.fillWidth: true
            Layout.fillHeight: true
            spacing: 15
            clip: true
        }

        Text {
            text: "StackView Depth: " + stack.depth
            color: "#333"
            font.pointSize: 11
        }
    }

    Component {
        id: mainView

        ColumnLayout {
            spacing: 10

            property color textColor: "#333"

            Button {
                text: "批量Push(5个页面)"
                Layout.fillWidth: true
                Layout.preferredHeight: 30
                enabled: stack.depth < 30
                onClicked: {
                    var items = []
                    for (var i = 1; i <= 5; i++) {
                        items.push(mainView, {"textColor":"green"})
                    }
                    stack.pushItems(items)
                }
            }

            Button {
                text: "批量Pop(5个页面)"
                Layout.fillWidth: true
                Layout.preferredHeight: 30
                enabled: stack.depth > 1
                onClicked: {
                    var popCount = Math.min(5, stack.depth - 1)
                    for (var i = 0; i < popCount; i++) {
                        stack.pop()
                    }
                }
            }

            Button {
                text: "清空栈(Pop全部)"
                Layout.fillWidth: true
                Layout.preferredHeight: 30
                enabled: stack.depth > 1
                onClicked: {
                    while (stack.depth > 1) {
                        stack.pop()
                    }
                }
            }

            Item { Layout.fillHeight: true }
        }
    }
}

关键逻辑解析

pushItems 接收的是"交替数组"。它要求数组元素按 item, properties, item, properties… 的顺序排列,每两项构成一次压栈。demo 的循环里 items.push(mainView, {...}) 一次塞进两个元素,循环 5 次得到 10 个元素的数组,pushItems 依次压入 5 页。注意它不会中途停:一批压栈视为一次连续操作,页面按顺序全部进入。

批量 Pop 本质是循环 Pop,要点在"算准次数"Math.min(5, stack.depth - 1) 防止两件事:一是栈里不够 5 层时只弹能弹的;二是 stack.depth - 1 保证不把栈底页弹掉。stack.depth 在每次 pop() 后都会实时变小,所以循环里不能预先存死一个总数再用它当条件。

清空栈用 while (depth > 1)。栈底 initialItem 被保留,所以循环条件是 > 1 而不是 > 0。这其实是"弹到只剩栈底"的朴素写法,官方有更短的等价调用:pop(null) 会一直弹到只剩第一项,pop(首页引用) 则弹到指定页成为栈顶,都不用手写循环。demo 用 while 是为了直观展示 depth 每弹一次实时变小。

clear() 区分开StackView 还有 clear() 方法,它把栈里所有项清空、包括初始项——清完栈里什么都没有,得再 push 一个页面才有内容。demo 里要保留首页当按钮页,所以不能用 clear;"整个栈推倒重来"的场景才轮到它。

批量按钮同样用 depth 做可用性判断enabled: stack.depth < 30 给批量 Push 设了个"天花板"(每个页面实例里都能继续压 5 层,不设限会无限叠下去),纯防演示时把栈压爆;批量 Pop 和清空栈则在 depth > 1 时才可点。

什么时候该用哪种写法

场景用哪个 API
进入一个详情页`push(item / url, properties)`
返回上一页`pop()`
回到某个指定页面(如首页)`pop(targetItem)`
弹到只剩栈底首页`pop(null)`
清空整个栈(含初始页)`clear()`
换掉当前页且不让用户回退`replace(item)`
跳过动画、立即切换加第三个参数 `StackView.Immediate`
流程化进入多页(向导、多步表单)`pushItems(items)`

depth 贯穿所有场景:它是栈当前的真实高度,任何按钮要不要可用、栈空没空、该弹几层,都该问它,而不是自己维护计数器。

运行验证

  1. Qt Creator 打开 qml_stackview/CMakeLists.txt,按 Ctrl+R 运行;
  2. 左侧点「基本操作」,连点 Push 叠加几页,观察底部 Depth 数字递增、Pop 按钮从置灰变为可点,再点 Replace 看栈深度不变但页面文字被替换;
  3. 点「批量操作」,分别点批量 Push、批量 Pop、清空栈,看 Depth 一次跳 5、跳 -5、回落到 1。

扩展复用方向

  • 业务里"详情页→编辑页→确认页"这种强制流程,用 pushItems 一次进栈,用户中途返回时按逆序逐层弹,天然保证不能乱跳;
  • push 的第二个参数是传参通道,往详情页带 idtitle 这类数据时直接塞进 properties 对象,页面声明同名属性即可接收;
  • 需要"回到首页"按钮的页面,与其循环 pop,不如记录首页对象后 pop(home) 一步到位,还自带从当前层回到栈底的转场动画。

已验证环境