12 GUI + JavaFX Ⅱ

Graphical User Interfaces with JavaFX Ⅱ

Objective

  • Bind UI components to model objects to allow the user to add, update, and delete objects listed in a TableView.
  • Use common UI components including Label, Button, ComboBox, DatePicker, RadioButton, ToggleGroup, TableView, TableColumn, ToolBar, and Separator.
  • Use PropertyValueFactory and ObservableList to bind data to components and Dialog to create modal dialogs.

Summary

  1. Data binding allows synchronization between the user interface components and the underlying data model.
  2. Data binding ensures that changes in the data model are automatically reflected in the user interface.
  3. Reading and writing files are combined with a graphical user interface for a user-friendly interactive experience.

Exercise 1*

Country Explorer

  1. Create a Country class based on the following diagram:
Country-name: String-alpha3: String-continent: String-region: String+get*():type+set*(type): void+toString(): String
  1. Create a CountryRepository model class to hold the list of countries and implement a method to load all the countries from data/countries.csv (countries.csv).
CountryRepository-countries: List<Country>+getCountries(): List<Country>-loadCountries(): void+getContinents(): List<String>+getRegions(String): List<String>+getCountriesRegion(String): List<Country>

The CSV file can be read line-by-line and each line split into its fields:

CountryRepository.java
String line = scanner.nextLine();
String[] tokens = line.split(",");
countries.add(new Country(tokens[0], tokens[1], tokens[2], tokens[3]));
  1. Create a CountryView view to display all the countries along with their details using a TableView.

    Countries

    The Name column, for example, can be created and bound using:

    CountryView.java
    TableView<Country> countryTable = new TableView<>();
    TableColumn<Country, String> nameCol = new TableColumn<>("Name");
    nameCol.setCellValueFactory(new PropertyValueFactory<Country, String>("name"));
    countryTable.getColumns().add(nameCol);

    The list of countries can be bound to the table view using:

    countryTable.setItems(FXCollections.observableArrayList(CountryRepository.getCountries()));
  2. Update the view to be able to filter countries based on the currently selected continent and region. You need to add the required components and implement the corresponding event handlers. Use a Toolbar to host the labels and dropdown lists.

    Countries

    The list of continents can be bound to the dropdown list using:

    continentComboBox.setItems(FXCollections.observableArrayList(CountryRepository.getContinents()));
Solution
Country.java
import java.io.Serializable;
 
public class Country implements Serializable {
  private String name;
  private String alpha3;
  private String continent;
  private String region;
 
  public Country(String name, String alpha3, String continent, String region) {
    this.name = name;
    this.alpha3 = alpha3;
    this.continent = continent;
    this.region = region;
  }
  public String getName() { return name; }
  public void setName(String name) { this.name = name; }
 
  public String getAlpha3() { return alpha3; }
  public void setAlpha3(String alpha3) { this.alpha3 = alpha3; }
 
  public String getContinent() { return continent; }
  public void setContinent(String continent) { this.continent = continent; }
 
  public String getRegion() { return region; }
  public void setRegion(String region) { this.region = region; }
 
  @Override
  public String toString() {
    return String.format("%s %s %s %s", getName(), getAlpha3(), getContinent(),
                         getRegion());
  }
}
CountryRepository.java
import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
 
public class CountryRepository {
  private static List<Country> countries;
 
  public static List<Country> getCountries() {
    if (countries == null) {
      loadCountries();
    }
    return countries;
  }
 
  public static List<String> getContinents() {
    List<String> continents = new ArrayList<>();
    for (Country c : getCountries()) {
      if (!continents.contains(c.getContinent())) {
        continents.add(c.getContinent());
      }
    }
    return continents;
  }
 
  public static List<String> getRegionsContinent(String continent) {
    List<String> regions = new ArrayList<>();
    for (Country c : getCountries()) {
      if (!regions.contains(c.getRegion()) &&
          c.getContinent().equalsIgnoreCase(continent)) {
        regions.add(c.getRegion());
      }
    }
    return regions;
  }
 
  public static List<Country> getCountriesRegionContinent(String continent,
                                                          String region) {
    List<Country> countries = new ArrayList<>();
    for (Country c : getCountries()) {
      if (c.getContinent().equalsIgnoreCase(continent) &&
          (region == null || c.getRegion().equalsIgnoreCase(region))) {
        countries.add(c);
      }
    }
    return countries;
  }
 
