Qt Quick 中的键盘焦点

当按键被按下或释放时,会生成一个按键事件并将其传递给具有聚焦的 Qt Quick Item。为了便于构建可重用的组件并解决一些适用于流体用户界面的独特情况,Qt Quick 项向 Qt 的传统键盘焦点模型添加了一个基于范围的扩展。

按键处理概述

当用户按下或释放按键时,以下操作发生

  1. Qt 接收到按键动作并生成一个按键事件。
  2. 如果 QQuickWindow 是应用程序的聚焦窗口,则按键事件将传递给它。
  3. 按键事件由场景传递到具有 活动聚焦Item。如果没有项有活动聚焦,则忽略按键事件。
  4. 如果具有活动聚焦的 QQuickItem 接受按键事件,则传播停止。否则,事件将传递到项的父项,直到事件被接受或达到根项。

    如果在以下示例中的 Rectangle 类型具有活动聚焦并且按下 A 键,则事件将不会被进一步传播。按下 B 键时,事件将传播到根项,因此被忽略。

    Rectangle {
        width: 100; height: 100
        focus: true
        Keys.onPressed: (event)=> {
            if (event.key == Qt.Key_A) {
                console.log('Key A was pressed');
                event.accepted = true;
            }
        }
    }
  5. 如果达到根 Item,则按键事件会被 忽略,并且继续进行常规的 Qt 按键处理。

另请参阅 带属性按键按键导航属性

查询活动聚焦项

可以通过查询 Item::activeFocus 属性来了解一项是否有活动聚焦。例如,这里有一个 Text 类型,其文本取决于是否有活动聚焦。

    Text {
        text: activeFocus ? "I have active focus!" : "I do not have active focus"
    }

获取聚焦和聚焦范围

通过将 focus 属性设置为 true 来请求 QQuickItem 的聚焦。

对于非常简单的情况,仅设置 focus 属性有时就足够了。如果我们用 qml 工具 运行以下示例,我们会看到 keyHandler 类型具有活动聚焦,并且按下 ABC 键会相应地修改文本。

Rectangle {
    color: "lightsteelblue"; width: 240; height: 25
    Text { id: myText }
    Item {
        id: keyHandler
        focus: true
        Keys.onPressed: (event)=> {
            if (event.key == Qt.Key_A)
                myText.text = 'Key A was pressed'
            else if (event.key == Qt.Key_B)
                myText.text = 'Key B was pressed'
            else if (event.key == Qt.Key_C)
                myText.text = 'Key C was pressed'
        }
    }
}

但是,如果上面的示例用作可重复使用或导入的组件,则这种简单的 focus 属性使用就不再足够。

为了演示,我们创建了之前定义组件的两个实例,并将第一个实例设置为具有焦点。目的是当按下ABC键时,前两个组件之一接收到事件并相应地做出响应。

导入并创建两个MyWidget实例的代码

//Window code that imports MyWidget
Rectangle {
    id: window
    color: "white"; width: 240; height: 150

    Column {
        anchors.centerIn: parent; spacing: 15

        MyWidget {
            focus: true             //set this MyWidget to receive the focus
            color: "lightblue"
        }
        MyWidget {
            color: "palegreen"
        }
    }
}

MyWidget代码

Rectangle {
    id: widget
    color: "lightsteelblue"; width: 175; height: 25; radius: 10; antialiasing: true
    Text { id: label; anchors.centerIn: parent}
    focus: true
    Keys.onPressed: (event)=> {
        if (event.key == Qt.Key_A)
            label.text = 'Key A was pressed'
        else if (event.key == Qt.Key_B)
            label.text = 'Key B was pressed'
        else if (event.key == Qt.Key_C)
            label.text = 'Key C was pressed'
    }
}

我们希望第一个MyWidget对象具有焦点,因此我们将它的focus属性设置为true。然而,通过运行代码,我们可以确认第二个小部件获得了焦点。

查看MyWidgetwindow代码,问题很明显——有三个类型将其focus属性设置为true。两个MyWidget将焦点设置为true,而window组件也将焦点设置。最终,只能有一个类型可以获得键盘焦点,系统必须决定哪个类型接收该焦点。当第二个MyWidget创建时,它因为最后一个将focus属性设置为true的类型而获得焦点。

这个问题是由于可见性引起的。MyWidget组件希望能够获得焦点,但它不能控制在被导入或复用时焦点。同样,window组件也没有知道其导入组件是否请求焦点的能力。

为了解决这个问题,QML引入了一个称为焦点范围的概念。对于现有Qt用户,焦点范围类似于自动焦点代理。通过声明FocusScope类型来创建焦点范围。

在下一个示例中,将一个FocusScope类型添加到组件中,并展示其可视化结果。

FocusScope {

    //FocusScope needs to bind to visual properties of the Rectangle
    property alias color: rectangle.color
    x: rectangle.x; y: rectangle.y
    width: rectangle.width; height: rectangle.height

    Rectangle {
        id: rectangle
        anchors.centerIn: parent
        color: "lightsteelblue"; width: 175; height: 25; radius: 10; antialiasing: true
        Text { id: label; anchors.centerIn: parent }
        focus: true
        Keys.onPressed: (event)=> {
            if (event.key == Qt.Key_A)
                label.text = 'Key A was pressed'
            else if (event.key == Qt.Key_B)
                label.text = 'Key B was pressed'
            else if (event.key == Qt.Key_C)
                label.text = 'Key C was pressed'
        }
    }
}

