MVP в JavaFX, часть 1

Данная работа посвящена лишениями и тяготам, которые испытывает неофит, ступивший на тернистый пусть разработки интерфейсов на джаве. Особенно после C#. С другой стороны, вы сами этого хотели.

В этой части мы рассмотрим, как сделать интерфейс для вывода табличных данных и добавим соответствующие обработчики и одну форму для создания/редактирования объектов.

Пока без всяких MVP. Так как мы бы это делали, если мы сейчас были на втором курсе.

Создаем приложение

Создаем JavaFX приложение. И прежде чем бежать и сразу пилить классы данных, немного упорядочим структуру проекта.

Так как у нас будет многооконное приложение, то, наверное, имеет смысл как-то выделить классы и файлы, связанные с интерфейсом в отдельную папку.

Также сделаем отдельную папку под классы данных и модели. [Чтобы создать новую папку нажмите правой кнопкой на месту куда хотите ее добавить и выберете New / Package]

У нас имеется такая структура:

Imgur

превращаем ее в вот такую:

Imgur

дополнительно

  • переименуем файлик sample.fxml в файл MainForm.fxml
  • переименуем файлик Controller в MainFormController

Imgur

папка models пока пустая, но сейчас мы будем добавлять в нее классы данных.

Добавляем классы данных

В качестве модели данных у меня следующая иерархия

  • Еда (название, количество калорий) и ее подклассы
    • Фрукт (свежий ли)
    • Шоколад (тип: [белый, горький, молочный])
    • Булочка (с сахаром, с маком, с кунжутом)

Создаем классы в папке models,

общий класс, в нем обязательно создаем getterы и setterы под свойства:

// файл ./sample/models/Food.java
package sample.models;

public class Food
{
    public int kkal;// количество калорий
    public String title;// название

    public Food(int kkal, String title) {
        this.setKkal(kkal);
        this.setTitle(title);
    }

    @Override
    public String toString() {
        return String.format("%s: %s ккал", this.getTitle(), this.getKkal());
    }
    
    public int getKkal() {
        return kkal;
    }

    public void setKkal(int kkal) {
        this.kkal = kkal;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }
}

класс под фрукты

// файл ./sample/models/Fruit.java
package sample.models;

public class Fruit extends Food {
    public Boolean isFresh;// свежий ли фрукт

    public Fruit(int kkal, String title, Boolean isFresh) {
        super(kkal, title);
        this.isFresh = isFresh;
    }
}

класс под шоколад

// файл ./sample/models/Chocolate.java
package sample.models;

public class Chocolate extends Food {
    public enum Type {white, black, milk;} // какие типы шоколада бывают
    public Type type;// а это собственно тип шоколада

    public Chocolate(int kkal, String title, Type type) {
        super(kkal, title);
        this.type = type;
    }
}

класс под булочку

// файл ./sample/models/Cookie.java
package sample.models;

public class Cookie extends Food // булочка
{
    public Boolean withSugar;// с сахаром ли?
    public Boolean withPoppy;// или маком?
    public Boolean withSesame;// а может с кунжутом?

    public Cookie(int kkal, String title, Boolean withSugar, Boolean withPoppy, Boolean withSesame) {
        super(kkal, title);
        this.withSugar = withSugar;
        this.withPoppy = withPoppy;
        this.withSesame = withSesame;
    }
}

итого, получаем такую структуру

Imgur

Делаем интерфейс

Открываем в SceneBuilder файл MainForm.fxml и лепим такой интерфейс

Imgur

для таблицы используем компоненту TableView из списка Controls, табличке ставим свойство fx:id = mainTable. Так же у таблицы в начале добавлены две колонки по умолчанию. Их удаляем.

Чтобы таблица растянулась на всю высоту ставим ей свойство Layout/Vgrow = ALWAYS

итого получим такую fxml разметку

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.VBox?>


