回到了上一篇:界面上一堆可交互元素,焦点怎么在它们之间移动?这一篇讲 QML 原生的两种导航方式------按 Tab 键在控件间循环,以及用方向键在网格里上下左右移动。前者靠控件的"Tab 顺序"默认机制,后者靠 KeyNavigation 附加属性声明"下个焦点是谁"。
这篇介绍两个 demo:
- Tab 顺序导航 --- 四个输入框,按 Tab 依次聚焦,底部实时显示当前焦点是哪个
- 方向键导航 --- 3×3 网格里用方向键在九宫格之间移动
Tab 顺序导航
一个常见的注册表单:用户名、邮箱、电话、地址四个输入框。用 Tab 键,焦点会一个接一个往下走;用 Shift+Tab 则往回走。底部一行文字实时显示当前焦点落在哪个字段,方便看清顺序。

演示代码
qml
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 时,底端文字跟着走,你就能直观验证顺序是否符合预期。这行体现的是 activeFocus 属性在"检测此刻谁有焦点"上的价值。
这是 QML 的默认行为,不是手工实现的
值得强调:整个 demo 除了打开 activeFocusOnTab、写了个状态回显,没写任何"按 Tab 跳到下一个"的逻辑。Tab 导航的循环是 QML 内置的,你只需要声明哪些元素可以进循环,剩下交给自己。理解这一点,才知道什么时候要手动(见下一篇的方向键),什么时候不用动。
方向键导航:KeyNavigation
Tab 只能上一项下一项,对于 3×3 的网格界面却不够直观------用户更想按"↑↓←→"上下左右移动。这就是 KeyNavigation 的用武之地:它是一个附加属性,能声明"按左键跳到谁、按右键跳到谁、按上跳到谁、按下跳到谁",做成一个二维的焦点地图。

演示代码
qml
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 声明"跳到谁"
以中间的格子为例:
qml
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 个格子是按 Navigation 里行列的规律摆放的。怎么知道某格右边是谁?看索引规律:一行 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
- GitHub:QML-Minimal-Demos/qml_keys_focus