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键处理。

另请参阅Keys附加属性KeyNavigation附加属性

查询活动焦点项目

是否一个Item具有活动焦点可以通过Item::activeFocus属性查询。例如,这里我们有一个Text类型,其文本由它是否具有活动焦点决定。

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

获取焦点和焦点范围

一个Item通过将focus属性设置为true来请求焦点。

对于非常简单的情况,有时只需设置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'
        }
    }
}
../_images/declarative-qmlfocus1.png

然而,如果上述示例被用作可重用或导入的组件,这种简单的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。然而,通过运行代码,我们可以确认第二个小部件获得了焦点。

../_images/declarative-qmlfocus2.png

查看MyWidgetwindow代码,问题很明显 - 有三种类型将focus属性设置为true。两个MyWidgetfocus设置为truewindow组件也设置了焦点。最终,只有一个类型可以获得键盘焦点,系统必须决定哪个类型接收焦点。当第二个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'
        }
    }
}
../_images/declarative-qmlfocus3.png

从概念上讲,焦点范围非常简单。

  • 在每个焦点范围内,一个对象可能将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 } }
}
../_images/declarative-qmlfocus4.png

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

请参阅 Qt Quick 示例 - 键交互 以了解如何使用 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)
                }
        }
    }
}
../_images/declarative-qmlfocus5.png

虽然这个例子很简单,但背后有很多事情在发生。每当当前项目发生变化时,ListView会设置委托的Item::focus属性。由于ListView是一个焦点范围,这不会影响应用程序的其余部分。然而,如果ListView本身具有活动焦点,这将导致委托本身接收活动焦点。在这个例子中,委托的根类型也是一个焦点范围,这反过来又给TextInput类型提供了活动焦点,该类型实际上执行处理Return键的工作。

所有的QML视图类,例如PathViewGridView,都以类似的方式运行,以允许在各自的委托中进行键处理。