<VBox prefHeight="382.0" prefWidth="537.0" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" 
      fx:controller="sample.gui.MainFormController">
   <children>
      <TableView fx:id="mainTable" prefHeight="248.0" prefWidth="291.0" VBox.vgrow="ALWAYS" />
   </children>
</VBox>

обязательно проверяем, что свойство fx:controller указывает на корректный контроллер. В моем случае sample.gui.MainFormController.

Теперь привяжем таблицу к контроллеру. Кстати, чтобы долго не мучатся можно это сделать прямо из разметки. Так оно будет быстрее. Ставим курсор на mainTable и жмем Alt+Enter, выбираем Create Field 'mainTable':

Imgur

получаем такой контроллер

package sample.gui;

import javafx.scene.control.TableView;

public class MainFormController {
    public TableView mainTable;
}

подключаем интерфейс Initializable, создаем список под данные с едой, и привязываем к табличке.

public class MainFormController implements Initializable {
    //...
    
    // создали список
    ObservableList<Food> foodList = FXCollections.observableArrayList();

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        // заполнили список данными
        foodList.add(new Fruit(100,"Яблоко",true));
        foodList.add(new Chocolate(200,"шоколад Аленка",Chocolate.Type.milk));
        foodList.add(new Cookie(300, "сладкая булочка с Маком", true, true, false));
        
        // подключили к таблице
        mainTable.setItems(foodList);
    }
}

пробуем запустить

Imgur

хм, чет колонок нет. Ну да, чтобы они стали видны надо сначала эти колонки создать. Указать какие данные в них выводить и привязать к таблице.

Сделаем это:

@Override
public void initialize(URL location, ResourceBundle resources) {
    // ...
    mainTable.setItems(foodList);

    // создаем столбец, указываем что столбец преобразует Food в String,
    // указываем заголовок колонки как "Название"
    TableColumn<Food, String> titleColumn = new TableColumn<>("Название");
    // указываем какое поле брать у модели Food
    // в данном случае title, кстати именно в этих целях необходимо было создать getter и setter для поля title
    titleColumn.setCellValueFactory(new PropertyValueFactory<>("title"));

    // тоже самое для калорийности
    TableColumn<Food, String> kkalColumn = new TableColumn<>("Калорийность");
    kkalColumn.setCellValueFactory(new PropertyValueFactory<>("kkal"));

    // подцепляем столбцы к таблице
    mainTable.getColumns().addAll(titleColumn, kkalColumn);
}

запускаем

Imgur

У нас в списке хранятся разнородные объекты, но нам все-таки хотелось бы видеть их описание в более подробном виде с учетом их уникальных свойств.

Для этого реализуем метод, который будет выдавать подробное описание еды.

Идем в класс Food и добавляем метод getDescription

public class Food
{
    // ...

    public String getDescription() {
        return "";
    }
}

переопределяем этот метод в наследниках

public class Fruit extends Food {
    // ...

    @Override
    public String getDescription() {
        String isFreshString = this.isFresh ? "свежий" : "не свежий";
        return String.format("Фрукт %s", isFreshString);
    }
}
public class Chocolate extends Food {
    // ...

    @Override
    public String getDescription() {
        String typeString = "";
        switch (this.type)
        {
            case white:
                typeString = "белый";
                break;
            case black:
                typeString = "черный";
                break;
            case milk:
                typeString = "молочный";
                break;
        }

        return String.format("Шоколад %s", typeString);
    }
}
import java.util.ArrayList;

public class Cookie extends Food // булочка
{
    // ...

    @Override
    public String getDescription() {
        ArrayList<String> items = new ArrayList<>();
        if (this.withSugar)
            items.add("с сахаром");
        if (this.withPoppy)
            items.add("с маком");
        if (this.withSesame)
            items.add("с кунжутом");

        return String.format("Булочка %s", String.join(", ", items));
    }
}

теперь подцепим функцию к столбцу

@Override
public void initialize(URL location, ResourceBundle resources) {
    // ...

    // добавляем столбец с описанием
    TableColumn<Food, String> descriptionColumn = new TableColumn<>("Описание");
    // если хотим что-то более хитрое выводить, то используем лямбда выражение
    descriptionColumn.setCellValueFactory(cellData -> {
        // плюс надо обернуть возвращаемое значение в обертку свойство
        return new SimpleStringProperty(cellData.getValue().getDescription());
    });

    // добавляем сюда descriptionColumn
    mainTable.getColumns().addAll(titleColumn, kkalColumn, descriptionColumn);
}

проверяем:

Imgur

Добавляем форму редактирования

Создаем FXML файлик и соответствующий контроллер, и привязываем контроллер к форме

Imgur

Теперь идем в SceneBuilder и клепаем форму вот такую:

Imgur

основные имена

Imgur

c вот такой разметкой

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.CheckBox?>
<?import javafx.scene.control.ChoiceBox?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.RowConstraints?>
<?import javafx.scene.layout.VBox?>

<GridPane hgap="4.0" prefHeight="278.0" prefWidth="202.0" vgap="4.0" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.gui.FoodFormController">
    <columnConstraints>
        <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" />
        <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
    </columnConstraints>
    <rowConstraints>
        <RowConstraints minHeight="0.0" vgrow="SOMETIMES" />
        <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
        <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
        <RowConstraints minHeight="10.0" vgrow="SOMETIMES" />
        <RowConstraints minHeight="30.0" prefHeight="30.0" vgrow="SOMETIMES" />
    </rowConstraints>
    <children>
        <ChoiceBox fx:id="cmbFoodType" maxWidth="1.7976931348623157E308" prefWidth="150.0" GridPane.columnSpan="2" />
        <Label text="Название" GridPane.rowIndex="1" />
        <TextField fx:id="txtFoodTitle" GridPane.columnIndex="1" GridPane.rowIndex="1" />
        <Label text="Кол-во колорий" GridPane.rowIndex="2" />
        <TextField fx:id="txtFoodKkal" GridPane.columnIndex="1" GridPane.rowIndex="2" />
        <VBox prefHeight="25.0" prefWidth="194.0" spacing="8.0" GridPane.columnSpan="2" GridPane.halignment="CENTER" GridPane.rowIndex="3" GridPane.vgrow="ALWAYS">
            <children>
                <VBox fx:id="fruitPane">
                    <children>
                        <CheckBox fx:id="chkIsFresh" mnemonicParsing="false" text="свежее" />
                    </children>
                </VBox>
                <HBox fx:id="chocolatePane" prefHeight="100.0" prefWidth="200.0" spacing="5.0">
                    <children>
                        <Label text="Тип">
                     <padding>
                        <Insets top="4.0" />
                     </padding></Label>
                        <ChoiceBox fx:id="cmbChocolateType" maxWidth="1.7976931348623157E308" prefWidth="150.0" HBox.hgrow="ALWAYS" />
                    </children>
                </HBox>
                <VBox fx:id="cookiePane" prefHeight="200.0" prefWidth="100.0" spacing="4.0">
                    <children>
                        <CheckBox fx:id="chkWithSugar" mnemonicParsing="false" text="с сахаром" />
                        <CheckBox fx:id="chkWithPoppy" mnemonicParsing="false" text="с маком" />
                        <CheckBox fx:id="chkWithSesame" mnemonicParsing="false" text="с кунжутом" />
                    </children>
                </VBox>
            </children>
         <padding>
            <Insets bottom="20.0" top="20.0" />
         </padding>
        </VBox>
        <Button mnemonicParsing="false" text="Сохранить" GridPane.halignment="LEFT" GridPane.rowIndex="4" />
        <Button mnemonicParsing="false" text="Отмена" GridPane.columnIndex="1" GridPane.halignment="RIGHT" GridPane.rowIndex="4" />
    </children>
    <padding>
        <Insets bottom="4.0" left="4.0" right="4.0" top="4.0" />
    </padding>
</GridPane>

В принципе форма как форма. Но с несколькими особенностями

Первая. Мы каждый набор уникальных свойств убираем в отдельный контейнер

Imgur

причем каждому контейнеру даем имя (прям VBox и HBox). Вот как оно в разметке выглядит. Три панельки fruitPane, chocolatePane, cookiePane:

<VBox prefHeight="25.0" prefWidth="194.0" spacing="8.0" GridPane.columnSpan="2" GridPane.halignment="CENTER" GridPane.rowIndex="3" GridPane.vgrow="ALWAYS">
    <children>
        <VBox fx:id="fruitPane" fillWidth="false">
            <!-- ... -->
        </VBox>
        <HBox fx:id="chocolatePane" fillHeight="false" prefHeight="100.0" prefWidth="200.0" spacing="5.0">
            <!-- ... -->
        </HBox>
        <VBox fx:id="cookiePane" fillWidth="false" prefHeight="200.0" prefWidth="100.0" spacing="4.0">
            <!-- ... -->
        </VBox>
    </children>
 <padding>
    <Insets bottom="20.0" top="20.0" />
 </padding>
</VBox>

мне это пригодится чтобы скрывать блоки контроллеров на форме, и таким образом получить универсальную форму для разных типов сладостей.

Вторая. Чтобы ComboBox растягивался на всю ширину мы устанавливаем свойство Max Width

Imgur

Третья. Чтобы свойства не слипались указываем свойства Padding и Spacing

Imgur

Четверая. Размеры GridPane указываю фиксированные чтобы форма не скакала в размерах при отключении панелек. Свойства prefHeight и prefWidth заполненые:

<GridPane hgap="4.0" prefHeight="278.0" prefWidth="202.0" vgap="4.0" xmlns="http://javafx.com/javafx/8.0.171" xmlns:fx="http://javafx.com/fxml/1" fx:controller="sample.gui.FoodFormController">
    <!-- ... -->
</GridPane>

Открываем форму как модальное окно

Идем в SceneBuilder открываем MainForm.fxml и добавляем кнопку

Imgur

теперь переключаемся на разметку MainForm.fxml и добавляем обработчик клика, прям прописываем свойство onAction, обязательно добавляем # перед именем функции. Затем жмякаем Alt+Enter и выбираем:

Imgur

попадаем в контроллер и пишем код который откроет нам нашу новую форму как модальное окно

public class MainFormController implements Initializable {
    // ...
    
    // добавляем инфу что наш код может выбросить ошибку IOException
    public void onAddClick(ActionEvent actionEvent) throws IOException {
        // эти три строчки создюат форму из fxml файлика
        // в принципе можно было бы обойтись
        // Parent root = FXMLLoader.load(getClass().getResource("FoodForm.fxml"));
        // но дальше вот это разбиение на три строки упростит нам жизнь
        FXMLLoader loader = new FXMLLoader();
        loader.setLocation(getClass().getResource("FoodForm.fxml"));
        Parent root = loader.load();
        
        // ну а тут создаем новое окно
        Stage stage = new Stage();
        stage.setScene(new Scene(root));
        // указываем что оно модальное
        stage.initModality(Modality.WINDOW_MODAL);
        // указываем что оно должно блокировать главное окно
        // ну если точнее, то окно, на котором мы нажали на кнопку
        stage.initOwner(this.mainTable.getScene().getWindow());
        
        // открываем окно и ждем пока его закроют
        stage.showAndWait();
    }
}

запускаем и жмем на кнопку:

Imgur

Настраиваем форму

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

Идем в FoodFormController и правим его:

package sample.gui;

import javafx.fxml.Initializable;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.TextField;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;

import java.net.URL;
import java.util.ResourceBundle;

