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

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.

Ohjelma ajettuna työasemassa
Ohjelma ajettuna työasemassa
# HelloWorld

HelloWorld.java

package hello.simple;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;

/**
 * Yksinkertainen esimerkki JavaFX ohjelmasta
 * @author vesal
 * @version 4.3.2016
 */
public class HelloWorld extends Application {
    @Override
    public void start(Stage primaryStage) {
        BorderPane root = new BorderPane();
        Label label = new Label("Hello World!");
        root.setCenter(label);
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }


    /** @param args  ei käytössä  */
    public static void main(String[] args) {
        launch(args);
    }
}

 

  • public class HelloWorld extends Application {
    peritään yleinen JavaFX sovellusluokka Application ja korvataan sen start-metodi omalla
  • launch(args);
    pääohjelmasta käynnistetään varsinainen JavaFX sovellus jolle viedään parametrina ohjelman käynnistykseen tulleet argumentit
  • public void start(Stage primaryStage) {
    tehdään oma versio start-metodista, jonka pitää luoda varsinainen näkyvä sisältö.
  • BorderPane root = new BorderPane();
    luodaan pohjapaneli (ks. Containers), jonka päälle laitetaan näytettäviä komponentteja
  • Label label = new Label("Hello World!");
    esimerkin vuoksi luodaan yksi tekstiä näyttävä komponentti (ks. Components)
  • root.setCenter(label);
    joka lisätään pohjapanelin keskelle
  • Scene scene = new Scene(root);
    luodaan "näytös", johon liitetään näytettävät panelit
  • primaryStage.setScene(scene);
    ja tämä lisätään parametrina tulleen stagen (näyttämö) päälle. Usein stage 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-hakemistossa Main.java nimeksi HelloWorld.java
  • korvaa koodi edellä olevalla koodilla
  • F11

Toki koodia voitaisiin vielä hieman lyhentää ketjuttamalla kutsuja:

# HelloWorld2

HelloWorld2.java

//
    public void start(Stage primaryStage) {
        BorderPane root = new BorderPane();
        root.setCenter(new Label("Hello World!"));
        primaryStage.setScene(new Scene(root));
        primaryStage.show();
    }

 

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.

# HelloWorldView2

HelloWorldView.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.BorderPane?>


<BorderPane xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8.0.65">
   <center>
      <Label text="Hello World!" BorderPane.alignment="CENTER" />
   </center>
</BorderPane>

 

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:

# HelloWorldMain

HelloWorldMain.java

//
    public void start(Stage primaryStage) {
        try {
            Pane root = FXMLLoader.load(getClass().getResource("HelloWorldView.fxml"));
            Scene scene = new Scene(root);
            primaryStage.setScene(scene);
            primaryStage.show();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

 

  • Pane root = FXMLLoader.load(getClass().getResource("HelloWorldView.fxml"));
    Tässä ohjelman luokan paketin (package) muodostamasta resurssista ladataan mainittu .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.

Ohjelma ajettuna työasemassa ja painiketta painettu
Ohjelma ajettuna työasemassa ja painiketta painettu

HUOM! TIM-ympäristöstä ajettuna ei voi painaa painiketta, eli esimerkki kannattaa ladata omaan IDEen (esim. Eclipse).

# HelloWorldButton

HelloWorld.java

//
    public void start(Stage primaryStage) {
        BorderPane root = new BorderPane();
        Label label = new Label("Hello World!");
        Button button = new Button("Press me!");
        BorderPane.setMargin(button, new Insets(10));
        button.setOnAction( e -> label.setText("Well Done!"));
        root.setCenter(label);
        root.setRight(button);
        Scene scene = new Scene(root);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

 

  • 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ä vaihdetaan label-komponentin teksti toiseksi. Itse tapahtumaparametria e 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.

# HelloWorldViewButton2

HelloWorldView.fxml

<BorderPane xmlns="http://javafx.com/javafx/8.0.65"
 xmlns:fx="http://javafx.com/fxml/1"
 fx:controller="hello.button.HelloWorldMain">
   <center>
      <Label fx:id="label" text="Hello World!" BorderPane.alignment="CENTER" />
   </center>
   <right>
      <Button mnemonicParsing="false" onAction="#handlePressed" text="Press me!"
       BorderPane.alignment="CENTER">
         <BorderPane.margin>
            <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
         </BorderPane.margin>
      </Button>
   </right>
</BorderPane>

 

  • <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!"...>
    Mainittu Label-komponentin liitos Java-tiedoston muuttujaan label 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 nimi label pitää olla samalla tavalla kirjoitettuna molemmissa tiedostoissa. Toki nimi voi olla mikä tahansa, kunhan se on sama molemmissa. Kahdelle elementille ei voi antaa samaa fx: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 kuin fx: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):

# HelloWorldMainButton

HelloWorldMain.java

//
    @FXML private Label label;

    @FXML private void handlePressed() {
        label.setText("Well Done!");
    }

 

  • @FXML private Label label;
    Tässä @FXML tarkoittaa että lataaja etsii näitä rivejä ja mikäli vastaava fx: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 kutsutaan handlePressed-nimistä metodia. Metodilla voisi tässä tapauksessa olla myös ActionEvent-tyyppinen parametri, mutta tässä esimerkissä sitä ei tarvita.
# controller

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:

# HelloWorldViewCtrl2

HelloWorldView.fxml

<BorderPane xmlns="http://javafx.com/javafx/8.0.65"
 xmlns:fx="http://javafx.com/fxml/1"
 fx:controller="hello.ctrl.HelloWorldController">
   <center>
      <Label fx:id="label" text="Hello World!" BorderPane.alignment="CENTER" />
   </center>
   <right>
      <Button mnemonicParsing="false" onAction="#handlePressed" text="Press me!"
       BorderPane.alignment="CENTER">
         <BorderPane.margin>
            <Insets bottom="10.0" left="10.0" right="10.0" top="10.0" />
         </BorderPane.margin>
      </Button>
   </right>
</BorderPane>

 

  • <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.

# HelloWorldControllerCtrl

HelloWorldController.java

public class HelloWorldController  {

    @FXML private Label label;

    @FXML private void handlePressed() {
        label.setText(viesti);
    }

    private String viesti = "Well Done!";

    /**
     * @param msg mikä viesti tulee painikkeen painamisesta
     */
    public void setViesti(String msg) {
       viesti = msg;
    }
}

 

  • 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.
# HelloWorldMainCtrl

HelloWorldMain.java

//
    public void start(Stage primaryStage) {
        try {
            FXMLLoader ldr =
              new FXMLLoader(getClass().getResource("HelloWorldView.fxml"));
            final Pane root = (Pane)ldr.load();
            final HelloWorldController helloCtrl =
              (HelloWorldController)ldr.getController();
            helloCtrl.setViesti("Kiitti!");
            Scene scene = new Scene(root);
            primaryStage.setScene(scene);
            primaryStage.show();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

 

  • 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 koska pane-viitettä ei koskaan muuteta tässä metodissa, se voi olla final.
  • 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

# HelloWorldViewBind2

HelloWorldView.fxml

//
      <Label fx:id="label" text="Hello World!" BorderPane.alignment="CENTER" />

 

Kontrolleriluokassa täytyy syntymisen yhteydessä tehdä tarvittava liitos:

# HelloWorldControllerBind

HelloWorldController.java

public class HelloWorldController implements Initializable {
    @FXML private Label label;

    @FXML private void handlePressed() {
        ilmoitus.set(viesti);
    }


    private SimpleStringProperty ilmoitus = new SimpleStringProperty("Hello World!");
    private String viesti = "Well Done!";


    @Override
    public void initialize(URL location, ResourceBundle resources) {
        label.textProperty().bindBidirectional(ilmoitus);
    }


    /**
     * @param msg mikä viesti tulee painikkeen painamisesta
     */
    public void setViesti(String msg) {
       viesti = msg;
    }
}

 

  • public class HelloWorldController implements Initializable {
    Luokan täytyy toteuttaa Initializable-rajapinta, jotta lataaja voi kutsua sen initialize-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 tekstiominaisuus ilmoitus-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ää esimerkiksi TextField-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.

# HelloWorldMainBind

HelloWorldMain.java

//
            FXMLLoader ldr =
              new FXMLLoader(getClass().getResource("HelloWorldView.fxml"));

 

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:

Ohjelma ajettuna työasemassa
Ohjelma ajettuna työasemassa

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.

# autolaskuricss

autolaskuri.css

.laskuri {
    -fx-font-size: 30px;
    -fx-background-color: cyan;
    -fx-background-radius: 10;
}

 

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ä.

# autolaskuriviewbasic

AutolaskuriView.fxml

<GridPane hgap="10.0" maxHeight="-Infinity" maxWidth="-Infinity"
   minHeight="-Infinity" minWidth="-Infinity"
   stylesheets="@autolaskuri.css" vgap="20.0"
   xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8.0.65"
   fx:controller="autolaskuri.basic.AutolaskuriController">
  <columnConstraints>
    <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="120.0" />
    <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="120.0" />
  </columnConstraints>
  <rowConstraints>
    <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
    <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
    <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
  </rowConstraints>
   <children>
      <Label fx:id="laskuriHA" alignment="CENTER_RIGHT"
         maxWidth="1E308" styleClass="laskuri" text="0">
         <padding>
            <Insets right="10.0" />
         </padding>
      </Label>
      <Label fx:id="laskuriKA" alignment="CENTER_RIGHT"
        maxWidth="1E308" styleClass="laskuri" text="0" GridPane.columnIndex="1">
         <padding>
            <Insets right="10.0" />
         </padding>
      </Label>
      <Button alignment="CENTER" maxWidth="1E308" mnemonicParsing="false"
         onAction="#handleHA" text="Henkilöautoja" GridPane.rowIndex="1" />
      <Button alignment="CENTER" maxWidth="1E308" mnemonicParsing="false"
        onAction="#handleKA" text="Kuorma-autoja" GridPane.columnIndex="1"
        GridPane.rowIndex="1" />
      <Button alignment="CENTER" maxWidth="1E308" mnemonicParsing="false"
        onAction="#handleNollaa" text="Nollaa" GridPane.columnSpan="2"
        GridPane.rowIndex="2">
         <GridPane.margin>
            <Insets left="30.0" right="30.0" />
         </GridPane.margin>
      </Button>
   </children>
   <padding>
      <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
   </padding>
</GridPane>

 

  • <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.
# AutolaskuriControllerBasic

AutolaskuriController.java

public class AutolaskuriController {
    @FXML private Label laskuriHA;
    @FXML private Label laskuriKA;
    private int ha = 0;
    private int ka = 0;

    @FXML void handleHA() {
        laskuriHA.setText("" + ++ha);
    }

    @FXML void handleKA() {
        laskuriKA.setText("" + ++ka);
    }

    @FXML void handleNollaa() {
        laskuriHA.setText("" + (ha=0));
        laskuriKA.setText("" + (ka=0));
    }
}

 

  • @FXML private Label laskuriHA;
    Tehdään oma näyttö henkilöautoille
  • private int ha = 0;
    Yksinkertaisuuden vuoksi tehdään oma laskurimuuttuja
  • laskuriHA.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ä.

# AutolaskuriMainBasic

AutolaskuriMain.java

//
    public void start(Stage primaryStage) {
        try {
            FXMLLoader ldr = new FXMLLoader(getClass().getResource("AutolaskuriView.fxml"));
            final Pane root = ldr.load();
            //final AutolaskuriController autolaskuriCtrl = (AutolaskuriController) ldr.getController();
            Scene scene = new Scene(root);
            scene.getStylesheets().add(getClass().getResource("autolaskuri.css").toExternalForm());
            primaryStage.setScene(scene);
            primaryStage.setTitle("Autolaskuri");
            primaryStage.show();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

 

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.

12 Mar 18 (edited 27 Feb 22)
# polkupyoratBasic

Tehtävä: Lisää polkupyörien laskeminen

Lisää edelliseen ohjelmaan myös polkupyörien laskeminen. Kerro mitä piti muuttaa.

 

# varitBasic

Tehtävä: Värit ja pyöristykset

Muuta edellä laskureiden taustaväri toisenlaiseksi. Muuta laskureiden nurkkien pyöristystä.

 

2.2 Autolaskuri sitomalla

Tehdään sama käyttöliittymä käyttäen sidontaa. .css, .fxml ja pääohjelma ovat täsmälleen samanlaisia.

# autolaskuricssbind

autolaskuri.css

.laskuri {
    -fx-font-size: 30px;
    -fx-background-color: cyan;
    -fx-background-radius: 10;
}

 

# autolaskuriviewbind

AutolaskuriView.fxml

<GridPane hgap="10.0" maxHeight="-Infinity" maxWidth="-Infinity"
   minHeight="-Infinity" minWidth="-Infinity"
   stylesheets="@autolaskuri.css" vgap="20.0"
   xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/8.0.65"
   fx:controller="autolaskuri.basic.AutolaskuriController">
  <columnConstraints>
    <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="120.0" />
    <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="120.0" />
  </columnConstraints>
  <rowConstraints>
    <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
    <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
    <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
  </rowConstraints>
   <children>
      <Label fx:id="laskuriHA" alignment="CENTER_RIGHT"
         maxWidth="1E308" styleClass="laskuri" text="0">
         <padding>
            <Insets right="10.0" />
         </padding>
      </Label>
      <Label fx:id="laskuriKA" alignment="CENTER_RIGHT"
        maxWidth="1E308" styleClass="laskuri" text="0" GridPane.columnIndex="1">
         <padding>
            <Insets right="10.0" />
         </padding>
      </Label>
      <Button alignment="CENTER" maxWidth="1E308" mnemonicParsing="false"
         onAction="#handleHA" text="Henkilöautoja" GridPane.rowIndex="1" />
      <Button alignment="CENTER" maxWidth="1E308" mnemonicParsing="false"
        onAction="#handleKA" text="Kuorma-autoja" GridPane.columnIndex="1"
        GridPane.rowIndex="1" />
      <Button alignment="CENTER" maxWidth="1E308" mnemonicParsing="false"
        onAction="#handleNollaa" text="Nollaa" GridPane.columnSpan="2"
        GridPane.rowIndex="2">
         <GridPane.margin>
            <Insets left="30.0" right="30.0" />
         </GridPane.margin>
      </Button>
   </children>
   <padding>
      <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
   </padding>
</GridPane>

 

Kontrolleritiedostoon tehdään SimpleIntegerProperty muuttujia ja niiden sitomiset laskureihin.

# AutolaskuriControllerBind

AutolaskuriController.java

public class AutolaskuriController implements Initializable {
    @FXML private Label laskuriHA;
    @FXML private Label laskuriKA;
    private SimpleIntegerProperty ha = new SimpleIntegerProperty(0);
    private SimpleIntegerProperty ka = new SimpleIntegerProperty(0);

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        laskuriHA.textProperty().bind(ha.asString());
        laskuriKA.textProperty().bind(ka.asString());
    }

    @FXML void handleHA() {
        ha.set(ha.get()+1);
    }

    @FXML void handleKA() {
        ka.set(ka.get()+1);
    }

    @FXML void handleNollaa() {
        ha.set(0);
        ka.set(0);
    }

}

 

  • private SimpleIntegerProperty ha = ...
    Luodaan tarkkailtava kokonaislukuolio.
  • laskuriHA.textProperty().bind(ha.asString());
    Pyydetään ha-oliolta StringBinding-tyyppinen olio, joka kuuntelee ha:n muutoksia ja kertoo niistä sitten laskuriHA:n tekstiominaisuudelle.
  • ha.set(ha.get()+1);
  • ha.set(0);
    Muutetaan ha-olion arvoa. Tällöin automaattisesti välitetään tieto kaikkialle, mihin ha 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.

# AutolaskuriMainBind

AutolaskuriMain.java

//
    public void start(Stage primaryStage) {
        try {
            FXMLLoader ldr = new FXMLLoader(getClass().getResource("AutolaskuriView.fxml"));
            final Pane root = ldr.load();
            //final AutolaskuriController autolaskuriCtrl = (AutolaskuriController) ldr.getController();
            Scene scene = new Scene(root);
            scene.getStylesheets().add(getClass().getResource("autolaskuri.css").toExternalForm());
            primaryStage.setScene(scene);
            primaryStage.setTitle("Autolaskuri");
            primaryStage.show();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

 

# polkupyoratBind

Tehtävä: Lisää polkupyörien laskeminen

Lisää edelliseen ohjelmaan myös polkupyörien laskeminen. Kerro mitä piti muuttaa.

 

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:

# autolaskuricssloop

autolaskuri.css

.laskuri {
    -fx-font-size: 30px;
    -fx-background-color: cyan;
    -fx-background-radius: 10;
}

 

# autolaskuriviewloop

AutolaskuriView.fxml

<GridPane hgap="10.0" stylesheets="@autolaskuri.css" vgap="20.0"
 xmlns="http://javafx.com/javafx/8.0.65"
 xmlns:fx="http://javafx.com/fxml/1"
 fx:controller="autolaskuri.loop.AutolaskuriController">
  <columnConstraints>
    <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="120.0" />
    <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="120.0" />
  </columnConstraints>
  <rowConstraints>
    <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
    <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
    <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
  </rowConstraints>
   <children>
      <Label id="HA" alignment="CENTER_RIGHT" maxWidth="2000" styleClass="laskuri" text="0">
         <padding>
            <Insets right="10.0" />
         </padding>
      </Label>
      <Label id="KA" alignment="CENTER_RIGHT" maxWidth="2000" styleClass="laskuri" text="0"
       GridPane.columnIndex="1">
         <padding>
            <Insets right="10.0" />
         </padding>
      </Label>
      <Button id="HA" alignment="CENTER" maxWidth="2000"
        onAction="#handleLaske" text="Henkilöautoja" GridPane.rowIndex="1" />
      <Button id="KA" alignment="CENTER" maxWidth="2000"
        onAction="#handleLaske" text="Kuorma-autoja" GridPane.columnIndex="1" GridPane.rowIndex="1" />
      <Button fx:id="buttonNollaa" alignment="CENTER" maxWidth="2000"
       onAction="#handleNollaa" text="Nollaa" GridPane.columnSpan="2" GridPane.rowIndex="2">
         <GridPane.margin>
            <Insets left="30.0" right="30.0" />
         </GridPane.margin>
      </Button>
   </children>
   <padding>
      <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
   </padding>
</GridPane>

 

  • <Label id="HA" ... styleClass="laskuri"...>
    Annetaan labeleille tunniste ja pidetään huoli että niiden css-luokkana on laskuri.
  • <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 ainostaan buttonNollaa-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.

# AutolaskuriControllerLoop

AutolaskuriController.java

public class AutolaskuriController implements Initializable {

    @FXML private Button buttonNollaa;
    private Map<String,SimpleIntegerProperty> laskettavat = new HashMap<>();

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        List<Label> laskurit =
            getNodes(buttonNollaa.getParent(), Label.class,
                     n -> n.getStyleClass().contains("laskuri"), true);

        for (Label laskuri: laskurit) {
            SimpleIntegerProperty laskettava = new SimpleIntegerProperty(0);
            laskuri.textProperty().bind(laskettava.asString());
            laskettavat.put(laskuri.getId(),laskettava);
        }
    }


    @FXML void handleLaske(ActionEvent event) {
        Node source = (Node)event.getSource();
        String id = source.getId();
        if ( id == null || id.length() < 1 ) return;
        SimpleIntegerProperty laskettava = laskettavat.get(id);
        if ( laskettava == null ) return;
        laskettava.set(laskettava.get()+1);
    }


    @FXML void handleNollaa() {
        for ( SimpleIntegerProperty laskettava: laskettavat.values())
            laskettava.set(0);
    }

}

 

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

25 Feb 19 (edited 25 Feb 19)
  • @FXML private Button buttonNollaa;
    Käytetään ainostaan buttonNollaa-painiketta yhteisenä elementtinä .fxml-tiedoston ja kontrollerin välillä.
  • private ... laskettavat = new HashMap<>();
    Tietorakenne, johon avaimen perusteella tallennetaan laskettavia SimpleIntegerProperty-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 on laskuri. getNodes on FXGui-kirjastossa.
  • for (Label laskuri: laskurit) {
    Käydään läpi kaikki löytyneet laskurit
  • SimpleIntegerProperty 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 tulevaa ActionEvent-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 vaikka inc-metodi, niin tämä kohta tulisi vielä siistimmäksi.
  • @FXML void handleNollaa() {
    Muutetaan nollaamiskäsittelyä käymään läpi kaikki laskurit
  • for ( ... laskettava: laskettavat.values())
    Pyydetään tietorakenteelta kaikki avaimien kohdalle tallennetut arvot (laskettava-viitteet) ja käydään ne yksitellen läpi
  • laskettava.set(0); joista kukin vuorollaan nollataan.

Vanha pääohjelma kelpaa jälleen sellaisenaan.

# AutolaskuriMainLoop

AutolaskuriMain.java

//
    public void start(Stage primaryStage) {
        try {
            FXMLLoader ldr = new FXMLLoader(getClass().getResource("AutolaskuriView.fxml"));
            final Pane root = ldr.load();
            //final AutolaskuriController autolaskuriCtrl = (AutolaskuriController) ldr.getController();
            Scene scene = new Scene(root);
            scene.getStylesheets().add(getClass().getResource("autolaskuri.css").toExternalForm());
            primaryStage.setScene(scene);
            primaryStage.setTitle("Autolaskuri");
            primaryStage.show();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

 

# polkupyoratLoop

Tehtävä: Lisää polkupyörien laskeminen

Lisää edelliseen ohjelmaan myös polkupyörien laskeminen. Kerro mitä piti muuttaa.

 

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 tullutta forEach-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 on 1/20 laskettavan arvosta. Koska ProgressBar-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?

26 Feb 22 (edited 27 Feb 22)

Käytetään vanhaa .css-tiedostoa:

# autolaskuricsscomp

autolaskuri.css

.laskuri {
    -fx-font-size: 30px;
    -fx-background-color: cyan;
    -fx-background-radius: 10;
}

 

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:

# laskuriviewcomp

LaskuriView.fxml

<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.layout.VBox?>

<fx:root spacing="15.0" stylesheets="@autolaskuri.css"
  type="VBox" xmlns="http://javafx.com/javafx/8.0.65"
  xmlns:fx="http://javafx.com/fxml/1" >
   <children>
      <Label fx:id="laskuri" alignment="CENTER_RIGHT" maxWidth="2000.0"
       styleClass="laskuri" text="0" textAlignment="RIGHT">
         <padding>
            <Insets right="10.0" />
         </padding>
      </Label>
      <Button fx:id="button" mnemonicParsing="false"
       maxWidth="2000.0" text="Henkilöautoja" />
   </children>
</fx:root>

 

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.

# Laskuricomp

Laskuri.java

public class Laskuri  extends VBox 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 Laskurit {
        private List<Laskuri> alkiot = new ArrayList<>();
        public void add(Laskuri alkio) { alkiot.add(alkio); }
        public void reset() { alkiot.forEach(l -> l.reset()); }
    }

    @FXML private Label laskuri;
    @FXML private Button button;

    private Laskettava laskettava = new Laskettava(0);

    /** Luodaan laskuri */
    public Laskuri() {
        FXMLLoader loader = new FXMLLoader(getClass().getResource("LaskuriView.fxml"));
        loader.setRoot(this);
        loader.setController(this);
        try {
            loader.load();
        }
        catch (IOException ex) {
            System.err.println(ex.getMessage());
        }
    }


    @Override
    public void initialize(URL location, ResourceBundle resources) {
        laskuri.textProperty().bind(laskettava.asString());
        laskuri.setOnMouseClicked(e -> laskettava.inc());
        laskuri.setOnTouchPressed(e -> laskettava.inc());
        button.setOnAction(e -> laskettava.inc());
    }


    /** Nollataan laskuri */
    public void reset() { laskettava.reset(); }

    /** @return painikkeen teksti */
    public String getCaption() { return button.getText(); }

    /** @param caption painikkeelle asetettava teksti */
    public void setCaption(String caption) { button.setText(caption); }
}

 

  • 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 lukea caption-ominaisuuden kautta .fxml-tiedostossa.

Komponettien ansiosta .fxml-tiedosto selkeytyy jonkin verran ja itse laskurin takia tulee nyt todella vähän koodia.

# autolaskuriviewcomp

AutolaskuriView.fxml

<GridPane hgap="10.0" stylesheets="@autolaskuri.css"
 vgap="20.0" xmlns="http://javafx.com/javafx/8.0.65"
 xmlns:fx="http://javafx.com/fxml/1"
 fx:controller="autolaskuri.comp.AutolaskuriController">
  <columnConstraints>
    <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="120.0" />
    <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="120.0" />
  </columnConstraints>
  <rowConstraints>
    <RowConstraints minHeight="40.0" prefHeight="70.0" vgrow="SOMETIMES" />
    <RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
  </rowConstraints>
   <children>
      <Laskuri caption="Henkilöautoja" />
      <Laskuri caption="Kuorma-autoja" GridPane.columnIndex="1" />
      <Button fx:id="buttonNollaa" alignment="CENTER" maxWidth="2000"
        onAction="#handleNollaa" text="Nollaa"
        GridPane.columnSpan="2" GridPane.rowIndex="1">
         <GridPane.margin>
            <Insets left="30.0" right="30.0" />
         </GridPane.margin>
      </Button>
   </children>
   <padding>
      <Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
   </padding>
</GridPane>

 

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.

11 Mar 17 (edited 06 Jun 17)

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 HBoxin sisään ja vaihtaa caption.

Nyt ohjelman kontrolleri lyhenee huomattavasti.

# AutolaskuriControllerComp

AutolaskuriController.java

public class AutolaskuriController implements Initializable {

    @FXML private Button buttonNollaa;
    private Laskuri.Laskurit laskettavat = new Laskuri.Laskurit();

    @Override
    public void initialize(URL location, ResourceBundle resources) {
        Node parent = buttonNollaa.getParent();
        List<Laskuri> laskurit = getNodes(parent, Laskuri.class, n->true, true);

        for (Laskuri laskuri: laskurit) {
            laskettavat.add(laskuri);
        }
    }


    @FXML void handleNollaa() { laskettavat.reset(); }
}

 

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.

# AutolaskuriMainComp

AutolaskuriMain.java

//
    public void start(Stage primaryStage) {
        try {
            FXMLLoader ldr = new FXMLLoader(getClass().getResource("AutolaskuriView.fxml"));
            final Pane root = ldr.load();
            //final AutolaskuriController autolaskuriCtrl = (AutolaskuriController) ldr.getController();
            Scene scene = new Scene(root);
            scene.getStylesheets().add(getClass().getResource("autolaskuri.css").toExternalForm());
            primaryStage.setScene(scene);
            primaryStage.setTitle("Autolaskuri");
            primaryStage.show();
        } catch(Exception e) {
            e.printStackTrace();
        }
    }

 

# polkupyoratComp

Tehtävä: Lisää polkupyörien laskeminen

Lisää edelliseen ohjelmaan myös polkupyörien laskeminen. Kerro mitä piti muuttaa.

 

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:

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:

  1. Klikkaa hiiren oikealla napilla projektin nimen päällä
  2. Properties/Java Build Path/Libraries
  3. Vaihda se oma JavaFX:ää varten tehty JRE tilalle.
  4. 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

12 Feb 21
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.

03 Feb 20

java.lang.ClassNotFoundException tulee jatkuvasti. Kaikki edellä mainitut ratkaisut kokeiltu eikä auta.

VL: Mahdotonta vastata näillä tiedoilla :-) Pääteohjaukseen ja näyttää ongelmaa.

12 Feb 21 (edited 12 Feb 21)

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.

JavaJDK

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"/>
# kannatus1

Puolueiden kannatus

    @Override
    public void start(Stage ikkuna) {
        // luodaan kaaviossa käytettävät x- ja y-akselit
        var xAkseli = new NumberAxis(1968, 2008, 4);
        var yAkseli = new NumberAxis();

        // asetetaan akseleille nimet
        xAkseli.setLabel("Vuosi");
        yAkseli.setLabel("Suhteellinen kannatus (%)");

        // luodaan viivakaavio. Viivakaavion arvot annetaan numeroina
        // ja se käyttää aiemmin luotuja x- ja y-akseleita
        var viivakaavio = new LineChart<>(xAkseli, yAkseli);
        viivakaavio.setTitle("Suhteellinen kannatus vuosina 1968-2008");

        // luodaan viivakaavioon lisättävä datajoukko
        var rkpData = new XYChart.Series<Number, Number>();
        rkpData.setName("RKP");
        // lisätään datajoukkoon yksittäisiä pisteitä
        rkpData.getData().add(new XYChart.Data<Number, Number>(1968, 5.6));
        rkpData.getData().add(new XYChart.Data<Number, Number>(1972, 5.2));
        rkpData.getData().add(new XYChart.Data<Number, Number>(1976, 4.7));
        rkpData.getData().add(new XYChart.Data<Number, Number>(1980, 4.7));
        rkpData.getData().add(new XYChart.Data<Number, Number>(1984, 5.1));
        rkpData.getData().add(new XYChart.Data<Number, Number>(1988, 5.3));
        rkpData.getData().add(new XYChart.Data<Number, Number>(1992, 5.0));
        rkpData.getData().add(new XYChart.Data<Number, Number>(1996, 5.4));
        rkpData.getData().add(new XYChart.Data<Number, Number>(2000, 5.1));
        rkpData.getData().add(new XYChart.Data<Number, Number>(2004, 5.2));
        rkpData.getData().add(new XYChart.Data<Number, Number>(2008, 4.7));

        // lisätään datajoukko viivakaavioon
        viivakaavio.getData().add(rkpData);

        // luodaan toinen viivakaavioon lisättävä datajoukko
        var vihreatData = new XYChart.Series<Number, Number>();
        vihreatData.setName("VIHR");
        // lisätään datajoukkoon yksittäisiä pisteitä
        vihreatData.getData().add(new XYChart.Data<Number, Number>(1984, 2.8));
        vihreatData.getData().add(new XYChart.Data<Number, Number>(1988, 2.3));
        vihreatData.getData().add(new XYChart.Data<Number, Number>(1992, 6.9));
        vihreatData.getData().add(new XYChart.Data<Number, Number>(1996, 6.3));
        vihreatData.getData().add(new XYChart.Data<Number, Number>(2000, 7.7));
        vihreatData.getData().add(new XYChart.Data<Number, Number>(2004, 7.4));
        vihreatData.getData().add(new XYChart.Data<Number, Number>(2008, 8.9));

        // lisätään datajoukko viivakaavioon
        viivakaavio.getData().add(vihreatData);

        // näytetään viivakaavio
        Scene nakyma = new Scene(viivakaavio, 640, 480);
        ikkuna.setScene(nakyma);
        ikkuna.show();
    }

 

Sama datan avulla tehtynä

# kannatus2

Puolueiden kannatus

    public void start(Stage ikkuna) {
        Map<String, double[][]> kannatukset = new TreeMap<String, double[][]>();
        double[][] rkp = {
            {1968, 5.6}, {1972, 5.2}, {1976, 4.7}, {1980, 4.7}, {1984, 5.1}, {1988, 5.3},
            {1992, 5.0}, {1996, 5.4}, {2000, 5.1}, {2004, 5.2}, {2008, 4.7},
        };
        double[][] vihr = {
            {1984, 2.8}, {1988, 2.3}, {1992, 6.9}, {1996, 6.3}, {2000, 7.7},
            {2004, 7.4}, {2008, 8.9}
        };
        kannatukset.put("RKP", rkp);
        kannatukset.put("VIHR", vihr);

        // luodaan kaaviossa käytettävät x- ja y-akselit
        var xAkseli = new NumberAxis(1968, 2008, 4);
        var yAkseli = new NumberAxis();

        // asetetaan akseleille nimet
        xAkseli.setLabel("Vuosi");
        yAkseli.setLabel("Suhteellinen kannatus (%)");

        // luodaan viivakaavio. Viivakaavion arvot annetaan numeroina
        // ja se käyttää aiemmin luotuja x- ja y-akseleita
        var viivakaavio = new LineChart<>(xAkseli, yAkseli);
        viivakaavio.setTitle("Suhteellinen kannatus vuosina 1968-2008");

        for (var puolue: kannatukset.entrySet()) {
            var data = new XYChart.Series<Number, Number>();
            data.setName(puolue.getKey());
            for (var rivi: puolue.getValue())
                data.getData().add(new XYChart.Data<Number, Number>(rivi[0], rivi[1]));
            viivakaavio.getData().add(data);
        }
        // näytetään viivakaavio
        Scene nakyma = new Scene(viivakaavio, 640, 480);
        ikkuna.setScene(nakyma);
        ikkuna.show();
    }

 

These are the current permissions for this document; please modify if needed. You can always modify these permissions from the manage page.