QML 常见的四种导航布局

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

一图一文讲清四种导航的组件选型与常见坑点,含传参解耦、裁剪遮罩、图标解码等实战细节,适合 Qt Quick 初学者与需快速搭建主界面的开发者直接取用。

本文主要介绍 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 顶部导航栏

1.gif

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

演示代码

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

关键逻辑解析

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

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

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

Demo 2 抽屉式导航

2.gif

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

演示代码

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

关键逻辑解析

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

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

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

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

Demo 3 侧边导航栏

3.gif

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

演示代码

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

关键逻辑解析

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

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

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

Demo 4 底部导航栏

4.gif

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

演示代码

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

关键逻辑解析

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

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

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

四种导航方案选型对比

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

快速选型核心思路

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

  1. 看页面关系:页面存在层级跳转、需要返回上一步的业务场景,优先选顶部导航(StackView 页面栈);所有纯平级、仅需来回切换的页面,选择剩余三种方案即可。
  2. 看屏幕空间与操作习惯:桌面端屏幕空间充足,优先用常驻式侧边导航,操作直观;小屏设备、导航项较多时,用抽屉导航节省空间;移动端 App 优先用底部导航,贴合拇指单手操作习惯。

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

运行验证环境