QML 常见的四种导航布局

本文主要介绍 qml_navigation_layout(QML导航布局示例工程,下载链接在结尾处),整合了 Qt Quick Controls 开发中最常用的四种导航样式:顶部导航、抽屉式导航、侧边导航栏、底部导航栏,几乎覆盖了日常软件开发中所有主流的导航界面场景。

提前分清:两个极易混淆的页面切换组件

在动手实操四种导航布局前,先吃透两个名字相似、用法和场景完全不同的核心组件,这是避开新手误区、理解导航切换逻辑的关键。

1. StackLayout 堆叠布局

StackLayout(堆叠布局) 的核心逻辑是将所有子页面层层堆叠在一起,同一时间只展示单个页面。它依靠 currentIndex 属性精准控制当前显示的页面,页面切换为瞬时完成,无任何过渡动画。

该组件专门适配平级页面切换场景,比如常见的标签页 Tab 切换,页面之间无先后顺序、无层级隶属关系,仅需快速切换展示内容。

2. StackView 页面栈

StackView(页面栈) 用于管理存在先后层级关系的页面,和 StackLayout 的平级逻辑完全不同。

可以简单理解为页面堆栈:需要打开新页面时,调用 push 方法将页面压入栈中;需要返回上一页时,调用 pop 方法弹出当前页面。页面栈始终只展示最顶层页面,完整记录了页面跳转轨迹,天然支持前进、后退操作,还能为页面进出配置专属转场动画,适配各类层级跳转业务。

核心组件选型总结

  1. 页面为平级并列关系 、仅需快速切换(标签页切换),优先使用 StackLayout
  2. 页面存在层级跳转、返回逻辑 ,需要转场动画,优先使用 StackView
  3. 本工程中的抽屉式导航为特殊实现,无需使用以上两个组件,内容区域直接通过矩形组件搭建,逻辑更简洁直观。

Demo 1 顶部导航栏

顶部导航栏采用经典的三段式布局。内容区域由 StackView 页面栈全权管理,页面前进时,新页面从右侧滑入、旧页面向左滑出。

演示代码

yaml 复制代码
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
FadeInAnimation {
    id: root
    QtObject {
        id: nav
        property int currentIndex: 0
        property var pages: [
            { name: "首页", color: "#3498db" },
            { name: "文件", color: "#9b59b6" },
            { name: "设置", color: "#e67e22" }
        ]
    }
    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 15
        // ... 省略标题组件 TitleSeparator ...
        RowLayout {
            Layout.fillWidth: true
            spacing: 10
            Button {
                text: "<<"
                enabled: nav.currentIndex > 0
                onClicked: {
                    nav.currentIndex--
                    stackView.pop()
                }
            }
            Text {
                Layout.fillWidth: true
                horizontalAlignment: Text.AlignHCenter
                text: nav.pages[nav.currentIndex].name
                font.pointSize: 10
            }
            Button {
                text: ">>"
                enabled: nav.currentIndex < nav.pages.length - 1
                onClicked: {
                    nav.currentIndex++
                    // 页面索引通过参数注入:若让每个页面都绑定 nav.currentIndex,
                    // 切换时栈内所有页面会一起变色。
                    stackView.push(pageComponent, { "idx": nav.currentIndex })
                }
            }
        }
        StackView {
            id: stackView
            Layout.fillWidth: true
            Layout.fillHeight: true
            // 转场时页面从边界外滑入,必须裁剪,否则会溢出到左侧导航栏
            clip: true
            initialItem: pageComponent
            pushEnter: Transition {
                PropertyAnimation {
                    property: "x"
                    from: stackView.width
                    to: 0
                    duration: 200
                }
                PropertyAnimation {
                    property: "opacity"
                    from: 0
                    to: 1
                    duration: 200
                }
            }
            // ... 省略 pushExit / popEnter / popExit 三组转场,写法对称、方向相反 ...
        }
    }
    Component {
        id: pageComponent
        Rectangle {
            id: pageRect
            property int idx: 0
            color: nav.pages[pageRect.idx].color
            radius: 4
            Text {
                anchors.centerIn: parent
                text: nav.pages[pageRect.idx].name + "内容"
                color: "#fff"
                font.pointSize: 12
                font.bold: true
            }
        }
    }
}

关键逻辑解析

本示例采用职责分离的清晰设计:StackView 专门负责页面渲染和层级跳转,而 nav.currentIndex 仅控制顶部标题与切换按钮状态。两者解耦,从根本上避免标题和页面状态不匹配的问题。

