JavaFX käyttö
Tässä dokumentissa kerrotaan:
- miten luodaan erittäin yksinkertainen JavaFX-ohjelma HelloWorld
- näytetään miten sama tehdään
.fxml
-tiedostoa käyttäen - lisätään ohjelmaan yksi painike, jolla muutetaan tekstiä
- tehdään yksinkertainen autolaskuri
- tutustutaan sitomiseen (binding), minkä avulla voidaan yksinkertaistaa käyttöliittymän ja datan välistä suhdetta.
- kerrotaan tyylitiedostojen käyttämisestä
- luetellaan joukko tyypillisiä virheitä, joita JavaFX ohjelmaan helposti tulee
Katso lisäksi:
1. Hello World
Tässä luvussa on kuvattuna yksinkertaisen JavaFX-ohjelman rakenne alkaen yhden Java-tiedoston esimerkistä useamman tiedoston MVC-mallin mukaiseen rakenteeseen. Esimerkit voi ladata (Download) versionhallinnasta osoitteesta:
tai koko projektin muine esimerkkeineen ja Eclipse-projekti:
1.1 Sovellus pelkästään Javalla tehtynä
Yksinkertaisin JavaFX-ohjelma olisi karkeasti kuten alla. Ohjelma tekee pienen ikkunan, jonka keskelle tulee teksti Hello World!
. TIM-ympäristössä ajettuna esimerkeissä ei tosin näy työasemissa näkyvää ikkunan kehystä ja otsikkopalkkia.
public class HelloWorld extends Application {
peritään yleinen JavaFX sovellusluokkaApplication
ja korvataan senstart
-metodi omallalaunch(args);
pääohjelmasta käynnistetään varsinainen JavaFX sovellus jolle viedään parametrina ohjelman käynnistykseen tulleet argumentitpublic void start(Stage primaryStage) {
tehdään oma versiostart
-metodista, jonka pitää luoda varsinainen näkyvä sisältö.BorderPane root = new BorderPane();
luodaan pohjapaneli (ks. Containers), jonka päälle laitetaan näytettäviä komponenttejaLabel label = new Label("Hello World!");
esimerkin vuoksi luodaan yksi tekstiä näyttävä komponentti (ks. Components)root.setCenter(label);
joka lisätään pohjapanelin keskelleScene scene = new Scene(root);
luodaan "näytös", johon liitetään näytettävät panelitprimaryStage.setScene(scene);
ja tämä lisätään parametrina tulleenstagen
(näyttämö) päälle. Useinstage
on työpöytäkoneissa ohjelman ikkuna. Tässä tapauksessa ohjelman pääikkuna. Tableteissa tai kännyköissästage
on yksi ohjelman näkymä joka yleensä on koko ruudun kokoinen.primaryStage.show();
laitetaan pääikkuna näkyville ja sen jälkeen sovellus pyörii "omillaan".
Application
-luokasta perinnän ansiosta se mm- sulkeutuu yläkulman ruksista. Muuten toiminnoista huolehditaan tapahtumankäsittelijöiden avulla. Niistä myöhemmin.
Eclipsessä projekti tehdään seuraavasti:
- jos ei ole vielä Workspacea, luo se, muuten voit käyttää vanhaa
File/New/Other/JavaFX/JavaFX project
- Projektin nimeksi vaikkapa
fxHello
- hakemiston paikka kannattaa vaihtaa omaan hakemistoon
Next
Next
- Package name:
fxHello
- Language:
None
- syntyneessä
src/fxHello
-hakemistossaMain.java
nimeksiHelloWorld.java
- korvaa koodi edellä olevalla koodilla
F11
Toki koodia voitaisiin vielä hieman lyhentää ketjuttamalla kutsuja:
Yleensä kuitenkin esimerkiksi halutaan vaihtaa kirjasimen (font) kokoa tms. jolloin Label
-muuttujaa tarvitaan näyttöön sijoittamisen jälkeenkin.
1.2 Sovellus käyttäen fxml-tiedostoa
Edellä koko ohjelman "ulkoasu" oli luotu itse siinä start
-metodissa. JavaFX:ssä "ikkunoiden" ulkoasu voidaan esittää myös XML-pohjaisissa .fxml
-tiedostoissa. Näitä tiedostoja voi tehdä millä tahansa teksieditorilla taikka esimerkiksi SceneBuilder-ohjelmalla.
Tässä tiedostossa on sanottu että perusrunko rakennetaan BorderPane
-containerista. Ja tämän keskiosaan laitetaan Label
jossa on haluttu teksti.
Vastaavasti pääohjelman start
-metodissa ladataan tämä .fxml
-tiedosto:
Tässä ohjelman luokan paketin (package) muodostamasta resurssista ladataan mainittuPane root = FXMLLoader.load(getClass().getResource("HelloWorldView.fxml"));
.fxml
-tiedosto. Koska latauksessa voi tulla poikkeus täytyy se käsitellä. Yleinen ongelma on se, että mainittua tiedostoa ei ole samassa hakemistossa (paketissa) kuin itse luokka.
Kun .fxml
-tiedosto ladataan, se tulkitaan ja luodaan kaikki vastaavat graafiset elementit mitä siellä on sanottu. Mikäli kaikki onnistuu, palautetaan container-olio (tässä tapauksessa BorderPane
) jonka päälle muut elementit on kasattu. Pääohjelma ei muutu miksikään vaikka .fxml
-tiedostoon lisättäisiin elementtejä kuinka paljon tahansa.
1.3 Tapahtumankäsittely
Yleensä ohjelma jossa ei ole lainkaan tapahtumankäsittelyä, on aika hyödytön. Lisätään ohjelman ensimmäiseen versioon painike, jota painamalla labelin
teksti vaihtuu toiseksi.
HUOM! TIM-ympäristöstä ajettuna ei voi painaa painiketta, eli esimerkki kannattaa ladata omaan IDEen (esim. Eclipse).
button.setOnAction( e -> label.setText("Well Done!"));
Tässä on asetettu Javan Lambda -lauseketta käyttäen tapahtumankäsittelijä, jossa painikkeen painamisesta tulee aliohjelmakutsu, jolle tulee parametrina tapahtuma (e
) ja tämä käsitellään niin, että vaihdetaanlabel
-komponentin teksti toiseksi. Itse tapahtumaparametriae
ei tässä esimerkissä käytetä mihinkään. Siitä pystyisi selvittämään tapahtuman lähdettä, tapahtuman aikahetkeä yms.
1.4 Tapahtumat ja FXML
Edellä label
-komponentin löytyminen oli helppoa, koska muuttuja oli itse luotu. Kun ulkoasu ladataan .fxml
-tiedostosta, ei siellä oleviin muuttujiin päästä ilman muuta käsiksi. Jotta .fxml
-tiedostossa luodut elementit olisivat käytössä Java-tiedostossa, pitää Java-tiedostoon tehdä vastaava muuttuja ja ilmoittaa .fxml
-tiedostossa että kun se ladataan, pitää tämä muuttuja alustaa viittamaan vastaavaan elementtiin.
<BorderPane ... fx:controller="hello.button.HelloWorldMain">
Mikäli.fxml
-tiedosto halutaan yhdistää johonkin Java-luokkaan, pitää.fxml
-tiedostossa sanoa mihin luokkaan elementtejä ja tapahtumankäsittelijöitä yhdistetään. Tässä luokan paketin nimi ja luokan nimi pitää kirjoittaa todella huolellisesti, muuten lataaminen ei onnistu.
Huomautus: Lataaja luo uuden käsittelijäluokan olion ja se on eri olio kuin se, josta lataajaa kutsuttiin!<Label fx:id="label" text="Hello World!"...>
MainittuLabel
-komponentin liitos Java-tiedoston muuttujaanlabel
on tehty tässä. Vastaavasti pitää itse Java-tiedostosta löytyä rivi:@FXML private Label label;
joka tarkoittaa sitä, että kun.fxml
-tiedostoa ladataan, etsitään luokasta vastaava muuttuja ja laitetaan se viittamaan latauksessa luotuun vastaavaan elementtiin. Tässä ehdottomasti nimilabel
pitää olla samalla tavalla kirjoitettuna molemmissa tiedostoissa. Toki nimi voi olla mikä tahansa, kunhan se on sama molemmissa. Kahdelle elementille ei voi antaa samaafx:id
-nimeä.<Button ... onAction="#handlePressed" ...>
vastaavasti yhdistetään kunkin komponentin tapahtumankäsittelijät Java-luokassa oleviin käsittelijämetodeihin. Jälleen metodin nimi pitää kirjoittaa huolella samaksi molemmissa tiedostoissa. Päinvastoin kuinfx:id
:n kanssa, voidaan usealle elementille ja elementin tapahtumalle (joita on kymmeniä) antaa sama käsittelijämetodin viite. Tämä on jopa usein järkevääkin, että esimerkiksi menu-valinnasta ja painikkeesta tapahtuu sama asia.
Java-tiedosto on vastaavasti (katso koko koodi):
@FXML private Label label;
Tässä@FXML
tarkoittaa että lataaja etsii näitä rivejä ja mikäli vastaavafx:id
attribuutti löytyy.fxml
-tiedostosta, niin viite ko. elementtiin sijoitetaan tähän muuttujaan. Vastaa karkeasti alkuperäisessä ohjelmassa ollutta
Label label = new Label(...);
@FXML private void handlePressed() { label.setText("Well Done!"); }
Vastaavasti nyt@FXML
tarkoittaa että jos jossakin komponentissa on esimerkiksi
onAction="#handlePressed"
niin tässä tapauksessa ko. painikkeen painamisesta kutsutaanhandlePressed
-nimistä metodia. Metodilla voisi tässä tapauksessa olla myösActionEvent
-tyyppinen parametri, mutta tässä esimerkissä sitä ei tarvita.
1.5 Käsittelijäluokka omaan tiedostoonsa - MVC-malli
Edellä käytettiin pääohjelman luokkaa käsittelijäluokkana. Tästä voi seurata helposti se väärinkäsitys, että mikäli start
-metodissa asetettaisiin joitakin olion attribuutteja, niin ne olisivat käytössä käsittelevässä metodissa. Näinhän ei voi olla, koska lataaja luo uuden käsittelijäolion ja silloin ajon aikana olisi kaksi HelloWorldMain
-luokan ilmentymää käytössä.
Tämän takia tyypillisessä .fxml
-tiedostoja käyttävässä JavaFX-ohjelmassa on selkeämpää tehdä käsittelijästä (controller) oma luokka. Tämä vastaa Model View Controller-mallia (MVC).
- Model: ohjelman käsittelemä data, joka ei riipu käyttöliittymästä. Lisätään omaan esimerkkiin
viesti
, joka vastaa karkeasti mallin Model-osaa - View: ohjelman käyttöliittymän näkyvä osa, jota voidaan muokata eri näköiseksi, mutta silti ohjelman toiminta säilyy samana. Esimerkissä
HelloWorldView.fxml
- Controller: ohjelman toiminnasta vastaava osa joka siirtää tietoa Modelin ja Viewn välillä. Esimerkissä
HelloWorldController
Nyt vastaavat tiedostot olisivat:
<BorderPane ... fx:controller="fxHelloCtrl.HelloWorldController">
Tässä ainut muuttunut rivi on se, että käytetään eri käsittelijäluokkaa (controller).
Periaatteessa kontrolleriluokassa on samat @FXML
-merkityt rivit kuin edellisessä esimerkissä. Tässä halutaan vielä lisäksi mahdollisuus tuoda pääohjelman puolelta tieto siitä, mikä viesti näytetään kun painiketta on painettu.
private String viesti = "Well Done!";
Ohjelman "model". Oletuksena on joku viesti, mitä voidaan tarvittaessa muuttaa.public void setViesti(String msg) {
Metodi, jolla kontrollerille voidaan tuoda sen käyttämä data.
FXMLLoader ldr = new FXMLLoader(...);
Tällä kertaa ei ladatakaan suoraan.fxml
-tiedoston sisältöä, koska latauksen lisäksi halutaan saada selville syntynyt kontrolleriluokan olio, jotta sen kanssa voidaan viestiä.final Pane root = (Pane)ldr.load();
Tässä suoritetaan varsinainen lataaminen ja luodaan siis samalla myös.fxml
-tiedostossa mainittu kontrolleriolio.final
-määre ei olisi pakollinen, mutta koskapane
-viitettä ei koskaan muuteta tässä metodissa, se voi ollafinal
.final HelloWorldController helloCtrl =
(HelloWorldController)ldr.getController();
Kysytään lataajalta (ldr
) minkä kontrolleriolion se loi.
helloCtrl.setViesti("Kiitti!");
Ilmoitetaan kontrollerioliolle mitä "modelia" pitää käyttää, eli mikä viesti näytetään kun painiketta painetaan.
1.6 Sitominen (bind)
Joissakin tilanteissa koodista saadaan yksinkertaisempaa kun liitetään (sidotaan) komponenttien ominaisuuksia muuttujiin ja käyttöliittymän ylläpitämiseksi riittää tällöin vain muuttujien arvojen ylläpitäminen. Pienenä "miinuksena" on että muuttujat voivat olla tyypiltään vain JavaFX:n omia tyyppejä.
Tämä ei vaikuta .fxml
-tiedostoon
Kontrolleriluokassa täytyy syntymisen yhteydessä tehdä tarvittava liitos:
public class HelloWorldController implements Initializable {
Luokan täytyy toteuttaaInitializable
-rajapinta, jotta lataaja voi kutsua seninitialize
-metodia kun kaikki elementit on luotu. Muodostajan aikanahan ei vielä ole tässä tapauksessa mitään valmista.ilmoitus.set(viesti);
Muutetaan JavaFX-tyyppistä muuttujaa, joka on sidottu labelin tekstiominaisuuteen. Tällöin myös labelin teksti muuttuu automaattisesti.private SimpleStringProperty ilmoitus =
new SimpleStringProperty("Hello World!")
Luodaan JavaFXn muuttuja, joka voidaan sitoa (bind) kontrollereiden ominaisuuksiin.public void initialize(...) {
Alustusmetodi jota lataaja kutsuu kun se on saanut muodostettua käyttöliittymän kaikki elementit.label.textProperty().bindBidirectional(ilmoitus);
Sidotaan kaksisuuntaisesti labelin tekstiominaisuusilmoitus
-muuttujaan. Mikäli jompaa kumpaa muutetaan, muuttuu toinen automaattisesti. Tosin labelin tapauksessa järkevämpi olisi yksisuuntainen sitominen (bind
), koska labelin arvoa harvemmin muutetaan muualta. Kaksisuuntainen sitominen olisi järkevää esimerkiksiTextField
-komponentin yhteydessä.
Koodista tuli hieman pidempi kuin aikaisemmin ja tässä tapauksessa hyöty jäikin kyseenalaiseksi. Hyötyä rupeaa tulemaan sitten, kun samaa sisältöä pitäisi muuttaa useammassa kohdassa, eli sama muuttuja voidaan sitoa muidenkin komponenttien ominaisuuksiin.
Myöskään päätiedostoa ei tarvitse muuttaa.
2. Autolaskuri
Seuraavaksi teemme sovelluksen jossa on hieman enemmän toimintaa. Eli sovelluksessa on kaksi painiketta, joita painamalla voidaan lisätä joko henkilöautojen tai kuorma-autojen määrää. Lisäksi on Nollaa-painike, jolla laskurit voidaan nollata.
Esimerkit voi ladata (Download) versionhallinasta osoitteesta:
2.1 Autolaskuri perustavalla
Tehdään ensin autolaskuri niin, että kullekin painikkeelle on oma käsittelijä ja apumuuttujat. Tässä tavassa uusien laskureiden lisääminen vaatii enemmän muutoksia.
Tehdään ensin tyylitiedosto, jossa kerrotaan miltä laskurit näyttävät.
Tehdään komponenttien sijoittelu tällä kertaa GridPane
-containerin päälle. Näin komponentit saadaan mukavasti linjaan. Viitataan samalla em. .css
-tiedostoon jolloin komponenteissa voidaan käyttää siinä esiteltyjä tyylejä.
<Label fx:id="laskuriKA" maxWidth="1E308" styleClass="laskuri"...
Sallitaan labelin levitä koko oman ruutunsa leveyteen ja pyydetään sitä käyttämään tyyliälaskuri
.
@FXML private Label laskuriHA;
Tehdään oma näyttö henkilöautoilleprivate int ha = 0;
Yksinkertaisuuden vuoksi tehdään oma laskurimuuttujalaskuriHA.setText("" + ++ha);
kasvatetaan henkilöautojen määrää ja kasvanut muuttuja muutetaan merkkijonoksi joka näytetään. Tämä voisi toki tehdä useamallakin rivillä. Miten?laskuriHA.setText("" + (ha=0));
Nollataan henkilöautojen määrä ja käytetään hyväksi sitä tietoa, että sijoitus palauttaa arvon, jolloin nolla voidaan samalla näyttää merkkijonona. Tämän voisi toki tehdä useamallakin rivillä. Miten?
Itse pääohjelma on taas aika tyypillinen. Vaikka .css
tiedosto ladataan tuossa, ei sitä olisi pakko tehdä, koska se on ladattu jo .fxml
-tiedoston yhteydessä.
tässä pitäis kai olla tyyppimuunnos final Pane root = (Pane) ldr.load();
VL: ei kai tarvii, koska Loader näyttäisi palauttavan nimenomaan Pane-viitteen. Mutta jos vasemmalle olisi esim BorderPane, niin silloin tarvittaisiin typecast. Mutta emme tarvitse pääohjelmassa panesta muita ominaisuuksia kuin mitä pn Pane luokassa, niin tämä on parempi kuin typecast.
2.2 Autolaskuri sitomalla
Tehdään sama käyttöliittymä käyttäen sidontaa. .css
, .fxml
ja pääohjelma ovat täsmälleen samanlaisia.
Kontrolleritiedostoon tehdään SimpleIntegerProperty
muuttujia ja niiden sitomiset laskureihin.
private SimpleIntegerProperty ha = ...
Luodaan tarkkailtava kokonaislukuolio.laskuriHA.textProperty().bind(ha.asString());
Pyydetäänha
-olioltaStringBinding
-tyyppinen olio, joka kuunteleeha
:n muutoksia ja kertoo niistä sittenlaskuriHA
:n tekstiominaisuudelle.ha.set(ha.get()+1);
ha.set(0);
Muutetaanha
-olion arvoa. Tällöin automaattisesti välitetään tieto kaikkialle, mihinha
on sidottu.
Eli nyt ohjelman tekeminen on sikäli helpompaa, että ei tarvitse sitomisen jälkeen enää miettiä miten tieto välitetään käyttöliittymälle.
Pääohjelma on täsmälleen sama kuin ennenkin.
2.3 Autolaskuri etsimällä laskurit
Seuraavassa versiossa ideana on se, että etsitään .fxml
:stä luodusta näkymästä kaikki laskuri
-tyylillä merkityt Label
-luokan komponentit ja liitetään niihin laskemisominaisuus. Samoin kaikille painikkeille annetaan sama käsittelijä ja painiketta painettaessa haetaan painikkeen id
ja lisätään laskuria, jolla on sama id
.
Tyylitiedosto on sama kuin ennenkin:
<Label id="HA" ... styleClass="laskuri"...>
Annetaan labeleille tunniste ja pidetään huoli että niiden css-luokkana onlaskuri
.<Button id="HA" ... onAction="#handleLaske"...>
Annetaan vastaaville painikkeille sama tunniste ja mennään kaikista painikkeista samaan käsittelijämetodiin. Metodissa otetaan selville tapahtuman aiheuttaneen elementin (source
) tunniste ja lisätään vastaavaa laskuria.<Button fx:id="buttonNollaa" ... onAction="#handleNollaa" ...>
Annetaan ainostaanbuttonNollaa
-painikkeelle tunniste, joka yhdistetään kontrolleriin. Kontrollerissa etsitään kaikki samaan containeriin lisätyt laskurit ja käytetään niitä.
Kontrolleriluokka kirjoitetaan lähes kokonaan uusiksi. Periaatteet ovat samat kuin aikaisemmassa esimerkissä, mutta nyt itse etsitään laskureita.
yritin importata fi.jyu.mit.fxgui.* mutta ohjelma ei siitä huolimatta tunnista getNodes
VL: Saattaa vaati että importtaa staattiseti, ks näytä koko koodi
fi.jyu.mit.fxgui.Functions.*
RE: Juu nyt toimii
—@FXML private Button buttonNollaa;
Käytetään ainostaanbuttonNollaa
-painiketta yhteisenä elementtinä.fxml
-tiedoston ja kontrollerin välillä.private ... laskettavat = new HashMap<>();
Tietorakenne, johon avaimen perusteella tallennetaan laskettaviaSimpleIntegerProperty
-muuttujia.laskurit = getNodes(buttonNollaa.getParent(),
Label.class, n -> n.getStyleClass().contains("laskuri"), true);
Etsitään listaan kaikki elementit, jotka ovat tyyppiäLabel
ja joiden ainakin yhtenä css-luokkana onlaskuri
.getNodes
on FXGui-kirjastossa.for (Label laskuri: laskurit) {
Käydään läpi kaikki löytyneet laskuritSimpleIntegerProperty laskettava = new ...;
Luodaan vastaava laskettava muuttuja.laskuri.textProperty().bind(laskettava.asString());
Liitetään samalla tavalla kuin edellisessä esimerkissä vastaavaan labeliin.laskettavat.put(laskuri.getId(),laskettava);
Lisätään luotu laskettava tietorakenteeseen laskuria vastaavalla avainsanalla, esim."HA"
.
@FXML void handleLaske(ActionEvent event) {
Kaikki lisäyspainikkeet tulevat samaan käsittelijään. Nyt käytetään hyväksi parametrina tulevaaActionEvent
-oliota.Node source = (Node)event.getSource();
Kysytään tapahtumaoliolta kuka aiheutti tapahtuman.String id = source.getId();
Otetaan tapahtuman aiheuttaneen (tässä esimerkissäButton
)id
. Mikäli ID:tä ei ole tai se on tyhjä, ei voida tehdä mitään järkevää.laskettava = laskettavat.get(id);
Pyydetään tietorakenteelta tätä avainta vastaava laskettava-olio. Mikäli sitä ei löydy, ei voida tehdä mitään.laskettava.set(laskettava.get()+1);
Kasvatetaan vastaavan olion arvoa. Toki voitaisiin periäSimpleIntegerProperty
ja tehdä siihen vaikkainc
-metodi, niin tämä kohta tulisi vielä siistimmäksi.
@FXML void handleNollaa() {
Muutetaan nollaamiskäsittelyä käymään läpi kaikki laskuritfor ( ... laskettava: laskettavat.values())
Pyydetään tietorakenteelta kaikki avaimien kohdalle tallennetut arvot (laskettava-viitteet) ja käydään ne yksitellen läpilaskettava.set(0);
joista kukin vuorollaan nollataan.
Vanha pääohjelma kelpaa jälleen sellaisenaan.
2.4 Lisää ylläpidettävyyttä
Periaatteessa käyttöliittymän ylläpitäjän työtä voitaisiin vielä vähentää jättämällä painikkeista tapahtumankäsittelijät pois ja lisäämällä ne initialize
-metodissa suoraan käsittelemään oikean laskettavan lisäämistä. Samalla voitaisiin lisätä labelille kuuntelija, jolloin sitäkin voi klikata. Esimerkissä on tehty peritty luokka Laskettava
jolloin lisääminen helpottuu. Samoin on tehty luokka Laskettavat
jotta Model
on selkeämmin näkyvillä.
public class AutolaskuriController implements Initializable {
@SuppressWarnings("javadoc")
public static class Laskettava extends SimpleIntegerProperty {
public Laskettava(int value) { super(value); }
public int inc() { set(get()+1); return get(); }
public int reset() { set(0); return get(); }
}
@SuppressWarnings("javadoc")
public static class Laskettavat {
private List<Laskettava> alkiot = new ArrayList<>();
public void add(Laskettava alkio) { alkiot.add(alkio); }
public void reset() { alkiot.forEach(l -> l.reset()); }
}
@FXML private Button buttonNollaa;
private Laskettavat laskettavat = new Laskettavat();
@Override
public void initialize(URL location, ResourceBundle resources) {
Node parent = buttonNollaa.getParent();
List<Label> laskurit = getNodes(parent, Label.class,
n -> n.getStyleClass().contains("laskuri"), true);
for (Label laskuri: laskurit) {
String id = laskuri.getId();
if ( id == null || id.length() < 1 ) continue;
Laskettava laskettava = new Laskettava(0);
laskuri.textProperty().bind(laskettava.asString());
laskettavat.add(laskettava);
laskuri.setOnMouseClicked(e -> laskettava.inc());
laskuri.setOnTouchPressed(e -> laskettava.inc());
Button button = getNode(parent, Button.class, n -> id.equals(n.getId()), true);
if ( button != null ) button.setOnAction(e -> laskettava.inc());
}
}
@FXML void handleNollaa() { laskettavat.reset(); }
}
public void reset() { alkiot.forEach(l -> l.reset()); }
Tässä on malliksi käytetty Java 8:n mukana tulluttaforEach
-algoritmia jolle viedään parametrina se, mitä tehdään kullekin rakenteen alkiolle.
Edellä on ehkä menty jo hieman liian pitkälle, sillä jos jätetään pois metodi handleLaske
, ei laskemista voida laittaa tulemaan esimerkiksi menuista yms. Vaihtoehtoja olisi jättää metodi käytettäväksi tai sitten laajentaa alustussilmukkaa niin, että se etsii kaikkia komponentteja joilla on sama id
kuin itse laskurin kohteella. Tähän pienen haasteen asettaa se, että eri komponenteilla on erilaisia tapahtumankäsittelijöitä.
Sitomisten ansiosta voitaisiin laskemista näyttää muissakin komponenteissa. Esimerkiksi mikäli olisi lisätty ProgressBar
-komponentteja, voitaisiin nekin ottaa mukaan alustussilmukassa tyyliin:
ProgressBar bar = getNode(parent, ProgressBar.class, n -> id.equals(n.getId()), true);
if ( bar != null ) bar.progressProperty().bind(laskettava.divide(20.0));
laskettava.divide(20.0)
Luo sitomisen, joka on1/20
laskettavan arvosta. KoskaProgressBar
-komponentin maksimi on 1.0, tulee se täyteen kun laskettavan arvo on 20.
2.5 Omat komponentit
Oletetaan että jonkun pitäisi tehdä paljon autolaskureita. Edellä tehtyjen muutostenkin jälkeen jokaisen laskurin osalta pitää vielä tehdä jonkin verran työtä.
Seuraava askel olisi tehdä oma komponentti laskurin ja painikkeen yhdistelmästä.
miksi package sisältää PolkupyoratView.fxml, johon ei näytetä viittaavan?
VL: Saattaa liittyä johonkin demotehtävään?
—Käytetään vanhaa .css
-tiedostoa:
Välttämättä emme tarvitsisi komponentille omaa .fxml
-tiedostoa, koska siinä on niin vähän osia, että se olisi aika helppo tehdä suoraan Javallakin. Kuitenkin esimerkkinä monimutkaisempia komponentteja varten esitetään yhden komponentin ulkoasu .fxml
-tiedostona:
Periaatteessa .fxml
-tiedostossa on aikaisemman esimerkin yhden laskurin ja painikkeen osalta oleva koodi. Nyt kontrolleria ei tarvitse kertoa (eikä saakkaan), kun kontrolleri kerrotaan ennen latausta .java
-tiedostossa.
Siirretään aikaisemmasta esimerkistä suuri osa koodista yhtä laskuria käsitteleväksi luokaksi. Luokan tehtävänä on ladata .fxml
-tiedosto, mutta koska kontrolleriksi pitää saada lataava luokka, on lataus hieman erilainen kuin aikaisemmissa pääohjelmissa.
loader.setController(this);
Tämän ansiosta kontrolleriluokkaa ei mainita.fxml
-tiedostossa.
public String getCaption()
public void setCaption(String caption)
Tätä kautta julistetaan painikkeen teksti näkyväksi komponentin ulkopuolelle ja se voidaan asettaa ja lukeacaption
-ominaisuuden kautta.fxml
-tiedostossa.
Komponettien ansiosta .fxml
-tiedosto selkeytyy jonkin verran ja itse laskurin takia tulee nyt todella vähän koodia.
Jotta komponenttia voisi käyttää mm. SceneBuilderistä
, pitää tiedostot
- autolaskuri.css
- LaskuriView.fxml
- Laskuri.java
pakata Eclipsen File/Export.../JAR file
-toiminnolla yhdeksi laskuri.jar tiedostoksi joka luetaan sisään SceneBuidleriin
. Nyt autolaskureita voi tehdä lähes pelkästään muokkaamalla koodia SceneBuilderissä.
Miten laskuri.jar luetaan sisään SceneBuilderiin?
VL: tästä on scenebuilder-sivulla. Samalla tavalla kuin FXGui.jar.
Mikäli GridPanel
-containerin sijaan käytettäisiin vaikkapa HBoxia
, saataisiin ohjelma sellaiseksi että uuden laskurin takia tulee koko ohjelmaan vain yksi rivi lisää. Tai SceneBuilderiä käytettäessä uuden laskurin lisääminen on raahata se HBox
in sisään ja vaihtaa caption
.
Nyt ohjelman kontrolleri lyhenee huomattavasti.
Itse asiassa koko kontrollerista voitaisiin luopua, mikäli tehtäisiin oma komponentti NollaaButton
, joka etsisi oman vanhempansa alueelta kaikki Laskuri
-komponentit ja nollaisi ne. Tällöin autolaskureita voitaisiin tehdä pelkästään luomalla .fxml
-tiedosto ja lisäämällä sinne sopivasti laskureita ja nollaa-painikkeita. Se minkälaisten containereiden päälle komponenttaja laitettaisiin, määräisi mikä nollaa mitäkin.
Pääohjelma on jälleen samanlainen kuin ennenkin.
3. Tyylit
JavaFX:ssä voidaan tyylitiedostojen avulla hallita sovelluksen ulkoasua värityksen yms osalta. Eclipsessä muista että projektissa on mukana JavaFX, jotta voit muokata Eclipsellä näitä .css
-tiedostoja.
Katso myös:
- Autolaskuri-esimerkki edellä
- JavaFX:än tyylit Oraclen sivuilta
- JavaFX tyylien käyttö
Periaatteessa riittää kirjoittaa tyylit esimerkiksi tiedostoon huone.css
:
.harmaa {
-fx-background-color: #EEE;
}
Sitten "pääohjelmassa" ladataan tyylitiedosto:
@Override
public void start(Stage primaryStage) {
try {
...
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("huone.css").toExternalForm());
Jokaisessa komponentissa, jossa halutaan käyttää tuota taustaväriä, kirjoitetaan .fxml
-tiedostoon:
<TextField fx:id="textTilavuus" onAction="#onEnter" styleClass="harmaa" ...
tai valitaan SceneBuilderissä
vastaava tyyli komponentin Properties/Style Class
-kohdasta.
Näin meneteltynä tyylit toimivat kun ohjelma ajetaan, mutta SceneBuiderissä
ne eivät näy suunnitteluaikana. Mikäli halutaan näkymään myös suunnitteluaikana, niin tyylitiedoston voi lisätä vaikkapa SceneBuilderissä
juurikomponenttiin (usein esim. BorderPane
) panen Properties/Style Sheets
-kohdasta. Silloin ei tarvitse tosin edes pääohjelmassa ladata tyylitiedostoa.
<BorderPane stylesheets="@huone.css" ...
Mikäli halutaan vaihtaa tyylejä ajon aikana johonkin komponenttiin, voi tämän tehdä esimerkiksi:
...
labelVirhe.getStyleClass().add("virhe");
...
labelVirhe.getStyleClass().removeAll("virhe");
3.1 Vinkkejä
TableView
-komponentin ylimääräisistä soluista väri ja rajat pois:
.table-row-cell:empty {
-fx-background-color: white;
}
.table-row-cell:empty .table-cell {
-fx-border-width: 0px;
}
3.2 Tyylit ModalController-luokan kautta kutsutuissa näkymissä
Jos tyylitiedoston tyylejä haluaa käyttää ModalController-luokan kautta kutsutussa, ulkoisessa näkymässä, tyylitiedosto tulee lisätä erikseen kyseisen näkymän fxml-dokumenttiin. Main-luokan pääohjelman käynnistyksessä tyylitiedosto kiinnitetään vain sovelluksen päänäkymään.
Tyylitiedoston voi lisätä SceneBuilderissa klikkaamalla valituksi hierarkiassa ylin container eli elementti, joka pitää sisällään muut elementit. Sen jälkeen valitun containerin ominaisuuksista voi asettaa halutun tyylitiedoston (properties -> stylesheets).
Tyylitiedoston voi lisätä myös suoraan fxml-dokumenttiin haluttuun container elementtiin, kuten edellä.
4. Virhemahdollisuuksia
Jos ongelmia ilmenee, niin laita korvan taakse seuraavat tyypilliset mokat…
4.1 Unohdit tallentaa tiedoston
- yksi yleisimmin tapahtuvista virheistä tuntuu olevan että unohtaa tallentaa tiedoston SceneBuilderissä ja sitten tuntuu että muutokset eivät vaikuta Eclipsessä.
- tarkista SceneBuilderissä että tallennusilmoitus näkyy
- Eclipsessä tee Refresh projektille (
F5
) - joskus käy myös niin, että Eclipsessä tehty muutos ei näy SceneBuilderissä, silloin sulje SceneBuilder ja käynnistä uudelleen.
4.2 Loit JavaFX-projektin sijaan Java-projektin
Jos vahingossa tekee Java-projektin, kun pitäisi olla JavaFX projekti, niin ei hätää, tämän voi korjata:
- Klikkaa hiiren oikealla napilla projektin nimen päällä
Properties/Java Build Path/Libraries
- Vaihda se oma JavaFX:ää varten tehty JRE tilalle.
Finish
4.3 Komponentin nimi kirjoitettu väärin
Exception in thread "JavaFX Application Thread" java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
at javafx.fxml.FXMLLoader$MethodHandler.invoke(Unknown Source)
at javafx.fxml.FXMLLoader$ControllerMethodEventHandler.handle(Unknown Source)
...
at javafx.event.Event.fireEvent(Unknown Source)
at javafx.scene.Node.fireEvent(Unknown Source)
at javafx.scene.control.Button.fire(Unknown Source)
...
Caused by: java.lang.reflect.InvocationTargetException
...
Caused by: java.lang.NullPointerException
at hello.ctrl.HelloWorldController.handlePressed(HelloWorldController.java:15)
... 59 more
Tällainen virheilmoitus kun HelloWorldMain
-ohjelmassa painetaan painiketta ja tiedostoihin on kirjoitettu
.fxml: <Label fx:id="labeli" ...>
.java: @FXML private Label label;
eli komponentin nimi on kirjoitettu eri tavalla eri tiedostoissa.
Mikäli .fxml
-tiedosto on auki Eclipsessä, näkyy sellaiset fx:id
attribuutit varoituksena, joille ei löydy kontrolleriluokasta vastaavaa @FXML
-viitemuuttujaa.
Mutta jos kontrolleriluokassa on @FXML
-esitelty viitemuuttuja, jolle ei ole vastaavaa fx:id
-attribuuttia, tästä ei tule mitään virhettä käännösaikana vaan vasta kaatuminen ajon aikana.
4.4 NullPointerException
Täsmälleen vastaava virheilmoitus kuin edellä tulee, mikäli Java-tiedostossa on:
@FXML private Label labelVirhe;
mutta tuota labelVirhe ei ole laitettu minkään FXML-tiedoston komponentin id:ksi. Eli jos koodissa on tuo @FXML...
, niin fxml-tiedostossa on oltava rivi tyyliin:
<Label fx:id="labelVirhe" maxWidth="1000.0" />
eli jonkun komponentin (joka on samaa tyyppiä) fx:id
:n on oltava tuo. Muuten em. tapauksessa tuota oliota labelVirhe
ei luo kukaan ja sitten esim. rivi
labelVirhe.setText("Kissa")
johtaa ilman muuta null-pointteriin.
Toki JavaFX ongelmien lisäksi on lukemattomia muita syitä NullPointerException
-poikkeukseen ja tuo siksi tuo virheilmoituksen stack trace
kannattaa lukea huolella läpi ja katsoa mistä virhe on kotoisin. Tarvittaessa laitetaan breakpoint sopiville virheilmoituksessa oleville riveille ja toistetaan ohjelman ajo ja sitten yritetään seurata mikä on null
ja milloinkin.
4.5 location
java.lang.NullPointerException: Location is required/Location is not set.
at javafx.fxml.FXMLLoader.loadImpl(FXMLLoader.java:3207)
Jos .fxml
tiedoston nimi on kirjoitettu väärin, niin virheilmoituksesta löytyy yleensä joku location error. Nimi esiintyy mm. Main
-tiedostossa.
4.6 Puuttuva tai väärin kirjoitettu käsittelijän nimi
javafx.fxml.LoadException: Error resolving onAction='#handleHA', either the event
handler is not in the Namespace or there is an error in the script.
/E:/kurssit/ohj2/FXExamples/Examples/bin/autolaskuri/loop/AutolaskuriView.fxml:31
Mikäli .fxml
-tiedostossa mainittua käsittelijän nimeä ei löydy täsmälleen samanlaisena kontrolleriluokasta, saadaan yllämainitun kaltainen virheilmoitus.
Tämä on siitä "helpoin" virhe, että mikäli .fxml
-tiedosto on auki Eclipsessä, virhe näkyy sielläkin.
4.7 Class no found
ClassNotFoundException
syntyi myös mystisesti vaikka kaikki nuo oli oikein, syynä oli että projektin classpathissa oli JRE kahdesti
javafx.fxml.LoadException:
/E:/kurssit/ohj2/FXExamples/Examples/bin/hello/ctrl/HelloWorldView.fxml:8
at javafx.fxml.FXMLLoader.constructLoadException(Unknown Source)
...
Caused by: java.lang.ClassNotFoundException: hello.ctrl.H1elloWorldController
at java.net.URLClassLoader.findClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
at java.lang.ClassLoader.loadClass(Unknown Source)
... 18 more
tällaisia classnotfoundeja sun muita saattaa tulla myös, jos controlleriin ei ole asetettu mitään. Esim. listchooserin lisättyäni heitti tämänkaltaista erroria. Scenebuilderilla kun laitoin code-osasta jonkinlaisen toiminnon tälle listchooserille ja lisäsin sen controlleriin (kts. scenebuilderin käyttö, 12.4: SceneBuilderilla piirretyn käyttöliittymän kontrolleri) ohjelma alkoi taas toimimaan. Eclipse ei ilmeisesti tykkää, jos fxml.tiedostoissa on käyttämättömiä asioita.
—java.lang.ClassNotFoundException tulee jatkuvasti. Kaikki edellä mainitut ratkaisut kokeiltu eikä auta.
VL: Mahdotonta vastata näillä tiedoilla :-) Pääteohjaukseen ja näyttää ongelmaa.
—tarkoittaa sitä että .fxml
-tiedostossa on kontrolleriluokan nimi tai paketti kirjoitettu väärin.
4.8 Väärä import
Ole todella huolellinen kun lisään import
-lauseita. Sieltä ehdotetaan hyvin helposti paketteja tyyliin java.awt
kun pitäisi olla esim javafx.scene.control.JOTAKIN
.
4.9 Gluon
Gluon on kirjasto erityisesti mobiilisovellusten tekemiseen. Jos satut käyttämään SceneBuilderin kautta noita Gluon komponentteja, pitää sinun lisätä jar-hakemistoon ja joko ohj2 ja johonkin omaan toiseen kirjastoon vastaavia jar-tiedostoja.
Niitä löydät osoitteesta:
Se mitä tarvitset, selviää virheilmoituksista. Eli lue huolella Eclipsessä tai Riderissä saamaasi virheilmoitus. Sieltä voi tulla pitkän rimpsun sekaan rivejä tyyliin :
...
Caused by: java.lang.ClassNotFoundException: com.gluonhq.attach.util.Platform
...
Tällöin lataat hakemistosta (huom, versionumerot voivat muuttua, katso uusimpia)
tiedoston: util-4.0.6.jar
. Vastaavasti jos tulee:
Caused by: java.lang.ClassNotFoundException: com.gluonhq.charm.glisten.control.CardPane
niin lataat hakemistosta (ks gluonhq
eteenpäin olevat pakettinimet):
tiedoston charm-glisten-6.2.3.jar
.
Kurssilla ei ole ohjeita gluon-komponenttien käytöstä, joten kannattaa harkita tulisiko toimeen ilman niitä. Esimerkiksi Idean sisältä käytetyssä SceneBuilderissä niitä ei oletuksena ole edes tarjolla.
Jos kuitenkin haluat niiden kanssa työskennellä, niin dokumentaatio löytyy osoitteesta:
4.10 Muita
Sitten tuli vastaan tilanne että JavaFX-ohjelmaa ajettaessa virheilmoitukset olivat tyyliin "class not found ListView" jne.
Tähän tuntui auttavan Java JDK:n uusimman version asentaminen ja bootti.
Ei tarkoita sitä, että joka virheen tapauksessa Java JDK pitäisi asentaa uudelleen, mutta tässä tapauksessa ohjelma toimi esim mikroluokkien koneilla, mutta ei käyttäjän koneella.
Kysymys:
Kysymys: Mitäköhän tällaiset FXMLLoader-virheet tarkoittavat
javafx.fxml.LoadException:
/C:/Users/x/Documents/demo3graafinen/bin/matkaFX/MatkaView.fxml:10
at javafx.fxml/javafx.fxml.FXMLLoader.constructLoadException(Unknown Source)
at javafx.fxml/javafx.fxml.FXMLLoader.access$700(Unknown Source)
at javafx.fxml/javafx.fxml.FXMLLoader$ValueElement.processAttribute(Unknown Source)
at javafx.fxml/javafx.fxml.FXMLLoader$InstanceDeclarationElement.processAttribute(Unknown Source)
at javafx.fxml/javafx.fxml.FXMLLoader$Element.processStartElement(Unknown Source)
at javafx.fxml/javafx.fxml.FXMLLoader$ValueElement.processStartElement(Unknown Source)
at javafx.fxml/javafx.fxml.FXMLLoader.processStartElement(Unknown Source)
at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
at javafx.fxml/javafx.fxml.FXMLLoader.loadImpl(Unknown Source)
at javafx.fxml/javafx.fxml.FXMLLoader.load(Unknown Source)
at matkaFX.MatkaMain.start(MatkaMain.java:19)
at javafx.graphics/com.sun.javafx.application.LauncherImpl.lambda$launchApplication1$9(Unknown Source)
at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runAndWait$11(Unknown Source)
at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$9(Unknown Source)
at java.base/java.security.AccessController.doPrivileged(Native Method)
at javafx.graphics/com.sun.javafx.application.PlatformImpl.lambda$runLater$10(Unknown Source)
at javafx.graphics/com.sun.glass.ui.InvokeLaterDispatcher$Future.run(Unknown Source)
at javafx.graphics/com.sun.glass.ui.win.WinApplication._runLoop(Native Method)
at javafx.graphics/com.sun.glass.ui.win.WinApplication.lambda$runLoop$3(Unknown Source)
at java.base/java.lang.Thread.run(Unknown Source)
Caused by: java.lang.ClassNotFoundException: muuttujat.matkaFX.MatkaController
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(Unknown Source)
at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(Unknown Source)
at java.base/java.lang.ClassLoader.loadClass(Unknown Source)
... 23 more
Vastaus: Vaikuttaa että ei ole tiedostoa (luokkaa) muuttujat.matkaFX.MatkaController
. Eli pitää katsoa MatkaView.fxml
tiedoston riviltä 10 että mihin tiedostoon siellä viitataan. Onko sillä eri nimi kuin mihin viitataan tai eri paketti, tai sitten koko tiedosto puuttuu. Kannattaa tehdä nuo JavaFX projektit sillä FXMLPackage.jar avulla, niin ei tule vääriä nimeämisiä.
Ja sitten kannattaa projekti ajaa aina ennen kuin tekee yhtään mitään, niin näkee että toimiko alun alkaenkaan ja sitten jos toimii ja lakkaa toimimasta, niin tietää että itse teki virheen.
5. Lisätietoa: Käyttöliittymän esteettömyyden parantaminen
Käyttöliittymä on esteetön silloin, kun mahdollisimman monet käyttäjäryhmät voivat käyttää sitä itsenäisesti. Esimerkiksi sokeat käyttävät tietokonetta ruudunlukuohjelmalla, jolla näytöllä oleva teksti tulostuu puheeksi ja/tai pistekirjoitukseksi. Tässä luvussa esitellään eräs hyvin olennainen käyttöliittymän esteettömyyteen vaikuttava seikka, nimittäin labelien yhdistäminen tekstikenttiin.
5.1 Taustaa
Graafisen käyttöliittymän tekstikenttien välillä liikutaan näppäimistöä käyttäen sarkaimella. Fokuksen ollessa tekstikentässä ruudunlukija ilmoittaa kyseessä olevan tekstikenttä, mutta jos kentän labelia ei ole yhdistetty sille kuuluvaan kenttään, labelin sisältöä ei lueta. Tällöin ei voi tietää, mitä varten kyseinen kenttä on; onko se kenties haku vai jonkin tiedon syöttämistä varten.
5.2 Labelien yhdistäminen javaFX:ssä
JavaFX:ssä labelin voi yhdistää tekstikenttään fxml-tiedostossa seuraavasti:
<Label text="Anna nimesi" GridPane.rowIndex="1" GridPane.columnIndex="1">
<labelFor>
<TextField fx:id="nimi" GridPane.rowIndex="1" GridPane.columnIndex="2"/>
</labelFor>
</Label>
<fx:reference source="nimi"/>
6. Tiedon visualisointi
Sama datan avulla tehtynä
These are the current permissions for this document; please modify if needed. You can always modify these permissions from the manage page.