从概念上讲,焦点范围相当简单。

  • 在每一个焦点范围内,一个对象可能将Item::focus设置为true。如果有超过一个Item设置了focus属性,最后一个设置focus的类型将获得焦点,其他类型将被设置为未设置,类似于没有焦点范围的情况。
  • 当一个焦点范围获得活动焦点时,其中设置focus(如果有的话)的类型也会获得活动焦点。如果这个类型也是一个FocusScope,代理行为将会继续。焦点范围和子焦点项都将有activeFocus属性设置为。

请注意,由于FocusScope类型不是视觉类型,因此其子项的属性需要暴露给FocusScope的父项。布局和定位类型将使用这些视觉和样式属性来创建布局。在我们的例子中,Column类型不能正确显示两个小部件,因为FocusScope缺少自己的视觉属性。MyWidget组件直接绑定到rectangle属性,以便让Column类型创建包含FocusScope子项的布局。

到目前为止,我们的示例静态选择了第二个组件。现在将这个组件扩展为可点击,并添加到原始应用程序中是简单的。我们仍然将其中一个小部件设置为默认焦点。现在,点击任何一个MyClickableWidget都会使它获得焦点,而其他小部件会失去焦点。

导入并创建两个MyClickableWidget实例的代码

Rectangle {
    id: window

    color: "white"; width: 240; height: 150

    Column {
        anchors.centerIn: parent; spacing: 15

        MyClickableWidget {
            focus: true             //set this MyWidget to receive the focus
            color: "lightblue"
        }
        MyClickableWidget {
            color: "palegreen"
        }
    }

}

MyClickableWidget代码

FocusScope {

    id: scope

    //FocusScope needs to bind to visual properties of the children
    property alias color: rectangle.color
    x: rectangle.x; y: rectangle.y
    width: rectangle.width; height: rectangle.height

    Rectangle {
        id: rectangle
        anchors.centerIn: parent
        color: "lightsteelblue"; width: 175; height: 25; radius: 10; antialiasing: true
        Text { id: label; anchors.centerIn: parent }
        focus: true
        Keys.onPressed: (event)=> {
            if (event.key == Qt.Key_A)
                label.text = 'Key A was pressed'
            else if (event.key == Qt.Key_B)
                label.text = 'Key B was pressed'
            else if (event.key == Qt.Key_C)
                label.text = 'Key C was pressed'
        }
    }
    MouseArea { anchors.fill: parent; onClicked: { scope.focus = true } }
}

当QML 明确放弃焦点(在其具有活动焦点时将其 focus 属性设置为 false),系统不会自动选择其他类型来接收焦点。也就是说,可能没有当前活动焦点。

请参阅 Qt Quick 例子 - 键交互,以演示如何使用 FocusScope 类型在多个区域之间移动键盘焦点。

FocusScope 的高级应用

FocusScope 允许轻松地将焦点分配进行分区。几个QML项都用于此目的。

例如,ListView 自身就是一个焦点作用域。一般情况下,这并不明显,因为 ListView 通常没有手动添加的视觉子项。作为焦点作用域,ListView 可以专注于当前列表项而无需担心这会对应用程序的其他部分造成什么影响。这将允许当前项代表对按键做出反应。

这个假设的例子显示了它是如何工作的。按 Return 键将打印当前列表项的名称。

Rectangle {
    color: "lightsteelblue"; width: 100; height: 50

    ListView {
        anchors.fill: parent
        focus: true

        model: ListModel {
            ListElement { name: "Bob" }
            ListElement { name: "John" }
            ListElement { name: "Michael" }
        }

        delegate: FocusScope {
                width: childrenRect.width; height: childrenRect.height
                x:childrenRect.x; y: childrenRect.y
                TextInput {
                    focus: true
                    text: name
                    Keys.onReturnPressed: console.log(name)
                }
        }
    }
}

虽然这个例子很简单,但幕后有很多事情在进行。每当当前项改变时,ListView 都会设置委托的 Item::focus 属性。由于 ListView 是焦点作用域,这不会影响其他应用程序。但是,如果 ListView 本身具有活动焦点,这将导致委托本身接收活动焦点。在这个例子中,委托的根类型也是一个焦点作用域,这反过来又让 TextInput 类型获得活动焦点,它实际处理 Return 键的工作。

所有QML视图类,如PathViewGridView,都以类似方式行为,以便在其各自的委托中处理按键。

© 2024 The Qt Company Ltd. 本文档中的贡献是各自所有者的版权。此处提供的文档是根据由自由软件基金会发布的GNU自由文档许可证版本1.3的条款许可的。Qt及其相关标志是芬兰的 The Qt Company Ltd. 以及/或世界其他国家的商标。所有其他商标均为其各自所有者的财产。