核心实用开发小技巧:通过 push(pageComponent, { "idx": nav.currentIndex }) 以传参方式为新页面注单独传入索引值参数,页面通过以 property int idx 接收使用。禁止页面不要直接绑定全局的 nav.currentIndex,否则切换页面时,栈内所有历史页面会同步刷新样式,引发逻辑错误出现异常 bug。

适用场景:向导式多步流程、软件设置页层级跳转、需要「返回」的层级导航场景。

Demo 2 抽屉式导航

抽屉式导航是轻量化临时导航方案,默认隐藏不占用屏幕空间,点击按钮即可从内容区左侧滑出深色菜单面板。点击菜单项切换页面,或点击背景遮罩,均可收起抽屉面板,交互简洁轻便。

演示代码

yaml 复制代码
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
FadeInAnimation {
    id: root
    QtObject {
        id: drawerNav
        property int currentIndex: 0
        property bool isOpen: false
        property var pages: [
            { name: "首页", color: "#3498db" },
            { name: "消息", color: "#9b59b6" },
            { name: "设置", color: "#e67e22" }
        ]
    }
    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 15
        // ... 省略标题组件 TitleSeparator ...
        RowLayout {
            Layout.fillWidth: true
            spacing: 10
            Button {
                implicitWidth: 120
                implicitHeight: 40
                text: drawerNav.isOpen ? "关闭菜单" : "打开菜单"
                onClicked: drawerNav.isOpen = !drawerNav.isOpen
            }
            Text {
                Layout.fillWidth: true
                horizontalAlignment: Text.AlignRight
                text: "当前页面: " + drawerNav.pages[drawerNav.currentIndex].name
                font.pointSize: 10
                color: "#666"
            }
        }
        Rectangle {
            Layout.fillWidth: true
            Layout.fillHeight: true
            radius: 4
            color: drawerNav.pages[drawerNav.currentIndex].color
            Behavior on color {
                ColorAnimation {
                    duration: 200
                }
            }
            Text {
                anchors.centerIn: parent
                text: drawerNav.pages[drawerNav.currentIndex].name + "内容"
                color: "#fff"
                font.pointSize: 14
                font.bold: true
            }
        }
    }
    // 遮罩层:覆盖整个内容区,点击关闭抽屉
    Rectangle {
        id: drawerMask
        anchors.fill: parent
        color: "#80000000"
        visible: drawerNav.isOpen
        z: 1
        MouseArea {
            anchors.fill: parent
            onClicked: drawerNav.isOpen = false
        }
    }
    // 抽屉面板:始终位于内容区内,关闭时滑出左侧边界由容器裁剪
    Rectangle {
        id: drawerPanel
        anchors.top: parent.top
        anchors.bottom: parent.bottom
        width: 140
        x: drawerNav.isOpen ? 0 : -width
        color: "#34495e"
        z: 2
        Behavior on x {
            NumberAnimation {
                duration: 200
                easing.type: Easing.OutQuad
            }
        }
        ColumnLayout {
            anchors.fill: parent
            anchors.margins: 10
            spacing: 5
            Text {
                text: "导航菜单"
                font.pointSize: 11
                font.bold: true
                color: "#fff"
                Layout.fillWidth: true
                horizontalAlignment: Text.AlignHCenter
            }
            Repeater {
                model: drawerNav.pages
                Rectangle {
                    id: menuItem
                    required property int index
                    required property var modelData
                    Layout.fillWidth: true
                    Layout.preferredHeight: 34
                    radius: 4
                    color: drawerNav.currentIndex === menuItem.index ? menuItem.modelData.color : "transparent"
                    Text {
                        anchors.centerIn: parent
                        text: menuItem.modelData.name
                        font.pointSize: 10
                        color: "#fff"
                    }
                    MouseArea {
                        anchors.fill: parent
                        cursorShape: Qt.PointingHandCursor
                        onClicked: {
                            drawerNav.currentIndex = menuItem.index
                            drawerNav.isOpen = false
                        }
                    }
                }
            }
            Item {
                Layout.fillHeight: true
            }
        }
    }
}

关键逻辑解析

抽屉导航的代码逻辑非常轻量化,依靠单一 isOpen 状态即可统一控制面板显隐、位移动画和遮罩展示。按钮、遮罩、菜单项的所有交互,本质都是切换这个状态值,代码简洁、维护成本极低。

滑出动画无需手动触发,借助 Behavior on x 监听位移属性变化,搭配 200ms 缓动动画,即可实现贴合系统交互的自然滑出效果,是 QML 属性动画最简洁的实现方式。

新手需要注意两个细节:通过 z 层级区分遮罩与面板,避免面板被遮挡无法点击;依靠根节点 clip 裁剪,让抽屉关闭时可以完全隐藏,不会残留边缘画面。遮罩透明度可通过颜色值前两位数值自由调整。

