TableView
Tyhjän rivin klikkaaminen
Oletuksena TableView-komponentti ei poista valintaa, jos käyttäjä klikkaa tyhjää riviä. Tämä on usein epäintuitiivista.
Tyhjän rivin klikkaaminen saadaan koodissa kiinni esimerkiksi asettamalla
riveille setOnMouseClicked-kuuntelija, joka tarkistaa, onko klikattu rivi null ja poistaa valinnan, jos näin on.
tableView.setRowFactory(tv -> {
TableRow<MyData> rivi = new TableRow<>();
rivi.setOnMouseClicked(tapahtuma -> {
if (rivi.isEmpty()) {
tableView.getSelectionModel().clearSelection();
}
});
return rivi;
});
Rivien suodattaminen
Esimerkki on pitkähkö; löydät sen kokonaisuudessaan GitHubista.
TableView-komponentti ei tarjoa suoraan tukea rivien
suodattamiseen.JavaFX-kirjastossa on FilteredList-luokka, joka mahdollistaa
suodattamisen.
Oletetaan, että meillä on tehtäviä ja kategorioita. Kukin tehtävä kuuluu johonkin yhteen kategoriaan. Haluamme valita kategorian pudotusvalikosta ja nähdä vain kyseiseen kategoriaan kuuluvat tehtävät.
Tehtävä ja Kategoria voisivat näyttää seuraavilta:
package fi.jyu.ohj2.esimerkit.filteredlist;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class Tehtava {
private final StringProperty otsikko = new SimpleStringProperty();
private final ObjectProperty<Kategoria> kategoria = new SimpleObjectProperty<>();
public Tehtava() {
// Tarvitaan Jacksonille
}
public Tehtava(String otsikko, Kategoria kategoria) {
this.otsikko.set(otsikko);
this.kategoria.set(kategoria);
}
// Otsikko
public void setOtsikko(String otsikko) {
this.otsikko.set(otsikko);
}
public String getOtsikko() {
return otsikko.get();
}
public StringProperty otsikkoProperty() {
return otsikko;
}
// Kategoria
public void setKategoria(Kategoria kategoria) {
this.kategoria.set(kategoria);
}
public Kategoria getKategoria() {
return kategoria.get();
}
public ObjectProperty<Kategoria> kategoriaProperty() {
return kategoria;
}
}
Käyttöliittymä voisi näyttää vaikkapa seuraavalta: Meillä on TableView
tehtävien näyttämistä varten, CheckBox-komponentti, jolla aktivoidaan
suoritus, ja ComboBox-komponentti, josta valitaan kategoria.
Tee kullekin elementille fx:id ja vastaavat kentät kontrolleriin.
Lisää attribuutti FilteredList<Tehtava> suodatetutTehtavat kontrolleriin.
Kun olet ladannut tehtävät ja kategoriat esimerkiksi JSON-tiedostosta, luo FilteredList-olio ja aseta se TableView-komponentin datalähteeksi:
// Oletetaan, että tehtavat on ladattu ObservableListiin nimeltä 'tehtavat'
suodatetutTehtavat = new FilteredList<>(tehtavat, t -> true);
tableView.setItems(suodatetutTehtavat);
Tässä t -> true on lambdalauseke joka määrittää mitkä tehtävät näytetään.
Aluksi kaikki näytetään, koska ehto on aina tosi.
Lisätään ComboBox-komponentille kuuntelija, joka päivittää suodatuskriteeriä:
comboBox.setOnAction(event -> {
paivitaSuodatus();
});
private void paivitaSuodatus() {
Kategoria valittuKategoria = comboBox.getSelectionModel().getSelectedItem();
suodatetutTehtavat.setPredicate(t ->
t.getKategoria().getNimi().equals(valittuKategoria.getNimi())
);
}
Tässä setPredicate-metodi määrittää suodatuskriteerin. Jos kategoria on
valitsematta, näytetään kaikki tehtävät. Muuten näytetään vain ne tehtävät,
joiden kategoria vastaa valittua kategoriaa.
Tässä on kuitenkin se ongelma, että kerran valittua filtteröintiä ei voida
poistaa. Siksi lisäsimme CheckBox-komponentin, jolla filtteröinti voidaan
aktivoida ja deaktivoida. Muutetaan paivitaSuodatus-metodia seuraavasti:
private void paivitaSuodatus() {
Kategoria valittuKategoria = comboBox.getSelectionModel().getSelectedItem();
if (checkBox.isSelected() && valittuKategoria != null) {
suodatetutTehtavat.setPredicate(t ->
t.getKategoria().getNimi().equals(valittuKategoria.getNimi())
);
} else {
suodatetutTehtavat.setPredicate(t -> true); // Näytä kaikki
}
}
Suodatus kannattaa disabloida kokonaan, kun CheckBox-komponentti ei ole
valittuna. Tämä onnistuu esimerkiksi näin.
comboBox.disableProperty().bind(checkBox.selectedProperty().not());
Tämä rivi vaatinee hieman selitystä. Tässä comboBox-komponentin sitominen
disabloidaan checkBox-komponentin selected-ominaisuuden käänteisen arvon
mukaisesti. Toisin sanoen, kun checkBox on valittuna, comboBox-komponenttia
"ei disabloida". JavaFX:ssä ei ole enableProperty()-metodia, joten meidän on
käytettävä disableProperty()-metodia ja käännettävä sen arvo.
Tämä selectedProperty on olemassa CheckBox-komponentissa
valmiina, joten sitä ei tarvitse erikseen määritellä.
Lopputulos näyttää vaikkapa tältä:
Solujen uudelleenmuotoilu
Tämäkin esimerkki löytyy kokonaisuudessaan GitHubista.
Joskus voi olla tarvetta muuttaa solun ulkoasua tietyissä olosuhteissa.
Jatkaen edellistä esimerkkiä, oletetaan, että kategorioita voisi poistaa. Poistettu kategoria halutaan näyttää punaisella tekstillä, jotta käyttäjä huomaa, että kategoria on poistettu. Käyttöliittymä voisi näyttää vaikkapa seuraavalta:

