Johdetut Observable-arvot¶
Käyttöliittymässä haluamme usein näyttää varsinaisen tiedon lisäksi tiedoista laskettuja arvoja, kuten arvojen summaa, keskiarvoa, yhdistelmää tai vastaavaa.
Esimerkiksi henkilön koko nimi voidaan laskea etunimen ja sukunimen yhdistelmänä:
Näin laskettu arvo ei kuitenkaan päivity automaattisesti käyttöliittymässä, jos etunimi tai sukunimi muuttuu.
Esimerkki¶
Katsotaan seuraavaa sovellusta henkilön tietojen syöttämiseksi:
public class MainController implements Initializable {
@FXML
private TableColumn<Pelaaja, String> nimiColumn;
@FXML
private TableColumn<Pelaaja, Number> syntymavuosiColumn;
@FXML
private TableColumn<Pelaaja, Number> ikaColumn;
@FXML
private TableView<Pelaaja> pelaajatTable;
@FXML
private Label pelaajiaLkmLabel;
@FXML
private Button lisaaPelaajaButton;
private ObservableList<Pelaaja> pelaajat = FXCollections.observableArrayList();
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
nimiColumn.setCellFactory(TextFieldTableCell.forTableColumn());
nimiColumn.setCellValueFactory(cellData -> cellData.getValue().nimiProperty());
syntymavuosiColumn.setCellFactory(TextFieldTableCell.forTableColumn(new NumberStringConverter("####")));
syntymavuosiColumn.setCellValueFactory(cellData -> cellData.getValue().syntymavuosiProperty());
// Tämä ei toimi!
// setCellValueFactory vaatii ObservableValue-arvon, mutta ikä on int-kokonaisluku
// ikaColumn.setCellValueFactory(cellData -> LocalDate.now().getYear() - cellData.getValue().getSyntymavuosi());
lisaaPelaajaButton.setOnAction(event -> {
Pelaaja uusiPelaaja = new Pelaaja();
uusiPelaaja.setNimi("Uusi Pelaaja");
uusiPelaaja.setSyntymavuosi(2000);
pelaajat.add(uusiPelaaja);
});
pelaajatTable.setItems(pelaajat);
}
}
public class Pelaaja {
StringProperty nimi = new SimpleStringProperty();
IntegerProperty syntymavuosi = new SimpleIntegerProperty();
public StringProperty nimiProperty() { return nimi; }
public String getNimi() { return nimi.get(); }
public void setNimi(String nimi) { this.nimi.set(nimi); }
public IntegerProperty syntymavuosiProperty() { return syntymavuosi; }
public int getSyntymavuosi() { return syntymavuosi.get(); }
public void setSyntymavuosi(int syntymavuosi) { this.syntymavuosi.set(syntymavuosi); }
}
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.TableColumn?>
<?import javafx.scene.control.TableView?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.VBox?>
<VBox alignment="CENTER" spacing="20.0" xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1" fx:controller="fi.ohj2.esimerkit.bindingstest.MainController">
<padding>
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
</padding>
<children>
<TableView fx:id="pelaajatTable" editable="true" prefHeight="200.0" prefWidth="200.0">
<columns>
<TableColumn fx:id="nimiColumn" prefWidth="200.0" text="Nimi" />
<TableColumn fx:id="syntymavuosiColumn" prefWidth="50.0" text="Syntymävuosi" />
<TableColumn fx:id="ikaColumn" editable="false" prefWidth="50.0" text="Ikä" />
</columns>
<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
</columnResizePolicy>
</TableView>
<HBox VBox.vgrow="NEVER">
<children>
<Label text="Pelaajia: " />
<Label fx:id="pelaajiaLkmLabel" text="0" />
</children>
</HBox>
<Button fx:id="lisaaPelaajaButton" mnemonicParsing="false" text="Lisää pelaaja" />
</children>
</VBox>
Tässä esimerkissä näemme kaksi ongelmaa:
- Uuden pelaajan lisääminen ei päivitä pelaajien lukumäärää
- Pelaajan syntymävuoden muokkaaminen ei päivitä pelaajan ikää
Observable-arvon muuttaminen toiseksi arvoksi¶
Kaikki ObservableValue-arvot, kuten StringProperty, IntegerProperty,
FloatProperty, jne. sisältävät map()-apumetodin (ks.
JavaDoc),
jonka avulla arvolle voi suorittaa laskutoimituksia.
Esimerkiksi pelaajan ikä saadaan laskettua syntymävuodesta seuraavasti:
ObservableValue<Number> ika = pelaaja.syntymavuosiProperty().map(vuosi -> LocalDate.now().getYear() - vuosi.intValue());
Tällöin ika-muuttujan sisältämä arvo lasketaan syntymävuodesta kaavalla
LocalDate.now().getYear() - vuosi.intValue() aina, kun pelaajan syntymävuosi
muuttuu. Koska ObservableValue on havaittava arvo, voimme käyttää sitä
setCellValueFactory-metodissa:
public void initialize(URL url, ResourceBundle resourceBundle) {
nimiColumn.setCellFactory(TextFieldTableCell.forTableColumn());
nimiColumn.setCellValueFactory(cellData -> cellData.getValue().nimiProperty());
syntymavuosiColumn.setCellFactory(TextFieldTableCell.forTableColumn(new NumberStringConverter("####")));
syntymavuosiColumn.setCellValueFactory(cellData -> cellData.getValue().syntymavuosiProperty());
ikaColumn.setCellValueFactory(cellData ->
cellData.getValue().syntymavuosiProperty().map(
syntymavuosi -> LocalDate.now().getYear() - syntymavuosi.intValue()));
lisaaPelaajaButton.setOnAction(event -> {
Pelaaja uusiPelaaja = new Pelaaja();
uusiPelaaja.setNimi("Uusi Pelaaja");
uusiPelaaja.setSyntymavuosi(2000);
pelaajat.add(uusiPelaaja);
});
pelaajatTable.setItems(pelaajat);
}
Funktion muuttaminen Observable-arvoksi¶
Pelaajien lukumäärä saadaan kutsumalla pelaajat.size().
size()-metodi ei kuitenkaan palauta havaittavaa arvoa, eikä pelaajat-lista
sisällä yllä mainittua map()-metodia.
Voimme kuitenkin muuntaa minkä tahansa funktion havaittavaksi käyttämällä
Bindings-luokan (ks.
JavaDoc)
createXBinding-apumetodeja. Tässä X tarkoittaa havaittavan arvon tyyppiä,
eli esimerkiksi Integer, Long, String tai Object.
Koska size() on kokonaisluku, käytämme
Bindings.createIntegerBinding()-metodia (ks.
JavaDoc).
Bindings.createIntegerBinding() ottaa vähintään kaksi parametria:
lambdalausekkeen, josta havaittava arvo lasketaan ja yhden tai useamman
Observable-arvon, jonka muuttuessa havaittava arvo lasketaan uudestaan.
Tässä tapauksessa ensimmäinen parametri kertoo, että pelaajienLkm-arvo lasketaan aina
lausekkeella pelaajat.size(). Toinen parametri pelaajat kertoo, että arvo on
päivitettävä aina, kun pelaajat-listan sisältö muuttuu.
Bindings.createXBinding-metodi palauttaa Binding-tyyppisen havaittavan
arvon, jonka voi käyttää samalla tavalla kuin muut Observable-arvot.
Tässä tapauksessa voimme sitoa pelaajiaLkmLabel-kentän tekstin
textProperty()-arvoon:
public void initialize(URL url, ResourceBundle resourceBundle) {
nimiColumn.setCellFactory(TextFieldTableCell.forTableColumn());
nimiColumn.setCellValueFactory(cellData -> cellData.getValue().nimiProperty());
syntymavuosiColumn.setCellFactory(TextFieldTableCell.forTableColumn(new NumberStringConverter("####")));
syntymavuosiColumn.setCellValueFactory(cellData -> cellData.getValue().syntymavuosiProperty());
ikaColumn.setCellValueFactory(cellData -> cellData.getValue().syntymavuosiProperty().map(syntymavuosi -> LocalDate.now().getYear() - syntymavuosi.intValue()));
lisaaPelaajaButton.setOnAction(event -> {
Pelaaja uusiPelaaja = new Pelaaja();
uusiPelaaja.setNimi("Uusi Pelaaja");
uusiPelaaja.setSyntymavuosi(2000);
pelaajat.add(uusiPelaaja);
});
IntegerBinding pelaajienLkm = Bindings.createIntegerBinding(() -> pelaajat.size(), pelaajat);
pelaajiaLkmLabel.textProperty().bind(pelaajienLkm.asString());
pelaajatTable.setItems(pelaajat);
}
Property-tyypin bind()-metodilla voimme sitoa arvon toiseen havaittavaan
arvoon. Tässä tapauksessa pelaajiaLkmLabel-kentän teksti sidotaan
pelaajien lukumäärään. Tällöin, jos pelaajat-lista muuttuu, niin
pelaajienLkm-arvo havaitsee muutoksen ja laskee arvonsa uudestaan lausekkeellapelaajat.size()pelaajienLkm.asString()havaitsee muutoksenpelaajienLkm-arvossa ja päivittää arvonsa kutsumallapelaajienLkm.toString()pelaajiaLkmLabel.textProperty()havaitsee muutoksenpelaajienLkm.asString()-arvossa ja päivittää oman sisältönsä vastaamaan uutta arvoa
Muutosten jälkeen pelaajien lukumäärä ja yksittäisen pelaajan ikä päivittyy automaattisesti: