Qt Quick 中键盘焦点#
处理键盘焦点
当按键被按下或释放时,会生成一个按键事件,并将其传递给聚焦的 Qt Quick Item 。为了方便构建可重用的组件并解决某些独特的流动用户界面场景,Qt Quick 项为 Qt 的传统键盘焦点模型添加了一个基于范围的扩展。
按键处理概述#
当用户按下或释放按键时,会发生以下操作
Qt 接收按键操作并生成一个按键事件。
如果一个
QQuickWindow
是应用程序的聚焦窗口,那么按键事件将被传递给它。按键事件由场景传递给具有 活动焦点 的 Item 。如果没有项具有活动焦点,则忽略按键事件。
如果具有活动焦点的
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; } } }如果达到根 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
类型具有活动焦点,按下 A
、B
或 C
键适当地修改文本。
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
属性的方法就不再足够了。
为了演示,我们创建了之前定义的组件的两个实例,并将第一个设置为具有焦点。我们的意图是当按下 A
、B
或 C
键时,两个组件中的第一个接收事件并相应地做出反应。
导入并创建两个 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
。然而,通过运行代码,我们可以确认第二个小部件接收了焦点。
查看 MyWidget
和 window
的代码,问题显而易见——有三个类型将 focus
属性设置为 true
。两个 MyWidget
将 focus
设置为 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 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) } } } }
虽然这个例子很简单,但在幕后有很多事情在进行。每当当前项发生变化时,ListView 会设置代理的 Item::focus
属性。由于 ListView 是一个焦点范围,这不会影响应用的其他部分。然而,如果 ListView 本身有活动的焦点,这会导致代理本身接收到活动的焦点。在这个例子中,代理的根类型也是一个焦点范围,这反过来又给了执行处理 Return
键实际工作的 TextInput 类型活动的焦点。