- COMP.CS.140
 - 12. Graafiset käyttöliittymät
 - 12.2 JavaFX-kirjasto
 - 12.2.3 Hiukan isompi esimerkki
 
Hiukan isompi esimerkki¶
Käyttöliittymien toteutuksessa käytetään yleensä valmiiksi toteutettuja kirjastokomponentteja. Yksinkertaisimpia tällaisia ovat nappula (engl. Button) ja tekstikenttä (engl. TextField). Listan tavallisista komponenteista löydät JavaFx-tutoriaalista.
Jokaisella elementillä on sijainti ruudulla. Sijainnin voi määritellä useammalla erilaisella elementillä.
Aiempi esimerkki käytti FlowPanea. Muita ladontaruutuja:
BorderPane: asemoi lapsielementtinsä ylös, alas, vasemalle, oikealle ja keskelle
HBox: asemoi lapsielementtinsä vaakasuoraan riviin
VBox: asemoi lapsielementtinsä pystysuoraksi sarakkeeksi
GridPane: asemoi lapsielementtinsä joustavaan ruudukkoon rivejä ja sarakkeita
TilePane: asemoi lapsielementtinsä vakiokokoisten ruutujen ruudukkoon
Ladontoja voi myös asettaa sisäkkäin. Ladonnat sijaitsevat pakkauksessa javafx.scene.layout. Otetaan esimerkki:
package example.range;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;
// Toteutetaan ohjelma, joka laskee sähköauton odotettavissa olevan käyttömatkan
// sen hetkisellä kulutuksella
public class App extends Application {
    @Override
    public void start(Stage stage) {
        // Ohjelman pääikkunan otsikoksi asetetaan "Range Calculator"
        stage.setTitle("Range Calculator");
        // Käytetään käyttöliittymäkomponenttien asemointiin
        // ruutuasettelua
        GridPane grid = new GridPane();
        // Luodaan skene, johon ruudukko sijoitetaan
        Scene scene = new Scene(grid, 350, 275);
        stage.setScene(scene);
        // Text luo yksinkertaisen tekstialueen
        Text scenetitle = new Text("Calculate expected range");
        // jonka voi sijoittaa ruudukolle
        // 0,0 tarkoittaa vasenta yläkulmaa
        // lisäksi määritellään, kuinka monta saraketta ja riviä elementti kattaa
        grid.add(scenetitle, 0, 0, 2, 1);
        // Vastaavasti voidaan lisätä paikoilleen tarvittavat syötekentät
        // Label eroaa Textistä siinä, ettei Labelin sisältöä voi muuttaa
        Label battery = new Label("Battery kWh:");
        grid.add(battery, 0, 1);
        Label consumption = new Label("Consumption kWh/100km");
        grid.add(consumption, 0, 2);
        Label result = new Label("Current Range");
        grid.add(result, 0, 3);
        // Lisätään ruudukkoon vielä tekstikentät
        // akuston kapasiteetille
        TextField inputBatt = new TextField();
        grid.add(inputBatt, 1, 1);
        // sen hetkiselle keskikulutukselle
        TextField inputCon = new TextField();
        grid.add(inputCon, 1, 2);
        // ja tulokselle
        TextField resultField = new TextField();
        grid.add(resultField, 1, 3);
        // Ohjelman kolme nappulaa ladotaan vaakasuoraan
        // HBoxin avulla
        // Rakentajan parametri määrittää elementtien välisen tilan suuruuden
        HBox hbBtn = new HBox(10);
        grid.add(hbBtn, 1, 5);
        Button btn1 = new Button("Calculate");
        hbBtn.getChildren().add(btn1);
        Button btn2 = new Button("Clear");
        hbBtn.getChildren().add(btn2);
        Button exitBtn = new Button("Exit");
        hbBtn.getChildren().add(exitBtn);
        // Nappula laskee käyttömatkan syötekenttien arvojen mukaisesti
        // Huom! ei sisällä virhetarkasteluja!
        btn1.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent e) {
                var b = Double.parseDouble(inputBatt.getText());
                var c = Double.parseDouble(inputCon.getText());
                var r = (b/c)*100;
                resultField.setText(String.format("%.2f", r));
            }
        });
        // Nappulan painaminen tyhjentää kentät
        btn2.setOnAction(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent e) {
                inputBatt.clear();
                inputCon.clear();
                resultField.clear();
            }
        });
        // Nappulan painaminen sulkee ohjelman
        // ensin suljetaan stage
        exitBtn.setOnAction((event) -> {stage.close();});
        // ja stagen sulkeminen sulkee koko ohjelman
        stage.setOnCloseRequest((event) -> { Platform.exit();});
        stage.show();
    }
    public static void main(String[] args) {
        launch();
    }
}
Ohjelma näyttää tältä:
Useiden näkymien käyttö¶
Ohjelmassa on usein tilanteita, joissa tarvitaan enemmän kuin esimerkeissä käytetty yksi ikkuna. Skenejä voi luoda useita ja niiden välillä vaihtaa tapahtumaan sidotusti.
public class App extends Application {
    @Override
    public void start(Stage stage) {
        Button printButton = new Button();
        printButton.setText("Print");
        TextField output = new TextField();
        output.setPrefWidth(250);
        Button switchButton = new Button();
        switchButton.setText("Switch");
        // Nappulat asemoidaan flowpanella
        var group = new FlowPane();
        group.getChildren().add(printButton);
        group.getChildren().add(switchButton);
        group.getChildren().add(output);
        printButton.setOnAction((event) -> {
            output.setText("And all the men and women merely players;");
        });
        printButton.setOnKeyPressed( new EventHandler<KeyEvent>() {
        @Override
        public void handle(KeyEvent k) {
            if (k.getCode().equals(KeyCode.ENTER)) {
                output.clear();
            }
        }
        });
        // Ensimmäinen skene ohjelman ikkunalle
        Scene scene = new Scene(group, 300, 150);
        stage.setScene( scene );
        stage.setTitle("All the world's a stage");
        // Gridpane ja uusi skene toiselle näkymälle
        GridPane grid = new GridPane();
        Scene scene2 = new Scene(grid, 350, 275);
        // Toinen skene sisältää nappulat poistumiseen
        // ja paluuseen ensimmäiseen näkymään
        Button exitButton = new Button("Exit");
        Button switchBack = new Button("Back");
        // jotka asemoidaan gridpanella
        grid.add(exitButton, 0,1);
        grid.add(switchBack, 0,0);
        // Ykkösnäkymässä nappula vaihtaa kakkosnäkymään
        switchButton.setOnAction((event) -> {
            stage.setScene(scene2);
        });
        // ja kakkosnäkymässä nappula palaa takaisin ykkösnäkymään
        switchBack.setOnAction((event) -> {
            stage.setScene(scene);
        });
        exitButton.setOnAction((event) -> {stage.close();});
        stage.setOnCloseRequest((event) -> {Platform.exit();});
        stage.show();
    }
    public static void main(String[] args) {
        launch( args );
    }
}
Tapahtumakäsittelijän sisällä voidaan luoda myös kokonaan uusi ikkuna ohjelmaan.
Toimintalogiikka erilleen!¶
On tärkeää pitää ohjelman toimintalogiikka erillään käyttöliittymän toteutuksesta.
start()-funktion alussa voi luoda ohjelman tarvitsemia olioita, jotka toteuttavat ohjelman toimintalogiikan.
Vältä ns. jumalluokkia. Jokaisella luokalla pitäisi olla selkeä vastuualue ohjelmassa.
Ohjelmointidemo (kesto 9:09)