使用 JavaFX 中的箭头键导航出 GridPane 中的 TextField
Navigating out of a TextField in a GridPane using the arrow keys in JavaFX
我正在 Java 中使用 JavaFX 库制作一个数独求解器程序。该程序包含一个交互式数独板,由 GridPane
中的一系列 TextField
组成。董事会看起来像这样:
现在,光标在最左上角TextField
。如果字段中有文本,用户可以使用箭头键在文本中移动光标。但是,我希望用户能够使用箭头键导航到不同的 TextField
。问题是,该字段处于“键入模式”(我不知道官方术语),因此箭头键只能将光标移动到文本中的不同点,否则它会停留在同一字段中。
这就是我的意思:
假装我画的那条线是光标。现在,如果我单击左箭头键,光标将移动到 1 的左侧,但我希望它移动到左侧的 TextField
。如果我单击向下箭头键,则没有任何反应,因为 1 下面没有可供光标导航到的文本,但我希望它移动到下面的 TextField
。
GridPane
的代码是这样的:
TextField[][] squares = new TextField[9][9];
GridPane grid = new GridPane();
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
squares[i][j] = new TextField();
squares[i][j].setPrefHeight(8);
squares[i][j].setPrefWidth(25);
grid.add(squares[i][j], j, i);
}
}
grid.setAlignment(Pos.CENTER);
squares
数组让我可以访问 GridPane
中的个人 TextField
。
关于如何解决这个问题有什么建议吗?
您需要将事件处理程序设置为在按下箭头键时移动,如下所示查看 setTextHandler
函数没有错误处理当你从那里创建 TextField
s 时,应该从循环中调用它,它会检查是否按下了箭头键,然后它将从下一个 TextField
开始 .requestFocus()
]
public class Main extends Application {
private TextField[][] squares;
@Override
public void start(Stage primaryStage) throws Exception {
squares = new TextField[9][9];
GridPane grid = new GridPane();
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
squares[i][j] = new TextField();
squares[i][j].setPrefHeight(8);
squares[i][j].setPrefWidth(25);
setTextHandler(squares[i][j], i, j);
grid.add(squares[i][j], j, i);
}
}
grid.setAlignment(Pos.CENTER);
Scene scene = new Scene(grid);
primaryStage.setScene(scene);
primaryStage.show();
}
private void setTextHandler(TextField textField, int i, int j){
textField.setOnKeyPressed(keyEvent -> {
System.out.println(keyEvent.getCode());
if(keyEvent.getCode().isArrowKey()) {
if (keyEvent.getCode() == KeyCode.UP) {
squares[i-1][j].requestFocus();
} else if (keyEvent.getCode() == KeyCode.DOWN) {
squares[i+1][j].requestFocus();
} else if (keyEvent.getCode() == KeyCode.LEFT) {
squares[i][j-1].requestFocus();
} else if (keyEvent.getCode() == KeyCode.RIGHT) {
squares[i][j+1].requestFocus();
}
}
});
}
}
为了完全避免焦点 TextField
处理箭头键,您需要在 KeyEvent
到达所述 TextField
之前拦截它。这可以通过向 GridPane
添加事件 filter 并适当地使用事件来实现。如果您不确定为什么会这样,您可以查看 JavaFX: Handling Events 教程。
然后您可以使用 Node#requestFocus()
以编程方式更改焦点节点。
我还建议设置每个 TextField
的 prefColumnCount
而不是尝试手动设置首选尺寸。这样,首选尺寸是根据字体大小计算的。
这是一个例子:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.text.Font;
import javafx.stage.Stage;
public class App extends Application {
private TextField[][] fields;
@Override
public void start(Stage primaryStage) {
GridPane grid = new GridPane();
grid.setHgap(3);
grid.setVgap(3);
grid.setPadding(new Insets(5));
grid.setAlignment(Pos.CENTER);
grid.addEventFilter(KeyEvent.KEY_PRESSED, this::handleArrowNavigation);
fields = new TextField[9][9];
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
fields[i][j] = createTextField();
grid.add(fields[i][j], j, i);
}
}
primaryStage.setScene(new Scene(grid));
primaryStage.show();
}
private void handleArrowNavigation(KeyEvent event) {
Node source = (Node) event.getSource(); // the GridPane
Node focused = source.getScene().getFocusOwner();
if (event.getCode().isArrowKey() && focused.getParent() == source) {
int row = GridPane.getRowIndex(focused);
int col = GridPane.getColumnIndex(focused);
// Switch expressions were standardized in Java 14
switch (event.getCode()) {
case LEFT -> fields[row][Math.max(0, col - 1)].requestFocus();
case RIGHT -> fields[row][Math.min(8, col + 1)].requestFocus();
case UP -> fields[Math.max(0, row - 1)][col].requestFocus();
case DOWN -> fields[Math.min(8, row + 1)][col].requestFocus();
}
event.consume();
}
}
private TextField createTextField() {
TextField field = new TextField();
// Rather than setting the pref sizes manually this will
// compute the pref sizes based on the font size.
field.setPrefColumnCount(1);
field.setFont(Font.font(20));
field.setTextFormatter(
new TextFormatter<>(
change -> {
// Only allow the text to be empty or a single digit between 1-9
if (change.getControlNewText().matches("[1-9]?")) {
// Without this the text goes "off screen" to the left. This also
// seems to have the added benefit of selecting the just-entered
// text, which makes replacing it a simple matter of typing another
// digit.
change.setCaretPosition(0);
return change;
}
return null;
}));
return field;
}
}
上面还为每个 TextField
添加了一个 TextFormatter
以显示将文本限制为 1 到 9 之间的数字的方法。注意箭头导航到达时不会“环绕”行或列的末尾。如果需要,您当然可以修改代码来实现它。
您可能要考虑为游戏创建模型。这样,业务逻辑就不会直接绑定到 JavaFX UI 对象。当您更新模型时,它会通知视图(可能通过“视图模型”,具体取决于体系结构)并且视图会相应地更新自身。
我正在 Java 中使用 JavaFX 库制作一个数独求解器程序。该程序包含一个交互式数独板,由 GridPane
中的一系列 TextField
组成。董事会看起来像这样:
现在,光标在最左上角TextField
。如果字段中有文本,用户可以使用箭头键在文本中移动光标。但是,我希望用户能够使用箭头键导航到不同的 TextField
。问题是,该字段处于“键入模式”(我不知道官方术语),因此箭头键只能将光标移动到文本中的不同点,否则它会停留在同一字段中。
这就是我的意思:
假装我画的那条线是光标。现在,如果我单击左箭头键,光标将移动到 1 的左侧,但我希望它移动到左侧的 TextField
。如果我单击向下箭头键,则没有任何反应,因为 1 下面没有可供光标导航到的文本,但我希望它移动到下面的 TextField
。
GridPane
的代码是这样的:
TextField[][] squares = new TextField[9][9];
GridPane grid = new GridPane();
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
squares[i][j] = new TextField();
squares[i][j].setPrefHeight(8);
squares[i][j].setPrefWidth(25);
grid.add(squares[i][j], j, i);
}
}
grid.setAlignment(Pos.CENTER);
squares
数组让我可以访问 GridPane
中的个人 TextField
。
关于如何解决这个问题有什么建议吗?
您需要将事件处理程序设置为在按下箭头键时移动,如下所示查看 setTextHandler
函数没有错误处理当你从那里创建 TextField
s 时,应该从循环中调用它,它会检查是否按下了箭头键,然后它将从下一个 TextField
.requestFocus()
]
public class Main extends Application {
private TextField[][] squares;
@Override
public void start(Stage primaryStage) throws Exception {
squares = new TextField[9][9];
GridPane grid = new GridPane();
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
squares[i][j] = new TextField();
squares[i][j].setPrefHeight(8);
squares[i][j].setPrefWidth(25);
setTextHandler(squares[i][j], i, j);
grid.add(squares[i][j], j, i);
}
}
grid.setAlignment(Pos.CENTER);
Scene scene = new Scene(grid);
primaryStage.setScene(scene);
primaryStage.show();
}
private void setTextHandler(TextField textField, int i, int j){
textField.setOnKeyPressed(keyEvent -> {
System.out.println(keyEvent.getCode());
if(keyEvent.getCode().isArrowKey()) {
if (keyEvent.getCode() == KeyCode.UP) {
squares[i-1][j].requestFocus();
} else if (keyEvent.getCode() == KeyCode.DOWN) {
squares[i+1][j].requestFocus();
} else if (keyEvent.getCode() == KeyCode.LEFT) {
squares[i][j-1].requestFocus();
} else if (keyEvent.getCode() == KeyCode.RIGHT) {
squares[i][j+1].requestFocus();
}
}
});
}
}
为了完全避免焦点 TextField
处理箭头键,您需要在 KeyEvent
到达所述 TextField
之前拦截它。这可以通过向 GridPane
添加事件 filter 并适当地使用事件来实现。如果您不确定为什么会这样,您可以查看 JavaFX: Handling Events 教程。
然后您可以使用 Node#requestFocus()
以编程方式更改焦点节点。
我还建议设置每个 TextField
的 prefColumnCount
而不是尝试手动设置首选尺寸。这样,首选尺寸是根据字体大小计算的。
这是一个例子:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.TextField;
import javafx.scene.control.TextFormatter;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.GridPane;
import javafx.scene.text.Font;
import javafx.stage.Stage;
public class App extends Application {
private TextField[][] fields;
@Override
public void start(Stage primaryStage) {
GridPane grid = new GridPane();
grid.setHgap(3);
grid.setVgap(3);
grid.setPadding(new Insets(5));
grid.setAlignment(Pos.CENTER);
grid.addEventFilter(KeyEvent.KEY_PRESSED, this::handleArrowNavigation);
fields = new TextField[9][9];
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
fields[i][j] = createTextField();
grid.add(fields[i][j], j, i);
}
}
primaryStage.setScene(new Scene(grid));
primaryStage.show();
}
private void handleArrowNavigation(KeyEvent event) {
Node source = (Node) event.getSource(); // the GridPane
Node focused = source.getScene().getFocusOwner();
if (event.getCode().isArrowKey() && focused.getParent() == source) {
int row = GridPane.getRowIndex(focused);
int col = GridPane.getColumnIndex(focused);
// Switch expressions were standardized in Java 14
switch (event.getCode()) {
case LEFT -> fields[row][Math.max(0, col - 1)].requestFocus();
case RIGHT -> fields[row][Math.min(8, col + 1)].requestFocus();
case UP -> fields[Math.max(0, row - 1)][col].requestFocus();
case DOWN -> fields[Math.min(8, row + 1)][col].requestFocus();
}
event.consume();
}
}
private TextField createTextField() {
TextField field = new TextField();
// Rather than setting the pref sizes manually this will
// compute the pref sizes based on the font size.
field.setPrefColumnCount(1);
field.setFont(Font.font(20));
field.setTextFormatter(
new TextFormatter<>(
change -> {
// Only allow the text to be empty or a single digit between 1-9
if (change.getControlNewText().matches("[1-9]?")) {
// Without this the text goes "off screen" to the left. This also
// seems to have the added benefit of selecting the just-entered
// text, which makes replacing it a simple matter of typing another
// digit.
change.setCaretPosition(0);
return change;
}
return null;
}));
return field;
}
}
上面还为每个 TextField
添加了一个 TextFormatter
以显示将文本限制为 1 到 9 之间的数字的方法。注意箭头导航到达时不会“环绕”行或列的末尾。如果需要,您当然可以修改代码来实现它。
您可能要考虑为游戏创建模型。这样,业务逻辑就不会直接绑定到 JavaFX UI 对象。当您更新模型时,它会通知视图(可能通过“视图模型”,具体取决于体系结构)并且视图会相应地更新自身。