QML 焦点导航:Tab 顺序与方向键移动

回到了上一篇:界面上一堆可交互元素,焦点怎么在它们之间移动?这一篇讲 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 顺序没有特殊配置,就是声明顺序 :从上往下、先写的先聚焦。这里 f1f2f3f4 依次排列,Tab 就从用户名走到邮箱再到电话、地址,Tab 到底之后循环回开头,Shift+Tab 反向。布局代码里谁先谁后,就是 Tab 的顺序。

实时反馈当前焦点

每个输入框的 onActiveFocusChangedactiveFocus 变 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 已经存在。如果边建格子边配导航,配到前面格子时它的邻居可能还没建出来。先把所有格子收集齐,再一次过连网,就能避免这类"引用还不存在"的问题。

运行验证

  1. Qt Creator 打开示例的 CMakeLists.txt
  2. Ctrl+R 运行;
  3. Tab 顺序导航:点第一个输入框,连按 Tab 看焦点依次走过用户名→邮箱→电话→地址,底部"当前焦点"同步跳转;按 Shift+Tab 反向走;
  4. 方向键导航:点击任意格子,然后按上、下、左、右方向键,看焦点在 3×3 网格里上下左右移动;按到边缘时不会再越界。

扩展复用方向

  • 表单里遇到 TextField,默认 activeFocusOnTab: true 就够用,无需手写导航
  • 九宫格菜单、卡片宫格、日历格子选日期,KeyNavigation 都是合适的选择
  • 若某个格子不想让方向键跳到它,把对应方向设成 null 即可,灵活度很高

已验证环境

相关推荐
程与留26 分钟前
11_常用控件速查(上):QPushButton、QLabel、QLineEdit、QComboBox
c++·qt
Quz30 分钟前
QML 按键事件之方向控制
qt
Littlehero_12114 小时前
QT自定义控件之内嵌报表(4)(源码开源)
开发语言·qt
lqj_本人18 小时前
鸿蒙跨端实战_QT后台切换导致OpenGLES上下文丢失:黑屏自救指南
qt·华为·harmonyos
小小龙学IT20 小时前
第七节 C++ 与 QML 交互
开发语言·qt
qq_4017004120 小时前
TCP 粘包问题深度解析:嵌入式网络开发的常见坑与解决方案
服务器·网络·qt·网络协议·tcp/ip
ShineWinsu21 小时前
对于TRAE中配置Qt的解析
开发语言·c++·ide·vscode·qt·ai·trae
小小龙学IT21 小时前
第一节 QML 背景介绍
c++·qt·js
此陆昭昭1 天前
qt,使用qml写一个简单的界面
c++·qt