使用 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 函数没有错误处理当你从那里创建 TextFields 时,应该从循环中调用它,它会检查是否按下了箭头键,然后它将从下一个 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() 以编程方式更改焦点节点。

我还建议设置每个 TextFieldprefColumnCount 而不是尝试手动设置首选尺寸。这样,首选尺寸是根据字体大小计算的。

这是一个例子:

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 对象。当您更新模型时,它会通知视图(可能通过“视图模型”,具体取决于体系结构)并且视图会相应地更新自身。