前言
本节我们将学习键盘行为相关的Keys附加属性,其区别于QWidget的key事件,可以灵活给qml组件添加key的附加属性。
一、了解Keys
查阅帮助文档:
详细描述
所有视觉图元都支持通过Keys attached属性进行键处理。按键可以通过onPressed和onRelease信号属性进行处理。
信号属性有一个KeyEvent参数,名为event,其中包含事件的详细信息。如果处理了密钥,则应将event.accepted设置为true,以防止事件在项目层次结构中向上传播。
简单来说,Keys属于QML中的一种附加机制,可以为某个元素赋予键盘监听的能力。例如默认情况下,Rectangle不知道如何处理按键,此时给他设置一个Keys附加属性,就可以处理按键事件了。
再看一下它的信号:
可以看到一些digit0~9这些数字按键,也又空格spacePressed这种。如果需要更复杂的按键,可以使用pressed信号,通过其event获取。
再看一下KeyEvent的描述:
这里的枚举太多了,我也只是截取出一部分的。
接下来,我们将通过简单的实例,来学习如何处理按键事件。
二、特定信号和通用信号
直接引用信号处理器试试:
cpp
Window {
visible: true
width: 640
height: 480
title: qsTr("QmlKeysAttachedProperty")
Rectangle{
id: containedRectId
anchors.centerIn: parent
width: 300
height: 50
color: "dodgerblue"
focus: true
Keys.onDigit5Pressed: {
console.log("press 5")
}
}
}

cpp
Keys.onDigit5Pressed: {
console.log("press 5")
}
就这么简单,你甚至不需要像MouseArea那样创建一个鼠标组件,还要考虑它的范围大小。
这里我分别点击键盘上方和右侧数字键盘中的"5",都能触发打印。
但这种单一按键的信号处理太麻烦了,所以我们使用press这个通用信号看看。
cpp
Keys.onPressed: (event)=> {
if(event.key === Qt.Key_5){
console.log("press 5")
}
}
效果是正常的,也能打印信息。
如果这两个处理器都写了呢?你会发现Qt会优先选择特定信号。
这个时候如果你还是想触发通用信号,那你可以选择在特定信号中不接受信号,继续向下传递:
cpp
Keys.onDigit5Pressed: (event)=> {
console.log("press 5")
event.accepted = false
}
Keys.onPressed: (event)=> {
if(event.key === Qt.Key_5){
console.log("press 5...")
}
}

另外提一嘴,press只是其中一种行为,还有release这种松开的信号,实现形式是一样的。
三、处理Ctrl+按键
有一种比较常见的按键情况,是Ctrl+x这种形式,意味着要同时按下多个按键。这个时候,我们可以稍作判断:
cpp
Keys.onDigit5Pressed: (event)=> {
if(event.modifiers === Qt.ControlModifier){
console.log("press 5 with ctrl")
event.accepted = false
}else{
console.log("press 5")
event.accepted = false
}
}
modifiers是修饰符的意思,Qt.ControlModifier代表当前按下了Ctrl修饰符。
四、用键盘移动方块
之前我们已经学习过如果通过鼠标拖动来移动方块,这次我们试试通过键盘来实现:
cpp
Window {
visible: true
width: 640
height: 480
title: qsTr("QmlMouseArea")
Rectangle{
id: dragContainerId
width:parent.width
height:400
color: "beige"
y: 100
focus: true
Rectangle{
id: moveableRectId
x: 0
y: 0
width: 50
height: width
color: "blue"
onXChanged: {
console.log("The x position is: " + x)
}
}
Keys.onPressed: (event)=> {
if((event.key === Qt.Key_Up) || (event.key === Qt.Key_W)){
console.log("press up...")
moveableRectId.y -= 10
}else if((event.key === Qt.Key_Down) || (event.key === Qt.Key_S)){
console.log("press down...")
moveableRectId.y += 10
}else if((event.key === Qt.Key_Left) || (event.key === Qt.Key_A)){
console.log("press left...")
moveableRectId.x -= 10
}else if((event.key === Qt.Key_Right) || (event.key === Qt.Key_D)){
console.log("press right...")
moveableRectId.x += 10
}
}
}
}
可以看到,这里就是用了简单的条件判断,代码层级比较简单,直接看效果:
可以实现,甚至长按的时候还可以一直移动,就是有些不太灵敏。
问了一下kimi,应该是底层机制的一些问题,事件刷新频率什么的。
于是我让它直接给我修改了段代码,跑起来效果还不错,就是我目前还没完全理解,但还是放上来吧:
cpp
import QtQuick 2.15
import QtQuick.Window 2.15
Window {
visible: true
width: 640
height: 480
title: qsTr("Smooth Key Move")
Rectangle {
id: dragContainerId
width: parent.width
height: 400
color: "beige"
focus: true
/* 可移动矩形 */
Rectangle {
id: moveableRectId
x: 0; y: 0
width: 50; height: width
color: "blue"
}
/* 位图记录当前按下的方向 */
property int pressedKeys: 0
readonly property int key_up: 1 << 0
readonly property int key_down: 1 << 1
readonly property int key_left: 1 << 2
readonly property int key_right: 1 << 3
/* 16 ms 刷新计时器 → ~60 FPS 移动 */
Timer {
interval: 16
repeat: true
running: dragContainerId.pressedKeys !== 0
onTriggered: {
const step = 4; // 每帧像素
if (dragContainerId.pressedKeys & dragContainerId.key_up) moveableRectId.y -= step
if (dragContainerId.pressedKeys & dragContainerId.key_down) moveableRectId.y += step
if (dragContainerId.pressedKeys & dragContainerId.key_left) moveableRectId.x -= step
if (dragContainerId.pressedKeys & dragContainerId.key_right) moveableRectId.x += step
}
}
/* 按键按下:打开对应方向位 */
Keys.onPressed: (event) => {
switch (event.key) {
case Qt.Key_Up: case Qt.Key_W:
dragContainerId.pressedKeys |= dragContainerId.key_up; break;
case Qt.Key_Down: case Qt.Key_S:
dragContainerId.pressedKeys |= dragContainerId.key_down; break;
case Qt.Key_Left: case Qt.Key_A:
dragContainerId.pressedKeys |= dragContainerId.key_left; break;
case Qt.Key_Right:case Qt.Key_D:
dragContainerId.pressedKeys |= dragContainerId.key_right; break;
}
event.accepted = true; // 吃掉事件,避免系统 repeat 干扰
}
/* 按键释放:关闭对应方向位 */
Keys.onReleased: (event) => {
switch (event.key) {
case Qt.Key_Up: case Qt.Key_W:
dragContainerId.pressedKeys &= ~dragContainerId.key_up; break;
case Qt.Key_Down: case Qt.Key_S:
dragContainerId.pressedKeys &= ~dragContainerId.key_down; break;
case Qt.Key_Left: case Qt.Key_A:
dragContainerId.pressedKeys &= ~dragContainerId.key_left; break;
case Qt.Key_Right:case Qt.Key_D:
dragContainerId.pressedKeys &= ~dragContainerId.key_right; break;
}
event.accepted = true;
}
}
}
五、总结
在我过往的工作当中,倒是没有对键盘有过特殊的需求,顶多接触过ESC全屏这种。不过对于简单的上下左右,还有字母数字这些键盘按键的监听,我们还是应该要学会的。在一些拖拽进度条、音量等等的需求中,键盘按键往往可以提供与鼠标不一样的精细操作。