  private static void loadCountries() {
    Scanner scanner = null;
    try {
      scanner = new Scanner(new File("data/countries.csv"));
    } catch (FileNotFoundException e) {
      e.printStackTrace();
    }
 
    if (scanner != null) {
      countries = new ArrayList<>();
      scanner.nextLine();
      while (scanner.hasNext()) {
        String line = scanner.nextLine();
        String[] tokens = line.split(",");
        countries.add(new Country(tokens[0], tokens[1], tokens[2], tokens[3]));
      }
      scanner.close();
    }
  }
}
CountryView.java
import java.util.Arrays;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.ToolBar;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
 
public class CountryView {
  private Pane root;
 
  public CountryView() {
    ToolBar toolbar = new ToolBar();
    Label continentLabel = new Label("Continent");
    ComboBox<String> continentComboBox = new ComboBox<>();
    Separator toolbarSeparator = new Separator();
    Label regionLabel = new Label("Region");
    ComboBox<String> regionComboBox = new ComboBox<>();
 
    continentComboBox.setPrefWidth(200.0);
    regionComboBox.setPrefWidth(200.0);
 
    toolbar.getItems().addAll(continentLabel, continentComboBox,
                              toolbarSeparator, regionLabel, regionComboBox);
 
    TableView<Country> countryTable = new TableView<>();
 
    TableColumn<Country, String> nameCol = new TableColumn<>("Name");
    TableColumn<Country, String> alpha3Col = new TableColumn<>("Code");
    TableColumn<Country, String> continentCol = new TableColumn<>("Continent");
    TableColumn<Country, String> regionCol = new TableColumn<>("Region");
 
    nameCol.setPrefWidth(250);
    nameCol.setCellValueFactory(
        new PropertyValueFactory<Country, String>("name"));
    alpha3Col.setPrefWidth(50);
    alpha3Col.setCellValueFactory(
        new PropertyValueFactory<Country, String>("alpha3"));
    continentCol.setPrefWidth(75);
    continentCol.setCellValueFactory(
        new PropertyValueFactory<Country, String>("continent"));
    regionCol.setPrefWidth(200);
    regionCol.setCellValueFactory(
        new PropertyValueFactory<Country, String>("region"));
 
    countryTable.getColumns().addAll(
        Arrays.asList(nameCol, alpha3Col, continentCol, regionCol));
 
    countryTable.setItems(
        FXCollections.observableArrayList(CountryRepository.getCountries()));
    continentComboBox.setItems(
        FXCollections.observableArrayList(CountryRepository.getContinents()));
 
    continentComboBox.setOnAction(new EventHandler<ActionEvent>() {
      @Override
      public void handle(ActionEvent event) {
        regionComboBox.setItems(FXCollections.observableArrayList(
            CountryRepository.getRegionsContinent(
                continentComboBox.getValue())));
        countryTable.setItems(FXCollections.observableArrayList(
            CountryRepository.getCountriesRegionContinent(
                continentComboBox.getValue(), regionComboBox.getValue())));
      }
    });
    regionComboBox.setOnAction(new EventHandler<ActionEvent>() {
      @Override
      public void handle(ActionEvent event) {
        countryTable.setItems(FXCollections.observableArrayList(
            CountryRepository.getCountriesRegionContinent(
                continentComboBox.getValue(), regionComboBox.getValue())));
      }
    });
 
    BorderPane pane = new BorderPane(countryTable, toolbar, null, null, null);
    this.root = pane;
  }
 
  public Pane getRoot() { return root; }
}
Countries.java
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
 
public class Countries extends Application {
  @Override
  public void start(Stage stage) throws Exception {
    CountryView view = new CountryView();
    stage.setScene(new Scene(view.getRoot(), 600.0, 500.0));
    stage.setTitle("Countries");
    stage.show();
  }
 
  public static void main(String[] args) { launch(args); }
}

Exercise 2

Trip Register

  1. Create a Trip class based on the following diagram:
Trip-accounts: String-type: String-fromDate: LocalDate-toDate: LocalDate-rating: String+get*():type+set*(type): void+toString(): String
  1. Create a TripRepository model class to hold the list of trips and implement methods to load/save all the trips from/to data/trips.csv (trips.csv).
