这一篇做一个简易的方向控制游戏:一个方块在网格场上,用 WASD 或方向键控制它移动,可以按住实现连续移动、支持斜向移动,按 R 键瞬间回到中心。这样就把 Keys 按键事件、时钟节拍、坐标运算串起来,做出真正有交互手感的东西。
多数人接触键盘控制第一反应是用上一篇的 KeyNavigation,但它只适用于"跳到另一个固定元素",想让人物连续走动就需要另一套思路:自己记按键状态,再用定时器持续更新位置。这篇讲的就是后者。

演示代码
qml
import QtQuick
import QtQuick.Layouts
FadeInAnimation {
ColumnLayout {
anchors.fill: parent
anchors.margins: 20
spacing: 15
TitleSeparator {
title: "方向键控制"
description: "使用 Keys.onPressed 处理 WASD 与方向键,按住移动、松开停止;同时按下多键时支持斜向移动。"
}
// 游戏场地
Rectangle {
id: field
Layout.fillWidth: true
Layout.fillHeight: true
radius: 10
color: "#FAFAFA"
border.color: "#ddd"
border.width: 1
clip: true
focus: true // 默认获取焦点
activeFocusOnTab: true
// 按键状态字典
property var pressedKeys: ({ up: false, down: false, left: false, right: false })
// 玩家角色
Rectangle {
id: player
width: 32
height: 32
radius: 6
color: "#1296FF"
x: field.width / 2 - width / 2
y: field.height / 2 - height / 2
border.color: "#0A6FCC"
border.width: 2
Behavior on x { NumberAnimation { duration: 60 } }
Behavior on y { NumberAnimation { duration: 60 } }
}
// 状态键提示
Column {
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: 8
spacing: 4
Text {
text: field.activeFocus ? "焦点已就绪" : "点击场地获取焦点"
font.pointSize: 11
color: field.activeFocus ? "#21be2b" : "#999"
font.bold: true
}
Text {
text: "坐标:(" + Math.round(player.x) + ", " + Math.round(player.y) + ")"
font.pointSize: 10
color: "#666"
}
}
Keys.onPressed: (event) => {
var handled = true
switch (event.key) {
case Qt.Key_W:
case Qt.Key_Up:
field.pressedKeys.up = true; break
case Qt.Key_S:
case Qt.Key_Down:
field.pressedKeys.down = true; break
case Qt.Key_A:
case Qt.Key_Left:
field.pressedKeys.left = true; break
case Qt.Key_D:
case Qt.Key_Right:
field.pressedKeys.right = true; break
case Qt.Key_R:
// 回到初始位置:先停掉移动定时器,避免其把回位动画读出的中间值写回
moveTimer.stop()
player.x = field.width / 2 - player.width / 2
player.y = field.height / 2 - player.height / 2
event.accepted = true
return // 提前返回,不启动移动定时器
default:
handled = false
}
if (handled) {
event.accepted = true
field.pressedKeys = field.pressedKeys // 强制 binding 刷新状态指示器
moveTimer.start()
}
}
Keys.onReleased: (event) => {
var handled = true
switch (event.key) {
case Qt.Key_W: case Qt.Key_Up: field.pressedKeys.up = false; break
case Qt.Key_S: case Qt.Key_Down: field.pressedKeys.down = false; break
case Qt.Key_A: case Qt.Key_Left: field.pressedKeys.left = false; break
case Qt.Key_D: case Qt.Key_Right: field.pressedKeys.right = false; break
default: handled = false
}
if (handled) {
event.accepted = true
field.pressedKeys = field.pressedKeys
if (!field.pressedKeys.up && !field.pressedKeys.down &&
!field.pressedKeys.left && !field.pressedKeys.right) {
moveTimer.stop() // 所有方向都松开,停止移动
}
}
}
// 移动定时器:节流刷新位置,支持斜向
Timer {
id: moveTimer
interval: 16 // ~60 FPS
repeat: true
onTriggered: {
var speed = 4
var newX = player.x
var newY = player.y
if (field.pressedKeys.left) newX -= speed
if (field.pressedKeys.right) newX += speed
if (field.pressedKeys.up) newY -= speed
if (field.pressedKeys.down) newY += speed
// 边界检测
newX = Math.max(0, Math.min(field.width - player.width, newX))
newY = Math.max(0, Math.min(field.height - player.height, newY))
player.x = newX
player.y = newY
}
}
MouseArea {
anchors.fill: parent
onClicked: field.forceActiveFocus()
}
}
// 提示
Row {
Layout.alignment: Qt.AlignHCenter
spacing: 16
Text { text: "WASD / 方向键:移动"; font.pointSize: 11; color: "#666" }
Text { text: "R:回到中心"; font.pointSize: 11; color: "#666" }
}
}
}
注:源码里还有一层 Canvas 画的网格背景和底部一组按键状态指示器,这里为聚焦核心的按键逻辑做了省略,不影响 Keys 的使用。
关键逻辑解析
这一套的核心是四件套:按键记录、状态存储、定时器驱动、坐标更新。下面拆开讲。
接收焦点是前提
场地 Rectangle 写了 focus: true(默认拿焦点)和 activeFocusOnTab: true(能被 Tab 到)。有了焦点,Keys.onPressed / onReleased 才收得到事件。旁边还有一句"点击场地获取焦点"的提示,点了之后 forceActiveFocus() 补拿焦点。缺了这一步,按什么键都没反应------这是所有键盘应用的第一道门槛。
用字典记录按键状态
核心数据结构是一个字典 pressedKeys,记录上下左右四个键当前是否按着:
qml
property var pressedKeys: ({ up: false, down: false, left: false, right: false })
onPressed 里按 W 或上箭头就把 up 置 true,onReleased 里反过来置 false。所以这个字典时刻反映"此刻正确按着哪些方向键"。它不属于方块,而属于场地------这样所有按键状态集中在父级,好管理。
WASD 与方向键等价的写法
switch 里把两套键写到同一个 case:
qml
case Qt.Key_W:
case Qt.Key_Up:
field.pressedKeys.up = true
break
多个 case 共用一段代码,W 和上箭头做一样的事。这正是"一套控制逻辑,双键位支持"的常见手法,游戏里很常见。
为什么不能只在 onPressed 里移动
有人会问:按一下键,方块不就应该动一步吗?那按住不放呢?onPressed 只在按下那个瞬间触发一次,如果只在里面改位置,就会"按一下动一下",没有连续移动的感觉。所以要实现"按住持续走",就得把每个按键的"开/关"记下来,再让一个持续的机制(定时器)去驱动移动。
定时器负责连续运动
Timer 每 16 毫秒触发一次(约每秒 60 帧),也就是一帧看一次 pressedKeys:左键按着就往 x 减 4,右键按着则加 4,上下同理。因为每帧都重新读一次字典,所以同时按两个方向,x 和 y 一起变 ,就实现了斜向移动------这是 KeyNavigation 做不到的。
位移与边界
速度常量 speed = 4,每帧移动 4 像素。边界用 Math.max(0, Math.min(边界, 目标值)) 做夹取:把 x 限制在 0 到 场地宽 - 方块宽 之间,这样方块不会跑出场地。按下和松开时都会给 pressedKeys 重新赋值,用来强制刷新那些绑定它的状态指示器(原 demo 里底部那排变色的按键灯)。
R 键回中的细节
按 R 时先 moveTimer.stop() 停掉定时器,再把方块坐标设回中心。这里有个隐蔽的坑:方块坐标加了 Behavior 动画,如果定时器还在跑,动画过渡的中间值可能被下一次定时器写回,打断回位过程。所以先停定时器,保证回位动画一口气走完。
松手停走
onReleased 里做了好事收尾:把所有方向都置 false 之后,检查是否四个方向全松下------如果是,moveTimer.stop()。如此"按住走、全松停",体验干净利落。
运行验证
- Qt Creator 打开示例的
CMakeLists.txt; - 按
Ctrl+R运行; - 点击场地获得焦点,按住 W(或上箭头)看方块持续向上走,松开即停;
- 同时按住 W 和 D,看方块沿右上方向斜向移动,验证多键支持;
- 把方块移到边缘,看它被挡住不会越界;按 R 看它顺滑回到场地中心。
扩展复用方向
- 把"按键字典 + 定时器驱动"这套抽出去,就够给贪吃蛇、飞机大战写移动逻辑
- 速度可做成变量,按 Shift 加速、按 Ctrl 减速,手感更丰富
- 定时器改驱动
rotation或scale,就能做出旋转、缩放类持续交互
已验证环境:
- Qt 版本:Qt 6.11.1
- 操作系统:Windows 11
- GitHub:QML-Minimal-Demos/qml_keys_focus