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::activeFocus 属性查询 Item 是否具有活动焦点。例如,这里有一个 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 设置为 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'
        }
    }
}
../_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 本身有活动的焦点,这会导致代理本身接收到活动的焦点。在这个例子中,代理的根类型也是一个焦点范围,这反过来又给了执行处理 Return 键实际工作的 TextInput 类型活动的焦点。

所有的 QML 视图类,如 PathViewGridView ,行为类似,允许在各自的代理中处理键。