MVP в JavaFX, часть 1
- Создаем приложение
- Добавляем классы данных
- Делаем интерфейс
- Добавляем форму редактирования
- Открываем форму как модальное окно
- Настраиваем форму
- Формируем экземпляр еды по данным с формы
- Реализуем обновление объекта
- Добавляем меню и реализуем удаление строки
Данная работа посвящена лишениями и тяготам, которые испытывает неофит, ступивший на тернистый пусть разработки интерфейсов на джаве. Особенно после C#. С другой стороны, вы сами этого хотели.
В этой части мы рассмотрим, как сделать интерфейс для вывода табличных данных и добавим соответствующие обработчики и одну форму для создания/редактирования объектов.
Пока без всяких MVP. Так как мы бы это делали, если мы сейчас были на втором курсе.
Создаем приложение
Создаем JavaFX приложение. И прежде чем бежать и сразу пилить классы данных, немного упорядочим структуру проекта.
Так как у нас будет многооконное приложение, то, наверное, имеет смысл как-то выделить классы и файлы, связанные с интерфейсом в отдельную папку.
Также сделаем отдельную папку под классы данных и модели. [Чтобы создать новую папку нажмите правой кнопкой на месту куда хотите ее добавить и выберете New / Package]
У нас имеется такая структура:
превращаем ее в вот такую:
дополнительно
- переименуем файлик sample.fxml в файл MainForm.fxml
- переименуем файлик Controller в MainFormController
папка 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;
}
}
итого, получаем такую структуру
Делаем интерфейс
Открываем в SceneBuilder файл MainForm.fxml и лепим такой интерфейс
для таблицы используем компоненту 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'
:
получаем такой контроллер
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);
}
}
пробуем запустить
хм, чет колонок нет. Ну да, чтобы они стали видны надо сначала эти колонки создать. Указать какие данные в них выводить и привязать к таблице.
Сделаем это:
@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);
}
запускаем
У нас в списке хранятся разнородные объекты, но нам все-таки хотелось бы видеть их описание в более подробном виде с учетом их уникальных свойств.
Для этого реализуем метод, который будет выдавать подробное описание еды.
Идем в класс 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);
}
проверяем:
Добавляем форму редактирования
Создаем FXML файлик и соответствующий контроллер, и привязываем контроллер к форме
Теперь идем в SceneBuilder и клепаем форму вот такую:
основные имена
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>
В принципе форма как форма. Но с несколькими особенностями
Первая. Мы каждый набор уникальных свойств убираем в отдельный контейнер
причем каждому контейнеру даем имя (прям 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
Третья. Чтобы свойства не слипались указываем свойства Padding и Spacing
Четверая. Размеры 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 и добавляем кнопку
теперь переключаемся на разметку MainForm.fxml и добавляем обработчик клика, прям прописываем свойство onAction, обязательно добавляем # перед именем функции. Затем жмякаем Alt+Enter и выбираем:
попадаем в контроллер и пишем код который откроет нам нашу новую форму как модальное окно
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();
}
}
запускаем и жмем на кнопку:
Настраиваем форму
Так как наше форма для еды, должна позволять собирать введенные данные в экземпляр класса наследника еды, то сначала надо добавить возможность выбирать тип этого самого наследника.
Идем в 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
));
}
}
можно запустить проверить
теперь воспользуемся тем что у нас все компоненты распиханы по отдельным панелькам и будем отображать или прятать ту или иную панель в зависимости от выбранного значения:
@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));
});
}
проверяем:
Сейчас у нас по сути просто скрываются панельки. Это выглядит не очень красиво. Чтобы у нас не появлялось больших отступов сверху или снизу, надо помимо скрытия панелек отключать учет их размеров при расчете размеров формы. Делается это так:
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)); // добавили
});
смотрим:
ну и осталось скрыть все панельки при открытии формы, для этого дополнительно вынесем обработчик в отдельную функцию
@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);
}
}
Реализуем обновление объекта
Чтобы добавить возможность править объект добавим новую кнопку
и добавляем ей обработчик
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());
}
}
проверяем
Добавляем меню и реализуем удаление строки
В этот раз вместо того, чтобы сразу добавлять новую кнопку, лучше возьмем и утащим все кнопки в меню. Но для этого добавим его сначала на главную форму
И добавим нужные элементы меню
привязка функцию к пункту меню делается по аналогии с той же кнопкой
Привязываем каждый пункт меню к соответствующей функции. Для пункта Данные/Удалить
указываем еще не созданную функцию onDeleteClick
.
И удаляем кнопки
идем в разметку MainForm.fxml и создаем функцию
правим ее код на следующий:
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 виду.