Переход из TextField в GridPane с помощью клавиш со стрелками в JavaFX

Я делаю программу решения sodoku на Java с библиотекой JavaFX. Программа включает в себя интерактивную доску содоку, состоящую из серии TextFields в GridPane. Плата выглядит так:

Доска Содоку

Прямо сейчас курсор находится в верхнем левом углу 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 предназначен для меня, чтобы иметь доступ к отдельным TextFields в GridPane.

Любые предложения о том, как я могу это исправить?


person QWERTY    schedule 08.07.2020    source источник


Ответы (2)


Чтобы сфокусированный TextField вообще не обрабатывал клавиши со стрелками, вам нужно перехватить KeyEvent до того, как он достигнет указанного TextField. Этого можно добиться, добавив фильтр события в GridPane и используя событие соответствующим образом. Если вы не уверены, почему это работает, вы можете проверить JavaFX: обработка событий".

Затем вы можете использовать Node#requestFocus() для программного изменения сфокусированного узла.

Я также рекомендую установить prefColumnCount для каждого TextField, а не пытаться установить предпочтительные размеры вручную. Таким образом, предпочтительные размеры вычисляются на основе размера шрифта.

Вот пример:

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;
  }
}

Вышеприведенное также добавляет TextFormatter к каждому TextField, чтобы показать способ ограничить текст цифрами от 1 до 9. Обратите внимание, что навигация со стрелкой не перемещается, когда она достигает конца строки или столбца. Вы, конечно, можете изменить код, чтобы реализовать это, если хотите.

Возможно, вы захотите рассмотреть возможность создания модели для игры. Таким образом, бизнес-логика не привязана напрямую к объектам пользовательского интерфейса JavaFX. Когда вы обновляете модель, она уведомляет представление (возможно, через модель представления, в зависимости от архитектуры), и представление соответствующим образом обновляется.

person Slaw    schedule 08.07.2020
comment
Это сработало отлично, но я не смог использовать эти лямбда-выражения, встроенные в оператор switch. Я использую Java 14, поэтому мне любопытно, почему это не сработало. В любом случае, я переписал его как стандартный оператор switch, и он заработал так, как предполагалось. - person QWERTY; 08.07.2020
comment
Рад помочь. Что касается вашей ошибки, я могу только предложить перепроверить, используете ли вы Java 14 для своего проекта и правильно ли настроена ваша IDE (например, в IntelliJ убедитесь, что уровень языка проекта установлен на Java 14). - person Slaw; 08.07.2020
comment
Как мне создать этот GridPane с помощью FXML? Нет никакого способа поместить Java-цикл for в файл XML, не так ли? - person QWERTY; 14.07.2020
comment
Нет, формат FXML не поддерживает циклические конструкции. Когда что-то слишком повторяющееся или динамичное, вы должны реализовать эту часть в контроллере FXML. В вашем случае, вероятно, будет лучше добавить поля в метод initialize. - person Slaw; 14.07.2020
comment
Вы знаете, где я могу найти в Интернете информацию о создании модели для игры? Я не очень понимаю, как это работает. - person QWERTY; 04.08.2020
comment
Не уверен, что у меня когда-либо был какой-то один источник информации. Со временем я просто продолжал читать о различных архитектурах, таких как MVC, MVP, MVVM и других, а также о программировании, управляемом событиями, и шаблонах, таких как шаблон состояния. Поскольку вы создаете игру, вы также можете проверить Введение в JavaFX для разработки игр, хотя я думаю, что эта статья больше ориентирована на игры в реальном времени. - person Slaw; 08.08.2020

Вам нужно настроить обработчик событий для перемещения по нажатию клавиши со стрелкой, как вы можете видеть ниже, посмотрите на функцию setTextHandler, там нет обработки ошибок. Я просто написал это, чтобы дать вам представление о том, что вы должны делать, это вызывается из цикла когда вы создаете TextField оттуда, он проверяет нажатие клавиши со стрелкой, и оттуда он будет .requestFocus() из следующего TextField

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();
                }
            }
        });

    }
}
person Matt    schedule 08.07.2020