目录
- Tab 顺序导航
- 演示代码
- 关键逻辑解析
- 方向键导航:KeyNavigation
- 演示代码
- 关键逻辑解析
- 运行验证
- 小结
上一篇讲了焦点怎么获取和管理,这一篇讲焦点怎么在元素之间移动。QML 原生有两种导航方式——按 Tab 键在控件间循环,以及用方向键在网格里上下左右移动。前者靠控件的"Tab 顺序"默认机制,后者靠KeyNavigation附加属性声明"下个焦点是谁"。
Tab 顺序导航
一个常见的注册表单:用户名、邮箱、电话、地址四个输入框。用 Tab 键,焦点会一个接一个往下走;用 Shift+Tab 则往回走。底部一行文字实时显示当前焦点落在哪个字段,方便看清顺序。
演示代码
import QtQuick import QtQuick.Controls import QtQuick.Layouts FadeInAnimation { ColumnLayout { id: root anchors.fill: parent anchors.margins: 20 spacing: 15 property string currentName: "无" TitleSeparator { title: "Tab 顺序导航" description: "默认情况下 QML 按 ActiveFocusOnTab 标记与子项声明顺序进行 Tab/Shift+Tab 导航" } // 表单:演示 Tab 默认顺序 Rectangle { Layout.fillWidth: true Layout.preferredHeight: 220 radius: 8 color: "#FAFAFA" border.color: "#ddd" border.width: 1 GridLayout { anchors.fill: parent anchors.margins: 14 columns: 2 columnSpacing: 16 rowSpacing: 10 Label { text: "用户名:"; font.pointSize: 11; color: "#666" } TextField { id: f1 Layout.fillWidth: true placeholderText: "Tab 顺序 1" activeFocusOnTab: true onActiveFocusChanged: { if (activeFocus) root.currentName = "用户名" } } Label { text: "邮箱:"; font.pointSize: 11; color: "#666" } TextField { id: f2 Layout.fillWidth: true placeholderText: "Tab 顺序 2" activeFocusOnTab: true onActiveFocusChanged: { if (activeFocus) root.currentName = "邮箱" } } Label { text: "电话:"; font.pointSize: 11; color: "#666" } TextField { id: f3 Layout.fillWidth: true placeholderText: "Tab 顺序 3" activeFocusOnTab: true onActiveFocusChanged: { if (activeFocus) root.currentName = "电话" } } Label { text: "地址:"; font.pointSize: 11; color: "#666" } TextField { id: f4 Layout.fillWidth: true placeholderText: "Tab 顺序 4" activeFocusOnTab: true onActiveFocusChanged: { if (activeFocus) root.currentName = "地址" } } } } Text { text: "当前焦点:" + root.currentName font.pointSize: 11 color: "#666" } Item { Layout.fillHeight: true } } }关键逻辑解析
activeFocusOnTab 决定能不能被 Tab 到
每个输入框都写了activeFocusOnTab: true。这个属性是导航的开关——它声明"这个元素允许通过 Tab 键获得焦点"。不是所有控件默认都能被 Tab 到,所以想让它进 Tab 循环就显式打开。这样四个输入框才都成为 Tab 的目标。
顺序来自声明顺序
Tab 顺序没有特殊配置,就是声明顺序:从上往下、先写的先聚焦。这里f1→f2→f3→f4依次排列,Tab 就从用户名走到邮箱再到电话、地址,Tab 到底之后循环回开头,Shift+Tab 反向。布局代码里谁先谁后,就是 Tab 的顺序。
实时反馈当前焦点
每个输入框的onActiveFocusChanged在activeFocus变 true 时,把对应的字段名写到底部"当前焦点"。按 Tab 时,底端文字跟着走,就能直观验证顺序是否符合预期。
这是 QML 的默认行为,不是手工实现的
整个 demo 除了打开activeFocusOnTab、写了个状态回显,没写任何"按 Tab 跳到下一个"的逻辑。Tab 导航的循环是 QML 内置的,你只需要声明哪些元素可以进循环,剩下交给自己。理解这一点,才知道什么时候要手动(见下面的方向键),什么时候不用动。
方向键导航:KeyNavigation
Tab 只能上一项下一项,对于 3×3 的网格界面却不够直观——用户更想按"↑↓←→"上下左右移动。这就是KeyNavigation的用武之地:它是一个附加属性,能声明"按左键跳到谁、按右键跳到谁、按上跳到谁、按下跳到谁",做成一个二维的焦点地图。
演示代码
import QtQuick import QtQuick.Layouts FadeInAnimation { ColumnLayout { anchors.fill: parent anchors.margins: 20 spacing: 15 TitleSeparator { title: "KeyNavigation 方向键导航" description: "KeyNavigation 附加属性为方向键提供声明式焦点跳转目标。常用于网格类、卡片选择、九宫格菜单。" } // 3x3 网格,方向键导航 Rectangle { Layout.fillWidth: true Layout.preferredHeight: 280 radius: 10 color: "#FAFAFA" border.color: "#ddd" border.width: 1 Grid { id: grid anchors.centerIn: parent columns: 3 spacing: 12 // 保存 9 个格子 property var cells: [] Repeater { model: 9 delegate: Rectangle { id: cell required property int index width: 90 height: 70 radius: 8 color: cell.activeFocus ? "#1296FF" : "#FFF" border.color: cell.activeFocus ? "#1296FF" : "#ccc" border.width: cell.activeFocus ? 2 : 1 // 让矩形本身可获取焦点 activeFocusOnTab: true // 把自己登记到 grid.cells,供导航使用 Component.onCompleted: grid.cells[index] = cell Text { anchors.centerIn: parent text: (index + 1) font.pointSize: 20 font.bold: true color: cell.activeFocus ? "#FFF" : "#666" } MouseArea { anchors.fill: parent onClicked: cell.forceActiveFocus() } } } // 所有格子创建完成后,统一设置方向键导航目标 Component.onCompleted: { for (var i = 0; i < grid.cells.length; i++) { var c = grid.cells[i] c.KeyNavigation.right = (i % 3 < 2) ? grid.cells[i + 1] : null c.KeyNavigation.left = (i % 3 > 0) ? grid.cells[i - 1] : null c.KeyNavigation.down = (Math.floor(i / 3) < 2) ? grid.cells[i + 3] : null c.KeyNavigation.up = (Math.floor(i / 3) > 0) ? grid.cells[i - 3] : null } } } } // 状态提示 Text { text: "提示:方向键 / Tab 切换单元格,鼠标点击亦可" font.pointSize: 11 color: "#666" } Item { Layout.fillHeight: true } } }关键逻辑解析
用 KeyNavigation 声明跳到谁
以中间的格子为例:
c.KeyNavigation.right = grid.cells[i + 1] c.KeyNavigation.left = grid.cells[i - 1] c.KeyNavigation.down = grid.cells[i + 3] c.KeyNavigation.up = grid.cells[i - 3]KeyNavigation.right表示"按下右箭头,焦点移向这个元素"。这四个属性各管一个方向,把每个格子下一步该去哪都写清楚,就构成了一张完整的方向键导航网。它不像用手写Keys.onPressed去逐个判断按键,而是声明式地给出目标。
需要 accessible 元素才能接收
方向键能不能导航,还取决于目标能否获得焦点。demo 里每个格子activeFocusOnTab: true——这不仅让它们能被 Tab 到,也是能作为KeyNavigation目标的前提。否则格子虽有外观,却不进焦点系统,导航就会失灵。
用索引算邻居
9 个格子按行列规律摆放。怎么知道某格右边是谁?看索引规律:一行 3 个,往右是i+1、往左是i-1、往下一行是i+3、往上一行是i-3。代码用取余i % 3判断是不是行首/行尾(行尾右边没人就null),用整除floor(i / 3)判断是不是第一/最后一行——这样每个格子的四个邻居都能正确求出,不会越到网格外面。
先收集、后连网
Repeater生成每个格子时并不直接写KeyNavigation,而是先把自己登记进grid.cells[index]。等 9 个格子全建好后,才在Component.onCompleted里统一用循环给每个格子配好四个方向的跳转目标。
原因很实际:给格子 i 指定"右边是格子 i+1",前提是格子 i+1 已经存在。如果边建格子边配导航,配到前面格子时它的邻居可能还没建出来。先把所有格子收集齐,再一次过连网,就能避免这类"引用还不存在"的问题。
运行验证
- Qt Creator 打开示例的
CMakeLists.txt; - 按
Ctrl+R运行; - Tab 顺序导航:点第一个输入框,连按 Tab 看焦点依次走过用户名→邮箱→电话→地址,底部"当前焦点"同步跳转;按 Shift+Tab 反向走;
- 方向键导航:点击任意格子,然后按上、下、左、右方向键,看焦点在 3×3 网格里上下左右移动;按到边缘时不会再越界。
小结
表单里遇到TextField,默认activeFocusOnTab: true就够用,无需手写导航。九宫格菜单、卡片宫格、日历格子选日期,KeyNavigation都是合适的选择。若某个格子不想让方向键跳到它,把对应方向设成null即可,灵活度很高。
已验证环境:
- Qt 版本:Qt 6.11.1
- 操作系统:Windows 11
- 项目地址:QML 示例合集V2.0 - qml_keys_focus