我正在使用 javafx 编写一个原始的 java 游戏。 我通过按下按钮来执行暂停功能
@FXML private Button btnPause;
暂停由方法中变化的字段private boolean isPause
调节
@FXML private void pauseClick(ActionEvent event){ isPause = !isPause; }
在 HelloApplication 类中,我处理键盘事件。
scene.setOnKeyPressed(e -> {
if (e.getCode() == KeyCode.ENTER)
HelloController.jump = true;
if (e.getCode() == KeyCode.RIGHT || e.getCode() == KeyCode.D)
HelloController.right = true;
if (e.getCode() == KeyCode.LEFT || e.getCode() == KeyCode.A)
HelloController.left = true;
});
scene.setOnKeyReleased(e -> {
if (e.getCode() == KeyCode.RIGHT || e.getCode() == KeyCode.D)
HelloController.right = false;
if (e.getCode() == KeyCode.LEFT || e.getCode() == KeyCode.A)
HelloController.left = false;
});
我测试发现按键盘上的任意键都会被程序读取为按按钮。也就是说,导致问题的是按钮的存在,而不是处理该按钮的方法。 如何同时处理键盘事件和按钮点击?
这是我的代码。
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
import java.io.IOException;
public class HelloApplication extends Application {
@Override
public void start(Stage stage) throws IOException {
FXMLLoader fxmlLoader = new FXMLLoader(HelloApplication.class.getResource("hello-view.fxml"));
Scene scene = new Scene(fxmlLoader.load(), 711, 400);
stage.initStyle(StageStyle.UNDECORATED);
stage.setScene(scene);
scene.setOnKeyPressed(e -> {
if (e.getCode() == KeyCode.ENTER)
HelloController.jump = true;
if (e.getCode() == KeyCode.RIGHT || e.getCode() == KeyCode.D)
HelloController.right = true;
if (e.getCode() == KeyCode.LEFT || e.getCode() == KeyCode.A)
HelloController.left = true;
});
scene.setOnKeyReleased(e -> {
if (e.getCode() == KeyCode.RIGHT || e.getCode() == KeyCode.D)
HelloController.right = false;
if (e.getCode() == KeyCode.LEFT || e.getCode() == KeyCode.A)
HelloController.left = false;
});
stage.show();
}
public static void main(String[] args) {
launch();
}
}
控制器
import java.net.URL;
import java.util.ResourceBundle;
import javafx.animation.*;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.shape.Rectangle;
import javafx.util.Duration;
public class HelloController {
@FXML
private ResourceBundle resources;
@FXML
private URL location;
@FXML
private Rectangle bg1, bg2, player, enemy;
@FXML
private Button btnPause;
@FXML
private Label labelPause;
private final int BG_WEIGHT = 711;
private ParallelTransition parallelTransition;
private TranslateTransition enemyTrans;
public static boolean right = false, left = false, jump = false;
private boolean isPause = false;
private int playerSpeed = 3, jumpSpeed = 4;
AnimationTimer timer = new AnimationTimer() {
@Override
public void handle(long l) {
if (jump && player.getLayoutY() > 60f)
player.setLayoutY(player.getLayoutY() - jumpSpeed);
else if (player.getLayoutY() <= 185f){
jump = false;
player.setLayoutY(player.getLayoutY() + jumpSpeed);
}
if (right && player.getLayoutX() < 100)
player.setLayoutX(player.getLayoutX() + playerSpeed);
if (left && player.getLayoutX() > 0)
player.setLayoutX(player.getLayoutX() - playerSpeed);
if (isPause && !labelPause.isVisible()) {
labelPause.setVisible(true);
playerSpeed = 0;
jumpSpeed = 0;
parallelTransition.pause();
enemyTrans.pause();
}
else if (!isPause && labelPause.isVisible()) {
labelPause.setVisible(false);
playerSpeed = 3;
jumpSpeed = 4;
parallelTransition.play();
enemyTrans.play();
}
}
};
// @FXML
// private void pauseClick(ActionEvent event){
// isPause = !isPause;
// }
@FXML
void initialize() {
TranslateTransition bgOneTrans = new TranslateTransition(Duration.millis(5000), bg1);
bgOneTrans.setFromX(0);
bgOneTrans.setToX(BG_WEIGHT * -1);
bgOneTrans.setInterpolator(Interpolator.LINEAR);
TranslateTransition bgTwoTrans = new TranslateTransition(Duration.millis(5000), bg2);
bgTwoTrans.setFromX(0);
bgTwoTrans.setToX(BG_WEIGHT * -1);
bgTwoTrans.setInterpolator(Interpolator.LINEAR);
enemyTrans = new TranslateTransition(Duration.millis(3500), enemy);
enemyTrans.setFromX(0);
enemyTrans.setToX(BG_WEIGHT * -1 - 200);
enemyTrans.setInterpolator(Interpolator.LINEAR);
enemyTrans.setCycleCount(Animation.INDEFINITE);
enemyTrans.play();
parallelTransition = new ParallelTransition(bgOneTrans, bgTwoTrans);
parallelTransition.setCycleCount(Animation.INDEFINITE);
parallelTransition.play();
timer.start();
}
}
fxml 文件
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.shape.Rectangle?>
<?import javafx.scene.text.Font?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="711.0" xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.example.realgame.HelloController">
<children>
<Rectangle fx:id="bg1" arcHeight="5.0" arcWidth="5.0" fill="#e2ff1f" height="400.0" stroke="#ffffff00" strokeType="INSIDE" width="711.0" />
<Rectangle fx:id="bg2" arcHeight="5.0" arcWidth="5.0" fill="#26ff00" height="400.0" layoutX="709.0" stroke="#ffffff00" strokeType="INSIDE" width="711.0" />
<Rectangle fx:id="player" arcHeight="5.0" arcWidth="5.0" fill="#13b734" height="124.0" layoutX="42.0" layoutY="225.0" stroke="BLACK" strokeType="INSIDE" width="124.0" />
<Rectangle fx:id="enemy" arcHeight="5.0" arcWidth="5.0" fill="#c51212" height="87.0" layoutX="853.0" layoutY="264.0" stroke="BLACK" strokeType="INSIDE" width="87.0" />
<Label fx:id="labelPause" focusTraversable="false" layoutX="239.0" layoutY="109.0" text="PAUSE" textFill="#4d0aff" visible="false">
<font>
<Font name="Bell MT Bold" size="84.0" />
</font>
</Label>
<Button fx:id="btnPause" layoutX="23.0" layoutY="18.0" mnemonicParsing="false" text="Pause" />
</children>
</AnchorPane>
解决方案
将正在消耗事件的按钮的焦点遍历设置为 false。
<Button fx:id="btnPause" focusTraversable="false"/>
解释
了解事件处理如何工作。
有一个捕获阶段,然后是冒泡阶段。
如果某些东西在处理过程中消耗了事件,它不会冒泡回场景进行处理。
这就是您向场景添加按钮时发生的情况。
按钮可以获取焦点并接收输入。当你只有一个按钮时,它就有焦点,因为没有其他东西可以关注。
一旦按钮获得焦点,它就会通过硬连线接受来自键盘的输入。如果它接受输入,它将消耗它,并且输入不会冒泡回场景中的事件处理程序。
按钮将处理某些关键事件。您可以在
com.sun.javafx.scene.control.behavior.ButtonBehavior
类的代码中看到:
// create a map for button-specific mappings (this reuses the default
// InputMap installed on the control, if it is non-null, allowing us to pick up any user-specified mappings)
buttonInputMap = createInputMap();
// add focus traversal mappings
addDefaultMapping(buttonInputMap, FocusTraversalInputMap.getFocusTraversalMappings());
// then button-specific mappings for key and mouse input
addDefaultMapping(buttonInputMap,
new KeyMapping(SPACE, KeyEvent.KEY_PRESSED, this::keyPressed),
new KeyMapping(SPACE, KeyEvent.KEY_RELEASED, this::keyReleased),
new MouseMapping(MouseEvent.MOUSE_PRESSED, this::mousePressed),
new MouseMapping(MouseEvent.MOUSE_RELEASED, this::mouseReleased),
new MouseMapping(MouseEvent.MOUSE_ENTERED, this::mouseEntered),
new MouseMapping(MouseEvent.MOUSE_EXITED, this::mouseExited),
// on non-Mac OS platforms, we support pressing the ENTER key to activate the button
new KeyMapping(new KeyBinding(ENTER, KeyEvent.KEY_PRESSED), this::keyPressed, event -> PlatformUtil.isMac()),
new KeyMapping(new KeyBinding(ENTER, KeyEvent.KEY_RELEASED), this::keyReleased, event -> PlatformUtil.isMac())
);
// Button also cares about focus
control.focusedProperty().addListener(focusListener);
如果按钮具有焦点,则输入映射中的任何键都将被按钮消耗。
焦点遍历键被添加到输入映射中(这些是上、下、左、右箭头键),因此焦点遍历机制会消耗这些键,并且场景中的左右箭头键处理程序不起作用(您拥有的导航备用键(D 和 A)确实有效,因为它们没有被消耗)。
应用程序中的 Enter 键处理程序适用于 Mac,但不适用于 Windows。 Windows 对按钮的 Enter 键按下进行了特殊处理,这将消耗该平台上的 Enter 键。
一般输入处理方法
以下是一些替代修复方法:
focusTraversable="false"
您已经在 FXML 中为标签设置了该设置(这是多余的,因为默认情况下标签不可焦点遍历),但您没有为按钮设置它。一旦将按钮的焦点遍历设置为关闭,游戏的左右箭头导航键处理程序就开始工作。
在按钮上将焦点遍历设置为 false 还可以修复 Windows 上的 Enter 键问题,因为如果按钮没有焦点,则不会消耗 Enter 键,并且无法保留焦点,因为它不是焦点的一部分遍历图。