TripRepository-trips: List<Trip>+getTrips(): List<Trip>-loadTrips(): void+saveTrips(): void+getCountries(): List<String>+getTypes(): List<String>+getRatings(): List<String>
  1. Create a TripView view to display all the trips along with their details using a TableView.

    Trips

  2. Add buttons to add, update, and delete a trip. Add a button to save all the trips as well.

  3. Create a TripEditorView view to display and edit the details of a trip. This can be used to add a new trip or update a selected trip from the table.

    Trip Editor

    You can check whether an item is selected in the table or not using:

    TripView.java
    updateButton.setOnAction(new EventHandler<ActionEvent>() {
      @Override
      public void handle(ActionEvent event) {
        int index = tripTable.getSelectionModel().getSelectedIndex();
        if (index != -1) {
          ...
        }
      }
    }
  4. Create a TripEditorDialog modal dialog and use it to add new trips or update existing ones. A modal dialog can be created using:

    TripEditorDialog.java
    Dialog<ButtonType> dialog = new Dialog<>();
    TripEditorView view = new TripEditorView(trip);
    dialog.getDialogPane().setContent(view.getRoot());
    dialog.getDialogPane().getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK);
    Optional<ButtonType> result = dialog.showAndWait();
    ButtonType button = result.orElse(ButtonType.CANCEL);
     
    if (button == ButtonType.OK) {
      ...
    }
Solution
Trip.java
import java.io.Serializable;
import java.time.LocalDate;
 
public class Trip implements Serializable {
  private String country;
  private String type;
  private LocalDate fromDate;
  private LocalDate toDate;
  private String rating;
 
  public Trip() {}
 
  public Trip(String country, String type, LocalDate fromDate, LocalDate toDate,
              String rating) {
    this.country = country;
    this.type = type;
    this.fromDate = fromDate;
    this.toDate = toDate;
    this.rating = rating;
  }
 
  public String getCountry() { return country; }
  public void setCountry(String country) { this.country = country; }
 
  public String getType() { return type; }
  public void setType(String type) { this.type = type; }
 
  public LocalDate getFromDate() { return fromDate; }
  public void setFromDate(LocalDate fromDate) { this.fromDate = fromDate; }
 
  public LocalDate getToDate() { return toDate; }
  public void setToDate(LocalDate toDate) { this.toDate = toDate; }
 
  public String getRating() { return rating; }
  public void setRating(String rating) { this.rating = rating; }
 
  @Override
  public String toString() {
    return String.format("%s %s %s %s %s", getCountry(), getType(),
                         getFromDate(), getToDate(), getRating());
  }
}
TripRepository.java
import java.io.File;
import java.io.FileNotFoundException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Formatter;
import java.util.List;
import java.util.Scanner;
 
public class TripRepository {
  private static List<Trip> trips;
 
  public static List<Trip> getTrips() {
    if (trips == null) {
      loadTrips();
    }
    return trips;
  }
 
  private static void loadTrips() {
    Scanner scanner = null;
    try {
      scanner = new Scanner(new File("data/trips.csv"));
    } catch (FileNotFoundException e) {
      e.printStackTrace();
    }
 
    if (scanner != null) {
      trips = new ArrayList<>();
      scanner.nextLine();
      while (scanner.hasNext()) {
        String line = scanner.nextLine();
        String[] tokens = line.split(",");
        trips.add(new Trip(tokens[0], tokens[1], LocalDate.parse(tokens[2]),
                           LocalDate.parse(tokens[3]), tokens[4]));
      }
      scanner.close();
    }
  }
 
  public static void saveTrips(Trip[] trips) {
    Formatter formatter = null;
    try {
      formatter = new Formatter("data/trips.csv");
    } catch (FileNotFoundException e) {
      e.printStackTrace();
    }
 
    if (formatter != null) {
      formatter.format("country,type,from,to,rating%n");
      for (Trip trip : trips) {
        formatter.format("%s,%s,%s,%s,%s%n", trip.getCountry(), trip.getType(),
                         trip.getFromDate(), trip.getToDate(),
                         trip.getRating());
      }
      formatter.close();
    }
  }
 
  public static List<String> getCountries() {
    List<String> countries = new ArrayList<>();
    for (Country c : CountryRepository.getCountries()) {
      countries.add(c.getName());
    }
    Collections.sort(countries);
    return countries;
  }
 
  public static List<String> getTypes() {
    return List.of("Business", "Leisure");
  }
 