适用场景:导航项数量多、无需常驻展示的界面,移动端汉堡菜单、软件临时侧边设置面板、小屏设备适配场景。

Demo 3 侧边导航栏

侧边导航栏与抽屉式导航逻辑相反,属于常驻型导航。

演示代码

yaml 复制代码
import QtQuick
import QtQuick.Layouts
FadeInAnimation {
    id: root
    QtObject {
        id: leftNav
        property int currentIndex: 0
        property var pages: [
            { name: "首页", color: "#3498db" },
            { name: "文件", color: "#9b59b6" },
            { name: "设置", color: "#e67e22" }
        ]
    }
    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 15
        // ... 省略标题组件 TitleSeparator ...
        RowLayout {
            Layout.fillWidth: true
            Layout.fillHeight: true
            spacing: 10
            Rectangle {
                Layout.preferredWidth: 60
                Layout.fillHeight: true
                radius: 4
                color: "#f0f0f0"
                ColumnLayout {
                    anchors.fill: parent
                    anchors.margins: 4
                    spacing: 4
                    Repeater {
                        model: leftNav.pages
                        Rectangle {
                            id: navItem
                            required property int index
                            required property var modelData
                            Layout.fillWidth: true
                            Layout.preferredHeight: 36
                            radius: 4
                            color: leftNav.currentIndex === navItem.index ? navItem.modelData.color : "transparent"
                            Text {
                                anchors.centerIn: parent
                                text: navItem.modelData.name
                                font.pointSize: 10
                                color: leftNav.currentIndex === navItem.index ? "#fff" : "#333"
                            }
                            MouseArea {
                                anchors.fill: parent
                                cursorShape: Qt.PointingHandCursor
                                onClicked: leftNav.currentIndex = navItem.index
                            }
                        }
                    }
                    // 撑开剩余空间,让导航项顶部对齐
                    Item {
                        Layout.fillHeight: true
                    }
                }
            }
            StackLayout {
                Layout.fillWidth: true
                Layout.fillHeight: true
                currentIndex: leftNav.currentIndex
                Repeater {
                    model: leftNav.pages
                    Rectangle {
                        id: contentPage
                        required property var modelData
                        color: contentPage.modelData.color
                        radius: 4
                        Text {
                            anchors.centerIn: parent
                            text: contentPage.modelData.name + "内容"
                            color: "#fff"
                            font.pointSize: 12
                            font.bold: true
                        }
                    }
                }
            }
        }
    }
}

关键逻辑解析

导航项通过 Repeater 批量生成,通过索引比对动态切换选中样式。底部空白 Item 利用填充高度特性,撑开剩余布局空间,让导航项置顶整齐排列,避免均匀分散、样式错乱。

内容区依托 StackLayout 实现平级瞬时切换,逻辑简单高效。它和抽屉导航的核心区别在于展示形态:侧边栏常驻页面、随时可视,抽屉导航默认隐藏、按需唤起,可根据屏幕空间自由选择。

适用场景:桌面端软件主界面、工具类应用模块切换、需要导航与内容长期同屏展示的场景。

Demo 4 底部导航栏

移动端 App 最主流的导航样式。

演示代码