public class FoodFormController  implements Initializable {
    // создаем 
    public ChoiceBox cmbFoodType;
    public TextField txtFoodTitle;
    public TextField txtFoodKkal;

    public VBox fruitPane;
    public CheckBox chkIsFresh;

    public HBox chocolatePane;
    public ChoiceBox cmbChocolateType;

    public VBox cookiePane;
    public CheckBox chkWithSugar;
    public CheckBox chkWithPoppy;
    public CheckBox chkWithSesame;

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        
    }
}

теперь добавим константы под типы еды и привяжем их к выпадающему списку:

public class FoodFormController  implements Initializable {
    // ...

    final String FOOD_FRUIT = "Фрукт";
    final String FOOD_CHOCOLATE = "Шоколадка";
    final String FOOD_COOKIE = "Булочка";

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        cmbFoodType.setItems(FXCollections.observableArrayList(
                FOOD_FRUIT,
                FOOD_CHOCOLATE,
                FOOD_COOKIE
        ));
    }
}

можно запустить проверить

Imgur

теперь воспользуемся тем что у нас все компоненты распиханы по отдельным панелькам и будем отображать или прятать ту или иную панель в зависимости от выбранного значения:

@Override
public void initialize(URL location, ResourceBundle resources) {
    // ...

    cmbFoodType.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
        this.fruitPane.setVisible(newValue.equals(FOOD_FRUIT));
        this.chocolatePane.setVisible(newValue.equals(FOOD_CHOCOLATE));
        this.cookiePane.setVisible(newValue.equals(FOOD_COOKIE));
    });
}

проверяем:

Imgur

Сейчас у нас по сути просто скрываются панельки. Это выглядит не очень красиво. Чтобы у нас не появлялось больших отступов сверху или снизу, надо помимо скрытия панелек отключать учет их размеров при расчете размеров формы. Делается это так:

cmbFoodType.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
    this.fruitPane.setVisible(newValue.equals(FOOD_FRUIT));
    this.fruitPane.setManaged(newValue.equals(FOOD_FRUIT)); // добавили
    this.chocolatePane.setVisible(newValue.equals(FOOD_CHOCOLATE));
    this.chocolatePane.setManaged(newValue.equals(FOOD_CHOCOLATE)); // добавили
    this.cookiePane.setVisible(newValue.equals(FOOD_COOKIE));
    this.cookiePane.setManaged(newValue.equals(FOOD_COOKIE)); // добавили
});

смотрим:

Imgur

ну и осталось скрыть все панельки при открытии формы, для этого дополнительно вынесем обработчик в отдельную функцию

@Override
public void initialize(URL location, ResourceBundle resources) {
    cmbFoodType.setItems(FXCollections.observableArrayList(
            FOOD_FRUIT,
            FOOD_CHOCOLATE,
            FOOD_COOKIE
    ));

    cmbFoodType.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
        // вызываю новую функцию
        updatePanes((String) newValue);
    });
    // вызываю новую функцию при инициализации формы
    updatePanes("");
}

// добавил новую функцию
public void updatePanes(String value) {
    this.fruitPane.setVisible(value.equals(FOOD_FRUIT));
    this.fruitPane.setManaged(value.equals(FOOD_FRUIT));
    this.chocolatePane.setVisible(value.equals(FOOD_CHOCOLATE));
    this.chocolatePane.setManaged(value.equals(FOOD_CHOCOLATE));
    this.cookiePane.setVisible(value.equals(FOOD_COOKIE));
    this.cookiePane.setManaged(value.equals(FOOD_COOKIE));
}

заполним теперь комобобокс с выбором типа шоколада, правим метод initialize