  public static List<String> getRatings() {
    return List.of("Excellent", "Good", "Poor");
  }
}
TripView.java
import java.time.LocalDate;
import java.util.Arrays;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.Button;
import javafx.scene.control.Separator;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.ToolBar;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
 
public class TripView {
  private Pane root;
  private ObservableList<Trip> trips;
 
  public TripView() {
    ToolBar toolbar = new ToolBar();
    Button addButton = new Button("Add");
    Button updateButton = new Button("Update");
    Button deleteButton = new Button("Delete");
    Separator toolbarSeparator = new Separator();
    Button saveButton = new Button("Save");
 
    toolbar.getItems().addAll(addButton, updateButton, deleteButton,
                              toolbarSeparator, saveButton);
 
    TableView<Trip> tripTable = new TableView<>();
 
    TableColumn<Trip, String> countryCol = new TableColumn<>("Country");
    TableColumn<Trip, String> typeCol = new TableColumn<>("Type");
    TableColumn<Trip, LocalDate> fromDateCol = new TableColumn<>("From");
    TableColumn<Trip, LocalDate> toDateCol = new TableColumn<>("To");
    TableColumn<Trip, String> ratingCol = new TableColumn<>("Rating");
 
    countryCol.setPrefWidth(100);
    countryCol.setCellValueFactory(
        new PropertyValueFactory<Trip, String>("country"));
    typeCol.setPrefWidth(70);
    typeCol.setCellValueFactory(new PropertyValueFactory<Trip, String>("type"));
    fromDateCol.setPrefWidth(85);
    fromDateCol.setCellValueFactory(
        new PropertyValueFactory<Trip, LocalDate>("fromDate"));
    toDateCol.setPrefWidth(85);
    toDateCol.setCellValueFactory(
        new PropertyValueFactory<Trip, LocalDate>("toDate"));
    ratingCol.setPrefWidth(70);
    ratingCol.setCellValueFactory(
        new PropertyValueFactory<Trip, String>("rating"));
 
    tripTable.getColumns().addAll(
        Arrays.asList(countryCol, typeCol, fromDateCol, toDateCol, ratingCol));
 
    trips = FXCollections.observableArrayList(TripRepository.getTrips());
    tripTable.setItems(trips);
 
    addButton.setOnAction(new EventHandler<ActionEvent>() {
      @Override
      public void handle(ActionEvent event) {
        Trip trip = new Trip();
        if (edit(trip)) {
          trips.add(trip);
        }
      }
    });
 
    updateButton.setOnAction(new EventHandler<ActionEvent>() {
      @Override
      public void handle(ActionEvent event) {
        int index = tripTable.getSelectionModel().getSelectedIndex();
        if (index != -1) {
          Trip trip = new Trip(
              trips.get(index).getCountry(), trips.get(index).getType(),
              trips.get(index).getFromDate(), trips.get(index).getToDate(),
              trips.get(index).getRating());
          if (edit(trip)) {
            trips.set(index, trip);
          }
        }
      }
    });
 
    deleteButton.setOnAction(new EventHandler<ActionEvent>() {
      @Override
      public void handle(ActionEvent event) {
        int index = tripTable.getSelectionModel().getSelectedIndex();
        if (index != -1) {
          trips.remove(index);
        }
      }
    });
 
    saveButton.setOnAction(new EventHandler<ActionEvent>() {
      @Override
      public void handle(ActionEvent event) {
        TripRepository.saveTrips(trips.toArray(new Trip[trips.size()]));
      }
    });
 
    BorderPane pane = new BorderPane(tripTable, toolbar, null, null, null);
    this.root = pane;
  }
 
  public Pane getRoot() { return root; }
 
  private boolean edit(Trip trip) { return TripEditorDialog.edit(trip); }
}
TripEditorView.java
import javafx.collections.FXCollections;
import javafx.geometry.Insets;
import javafx.scene.control.ComboBox;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Label;
import javafx.scene.control.RadioButton;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
 
public class TripEditorView {
  private Pane root;
  Trip trip;
 
  ComboBox<String> countryComboBox;
  ToggleGroup typeToggle;
  // ComboBox<String> typeComboBox;
  DatePicker fromDatePicker;
  DatePicker toDatePicker;
  ComboBox<String> ratingComboBox;
 