yaml 复制代码
import QtQuick
import QtQuick.Layouts
FadeInAnimation {
    id: root
    QtObject {
        id: bottomNav
        property int currentIndex: 0
        property var pages: [
            { icon: "qrc:/icons/home.svg", label: "首页", color: "#3498db" },
            { icon: "qrc:/icons/search.svg", label: "发现", color: "#e74c3c" },
            { icon: "qrc:/icons/message.svg", label: "消息", color: "#f39c12" },
            { icon: "qrc:/icons/user.svg", label: "我的", color: "#9b59b6" }
        ]
    }
    ColumnLayout {
        anchors.fill: parent
        anchors.margins: 20
        spacing: 15
        // ... 省略标题组件 TitleSeparator ...
        Rectangle {
            Layout.fillWidth: true
            Layout.fillHeight: true
            radius: 4
            StackLayout {
                anchors.fill: parent
                currentIndex: bottomNav.currentIndex
                Repeater {
                    model: bottomNav.pages
                    Rectangle {
                        id: tabPage
                        required property var modelData
                        color: tabPage.modelData.color
                        Text {
                            anchors.centerIn: parent
                            text: tabPage.modelData.label
                            color: "#fff"
                            font.pointSize: 14
                            font.bold: true
                        }
                    }
                }
            }
        }
        Rectangle {
            Layout.fillWidth: true
            Layout.preferredHeight: 44
            color: "#f8f9fa"
            radius: 4
            border.color: "#dee2e6"
            RowLayout {
                anchors.fill: parent
                anchors.margins: 5
                spacing: 4
                Repeater {
                    model: bottomNav.pages
                    Rectangle {
                        id: tabButton
                        required property int index
                        required property var modelData
                        Layout.fillWidth: true
                        Layout.fillHeight: true
                        color: bottomNav.currentIndex === tabButton.index ? "#e3f2fd" : "transparent"
                        radius: 4
                        ColumnLayout {
                            anchors.centerIn: parent
                            spacing: 2
                            Image {
                                source: tabButton.modelData.icon
                                sourceSize.width: 18
                                sourceSize.height: 18
                                Layout.alignment: Qt.AlignHCenter
                            }
                            Text {
                                text: tabButton.modelData.label
                                font.pointSize: 8
                                color: bottomNav.currentIndex === tabButton.index ? tabButton.modelData.color : "#666"
                                Layout.alignment: Qt.AlignHCenter
                            }
                        }
                        MouseArea {
                            anchors.fill: parent
                            cursorShape: Qt.PointingHandCursor
                            onClicked: bottomNav.currentIndex = tabButton.index
                        }
                    }
                }
            }
        }
    }
}

关键逻辑解析

底部导航最大的优势是数据与视图解耦,通过一份pages 数组同时驱动顶部内容区和底部 Tab 栏。新增、删减导航页面只需修改数组,界面会自动同步更新,代码复用率高、维护简单。

图标设置固定 sourceSize 解码尺寸,避免 SVG 原图尺寸异常导致 Tab 布局变形。默认使用 StackLayout 实现快速 Tab 切换,若需要左右滑动效果,直接替换为 SwipeView 即可,拓展性良好。

适用场景:移动端 App 主界面、多平级模块快速切换、便捷操作的移动端交互场景。

四种导航方案选型对比

维度 顶部导航栏 抽屉式导航 侧边导航栏 底部导航栏
导航条位置 内容区上方 覆盖内容区,按需弹出 内容区左侧常驻 内容区下方
切换组件 StackView 页面栈 无需切换组件 StackLayout StackLayout
切换动效 滑动+淡入淡出 面板位移+遮罩渐变 瞬时切换,无动画 瞬时切换,无动画
页面历史 保留,支持返回上一级 不保留页面历史 不保留页面历史 不保留页面历史
屏幕占位 顶部窄横条,常驻 默认零占位,临时占用 左侧竖栏,常驻 底部窄横条,常驻
页面关系 有层级、有先后顺序 纯平级并列 纯平级并列 纯平级并列

快速选型核心思路

新手选型无需死记硬背,只需看两个核心条件:

  1. 看页面关系:页面存在层级跳转、需要返回上一步的业务场景,优先选顶部导航(StackView 页面栈);所有纯平级、仅需来回切换的页面,选择剩余三种方案即可。

  2. 看屏幕空间与操作习惯:桌面端屏幕空间充足,优先用常驻式侧边导航,操作直观;小屏设备、导航项较多时,用抽屉导航节省空间;移动端 App 优先用底部导航,贴合拇指单手操作习惯。

四种导航方案并非互斥,实际开发中可组合使用:常见搭配为底部 Tab 导航管理 APP 一级核心模块,每个模块内部通过顶部导航实现二级页面层级跳转,侧边导航、抽屉导航适配特殊常驻/临时页面场景,适配绝大多数复杂界面需求。

运行验证环境

相关推荐
江湖人称菠萝包3 小时前
【Qt】《Qt 5.9 C++开发指南》笔记-Chapter2-GUI应用程序设计基础
笔记·qt·qt5
重生的黑客4 小时前
Qt 常用控件精讲(1):QWidget 核心属性全解 —— 从 geometry 到 qrc 与 QSS
c++·qt·qwidget·qss·常用控件
江湖人称菠萝包5 小时前
【Qt】《Qt 5.9 C++开发指南》笔记-Chapter1-认识Qt
笔记·qt·qt5
Quz1 天前
QML StackView:实现卡片堆叠效果
qt
Quz1 天前
QML Drawer 抽屉导航详解
qt
≮傷£≯√1 天前
QGraphicsScene(一) drawCAD 问题
qt
qq_401700412 天前
Qt QUrl 详解与代码示例
开发语言·数据库·qt
雨田言炎2 天前
Qt的常用类QList--序列数据管理
嵌入式硬件·qt
朽棘不雕2 天前
Qt项目代码解释
开发语言·qt