Tieto siitä, onko kategoria poistettu, voisi olla Kategoria-luokan
ominaisuutena, ja luonnollisesti mukana myös JSON-tiedostossa. Katso esimerkit
näistä luokista GitHubista: Kategoria.java, kategoriat.json.
Toki voisimme lisätä taulukkoon uuden sarakkeen, joka näyttää, onko kategoria poistettu ja tehdä suodatusta sitä kautta. Tämä ei kuitenkaan ole aina kovin elegantti ratkaisu. Parempi idea on näyttää tieto poistetuista kategorioista suoraan kategorian nimessä, esimerkiksi punaisella tekstillä.
Edellisessä esimerkissä lisäsimme kategoriasarakkeen seuraavasti.
kategoriaColumn.setCellValueFactory(cellData -> cellData.getValue().kategoriaProperty().asString());
Tällä määritellään, mistä tieto soluun haetaan. Sillä ei voi vaikuttaa solun
ulkoasuun. Se täytyy tehdä setCellFactory-metodin avulla, kuten opimme osassa
8.2. Valitettavasti ei
ole mitään valmista CellFactory-luokkaa, joka osaisi muuttaa tekstin värin.
Niinpä meillä on kaksi vaihtoehtoa: (1) Voimme tehdä oman CellFactory-luokan
perimällä TableCell-luokan tai (2) käyttää lambdalauseketta. Ensimmäinen
vaihtoehto on työläämpi, mutta hyödyllinen, jos käytämme samaa logiikkaa
useammassa sarakkeessa tai jopa useammassa taulukossa. Tässä tapauksessa meille
kuitenkin riittää yhden sarakkeen muokkaus, joten käytämme lambdalauseketta.
kategoriaColumn.setCellFactory(column -> {
return new TableCell<Tehtava, String>() {
@Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) { // Tyhjä solu täytyy käsitellä erikseen
setText(null);
setStyle("");
} else {
setText(item); // Näytä soluun kuuluva teksti
Tehtava tehtava = getTableRow().getItem(); // Hakee koko rivin datan
if (tehtava != null && tehtava.getKategoria().isPoistettu()) {
setStyle("-fx-text-fill: red;"); // Asettaa poistetun tekstin punaiseksi
} else {
setStyle(""); // Palauttaa oletustyylin, jos kategoria ei ole poistettu
}
}
}
};
});
Perusideana on ylikirjoittaa TableCell-luokan updateItem-metodi. Tätä metodia
kutsutaan aina, kun TableView-oliossa olevan solun sisältöä tai ulkoasua
tarvitsee päivittää, esimerkiksi kun taulukko renderöidään tai kun solun arvo
muuttuu. Ylikirjoittamalla updateItem-metodin voimme määritellä tarkasti,
miten solun sisältö ja ulkoasu päivitetään.
Lisätään vielä valintapainike, jolla käyttäjä voi näyttää poistetut kategoriat tai piilottaa ne. Oletuksena tämän valintapainikkeen pitää olla pois päältä ja silloin näytetään vain ne tehtävät, jotka eivät kuulu poistettuihin kategorioihin.
@FXML
private CheckBox naytaMyosPoistetutCheckBox;
// ...
public void initialize(URL url, ResourceBundle resourceBundle) {
// ...
naytaMyosPoistetutCheckBox.setOnAction(event -> {
if (naytaMyosPoistetutCheckBox.isSelected()) {
suodatetutTehtavat.setPredicate(t -> true); // Näytä kaikki
} else {
paivitaSuodatus();
}
});
// ...
}
@FXML
private void paivitaSuodatus() {
Kategoria valittuKategoria = valitseKategoriaComboBox.getSelectionModel().getSelectedItem();
if (suodataCheckBox.isSelected() && valittuKategoria != null) {
suodatetutTehtavat.setPredicate(t -> t.getKategoria().getNimi().equals(valittuKategoria.getNimi()));
} else {
// Näytä kaikki tehtävät, jotka eivät kuulu poistettuihin kategorioihin
suodatetutTehtavat.setPredicate(t -> !t.getKategoria().isPoistettu());
}
}
Poistettujen kategorioiden valintapainike kannattaa disabloida, kun muu suodatus on käytössä.
naytaMyosPoistetutCheckBox.disableProperty().bind(suodataCheckBox.selectedProperty());
Tämän seurauksena pitää myös resetoida käyttöliittymästä poistettujen kategorioiden valintapainike suodatuksen yhteydessä.
suodataCheckBox.selectedProperty()
.addListener((obs, vanha, uusi) ->
naytaMyosPoistetutCheckBox.setSelected(false));