  public TripEditorView(Trip trip) {
    this.trip = trip;
 
    GridPane grid = new GridPane();
 
    Label countryLabel = new Label("Country");
    countryComboBox = new ComboBox<>();
    Label typeLabel = new Label("Type");
    HBox typeContainer = new HBox();
    typeToggle = new ToggleGroup();
    for (String type : TripRepository.getTypes()) {
      RadioButton typeRadio = new RadioButton(type);
      typeRadio.setToggleGroup(typeToggle);
      typeContainer.getChildren().add(typeRadio);
      if (type.equals(trip.getType())) {
        typeRadio.setSelected(true);
      }
    }
    typeContainer.setSpacing(10.0);
    // typeComboBox = new ComboBox<>();
    Label fromDateLabel = new Label("From");
    fromDatePicker = new DatePicker();
    Label toDateLabel = new Label("To");
    toDatePicker = new DatePicker();
    Label ratingLabel = new Label("Rating");
    ratingComboBox = new ComboBox<>();
 
    countryComboBox.setItems(
        FXCollections.observableArrayList(TripRepository.getCountries()));
    // typeComboBox.setItems(
    //     FXCollections.observableArrayList(TripRepository.getTypes()));
    ratingComboBox.setItems(
        FXCollections.observableArrayList(TripRepository.getRatings()));
 
    countryComboBox.setValue(trip.getCountry());
    // typeComboBox.setValue(trip.getType());
    fromDatePicker.setValue(trip.getFromDate());
    toDatePicker.setValue(trip.getToDate());
    ratingComboBox.setValue(trip.getRating());
 
    grid.add(countryLabel, 0, 0);
    grid.add(countryComboBox, 1, 0);
    grid.add(typeLabel, 0, 1);
    grid.add(typeContainer, 1, 1);
    // grid.add(typeComboBox, 1, 1);
    grid.add(fromDateLabel, 0, 2);
    grid.add(fromDatePicker, 1, 2);
    grid.add(toDateLabel, 0, 3);
    grid.add(toDatePicker, 1, 3);
    grid.add(ratingLabel, 0, 4);
    grid.add(ratingComboBox, 1, 4);
 
    grid.getColumnConstraints().add(new ColumnConstraints(100.0));
    grid.getColumnConstraints().add(new ColumnConstraints(180.0));
 
    grid.setHgap(10);
    grid.setVgap(10);
    grid.setPadding(new Insets(10.0));
 
    this.root = grid;
  }
 
  public Pane getRoot() { return root; }
}
TripEditorDialog.java
import java.util.Optional;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.DialogPane;
import javafx.scene.control.RadioButton;
 
public class TripEditorDialog {
  public static boolean edit(Trip trip) {
    Dialog<ButtonType> dialog = new Dialog<>();
    TripEditorView view = new TripEditorView(trip);
    DialogPane pane = dialog.getDialogPane();
    pane.setContent(view.getRoot());
    pane.getButtonTypes().addAll(ButtonType.CANCEL, ButtonType.OK);
 
    Optional<ButtonType> result = dialog.showAndWait();
    ButtonType button = result.orElse(ButtonType.CANCEL);
 
    if (button == ButtonType.OK) {
      if (view.countryComboBox.getValue() != null &&
          // view.typeComboBox.getValue() != null &&
          view.typeToggle.getSelectedToggle() != null &&
          view.fromDatePicker.getValue() != null &&
          view.toDatePicker.getValue() != null &&
          view.ratingComboBox.getValue() != null &&
          !view.fromDatePicker.getValue().isAfter(
              view.toDatePicker.getValue())) {
        view.trip.setCountry(view.countryComboBox.getValue());
        // view.trip.setType(view.typeComboBox.getValue());
        view.trip.setType(
            ((RadioButton)view.typeToggle.getSelectedToggle()).getText());
        view.trip.setFromDate(view.fromDatePicker.getValue());
        view.trip.setToDate(view.toDatePicker.getValue());
        view.trip.setRating(view.ratingComboBox.getValue());
        return true;
      }
    }
 
    return false;
  }
}
Trips.java
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
 
public class Trips extends Application {
  @Override
  public void start(Stage stage) throws Exception {
    TripView view = new TripView();
    stage.setScene(new Scene(view.getRoot(), 460.0, 400.0));
    stage.setTitle("Trips");
    stage.show();
  }
 
  public static void main(String[] args) { launch(args); }
}