@Override
public void initialize(URL location, ResourceBundle resources) {
    cmbFoodType.setItems(FXCollections.observableArrayList(
            FOOD_FRUIT,
            FOOD_CHOCOLATE,
            FOOD_COOKIE
    ));

    cmbFoodType.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {
        updatePanes((String) newValue);
    });
    
    // добавляем все три типа шоколада в комобобокс
    cmbChocolateType.getItems().setAll(
            Chocolate.Type.white,
            Chocolate.Type.black,
            Chocolate.Type.milk
    );
    
    // и используем метод setConverter, 
    // чтобы типы объекты рендерились как строки
    cmbChocolateType.setConverter(new StringConverter<Chocolate.Type>() {
        @Override
        public String toString(Chocolate.Type object) {
            // просто указываем как рендерить
            switch (object) {
                case white:
                    return "Белый";
                case black:
                    return "Черный";
                case milk:
                    return "Молочный";
            }
            return null;
        }
        
        @Override
        public Chocolate.Type fromString(String string) {
            // этот метод не трогаем так как наш комбобкос имеет фиксированный набор элементов
            return null;
        }
    });

    updatePanes("");
}

добавим теперь реакцию на нажатие кнопок. К сожалению, в JavaFX нет нормальных модальных окон и приходится имитировать процесс закрытия окна с результатом вручную.

public class FoodFormController implements Initializable {
    // ...
    
    final String FOOD_FRUIT = "Фрукт";
    final String FOOD_CHOCOLATE = "Шоколадка";
    final String FOOD_COOKIE = "Булочка";
    
    // добавляем новое поле
    private Boolean modalResult = false;
    
    // ...
    
    // обработчик нажатия на кнопку Сохранить
    public void onSaveClick(ActionEvent actionEvent) {
        this.modalResult = true; // ставим результат модального окна на true
        // закрываем окно к которому привязана кнопка 
        ((Stage)((Node)actionEvent.getSource()).getScene().getWindow()).close();
    }

    public void onCancelClick(ActionEvent actionEvent) {
        this.modalResult = false; // ставим результат модального окна на false
        // закрываем окно к которому привязана кнопка
        ((Stage)((Node)actionEvent.getSource()).getScene().getWindow()).close();
    }
    
    // геттер для результата модального окна
    public Boolean getModalResult() {
        return modalResult;
    }
}

главное не забыть привязать обработчики к форме

Формируем экземпляр еды по данным с формы

добавим теперь метод, который будет возвращать новый фрукт на основании данных собранных с формы. Добавляем метод getFood()

public class FoodFormController implements Initializable {
    // ...
    
    public Food getFood() {
        Food result = null;
        int kkal = Integer.parseInt(this.txtFoodKkal.getText());
        String title = this.txtFoodTitle.getText();
        
        switch ((String)this.cmbFoodType.getValue()) {
            case FOOD_CHOCOLATE:
                result = new Chocolate(kkal, title, (Chocolate.Type)this.cmbChocolateType.getValue());
                break;
            case FOOD_COOKIE:
                result = new Cookie(
                        kkal,
                        title,
                        this.chkWithSugar.isSelected(),
                        this.chkWithPoppy.isSelected(),
                        this.chkWithSesame.isSelected()
                );
                break;
            case FOOD_FRUIT:
                result = new Fruit(kkal, title, this.chkIsFresh.isSelected());
                break;
        }
        return result;
    }
}

снова возвращаемся на MainFormController, и правим метод onAddClick

public void onAddClick(ActionEvent actionEvent) throws IOException {
    // ...
    stage.showAndWait();
    
    // вытаскиваем контроллер который привязан к форме    
    FoodFormController controller = loader.getController();
    // проверяем что наали кнопку save
    if (controller.getModalResult()) {
        // собираем еду с формы
        Food newFood = controller.getFood();
        // добавляем в список
        this.foodList.add(newFood);
    }
}

Реализуем обновление объекта

Чтобы добавить возможность править объект добавим новую кнопку

Imgur

и добавляем ей обработчик

public void onEditClick(ActionEvent actionEvent) throws IOException {
    FXMLLoader loader = new FXMLLoader();
    loader.setLocation(getClass().getResource("FoodForm.fxml"));
    Parent root = loader.load();

    Stage stage = new Stage();
    stage.setScene(new Scene(root));
    stage.initModality(Modality.WINDOW_MODAL);
    stage.initOwner(this.mainTable.getScene().getWindow());

    stage.showAndWait();
}

пока наш обработчик просто показывает форму. Добавим в контроллер FoodFormController метод который позволит передать на форму объект для редактирования.

public class EditForm implements Initializable {
    // ...

    public void setFood(Food food) {
        // делаем так что если объект редактируется, то нельзя переключать тип
        this.cmbFoodType.setDisable(food != null);
        if (food != null) {
            // ну а тут стандартное заполнение полей в соответствии с переданной едой
            this.txtFoodKkal.setText(String.valueOf(food.getKkal()));
            this.txtFoodTitle.setText(food.getTitle());

            if (food instanceof Fruit) { // если фрукт
                this.cmbFoodType.setValue(FOOD_FRUIT);
                this.chkIsFresh.setSelected(((Fruit) food).isFresh);
            } else if (food instanceof Cookie) { // если булочка
                this.cmbFoodType.setValue(FOOD_COOKIE);
                this.chkWithSugar.setSelected(((Cookie) food).withSugar);
                this.chkWithPoppy.setSelected(((Cookie) food).withPoppy);
                this.chkWithSesame.setSelected(((Cookie) food).withSesame);
            } else if (food instanceof Chocolate) { // если шоколад
                this.cmbFoodType.setValue(FOOD_CHOCOLATE);
                this.cmbChocolateType.setValue(((Chocolate) food).type);
            }
        }
    }

    public Food getFood() { /* ... */}
    
    // ...
}

возвращаемся обратно в MainFormController и правим реакцию на кнопку редактировать

public void onEditClick(ActionEvent actionEvent) throws IOException {
    // ...

    // передаем выбранную еду
    FoodFormController controller = loader.getController();
    controller.setFood((Food) this.mainTable.getSelectionModel().getSelectedItem());

    stage.showAndWait();

    // если нажали кнопку сохранить
    if (controller.getModalResult()) {
        // узнаем индекс выбранной в таблице строки
        int index = this.mainTable.getSelectionModel().getSelectedIndex();
        // подменяем строку в таблице данными на форме
        this.mainTable.getItems().set(index, controller.getFood());
    }
}

проверяем

Добавляем меню и реализуем удаление строки

В этот раз вместо того, чтобы сразу добавлять новую кнопку, лучше возьмем и утащим все кнопки в меню. Но для этого добавим его сначала на главную форму

Imgur

И добавим нужные элементы меню

Imgur

привязка функцию к пункту меню делается по аналогии с той же кнопкой

Imgur

Привязываем каждый пункт меню к соответствующей функции. Для пункта Данные/Удалить указываем еще не созданную функцию onDeleteClick.

И удаляем кнопки

Imgur

идем в разметку MainForm.fxml и создаем функцию

Imgur

правим ее код на следующий:

public void onDeleteClick(ActionEvent actionEvent) {
    // берем выбранную на форме еду
    Food food = (Food) this.mainTable.getSelectionModel().getSelectedItem();
    
    // выдаем подтверждающее сообщение
    Alert alert = new Alert(Alert.AlertType.CONFIRMATION);
    alert.setTitle("Подтверждение");
    alert.setHeaderText(String.format("Точно удалить %s?", food.getTitle()));
    
    // если пользователь нажал OK
    Optional<ButtonType> option = alert.showAndWait();
    if (option.get() == ButtonType.OK) {
        // удаляем строку из таблицы
        this.mainTable.getItems().remove(food);
    }
}

проверяем:

Для первой части хватит. В следующей части мы рассмотрим, как это приложения привести к MVP виду.

Перейти к следующей части