Tämä ei ole uusin versio kurssista. Katso myös Ohjelmoinnin MOOC 2019: https://ohjelmointi-19.mooc.fi.
Tehtävät
Kahdennentoista osan tavoitteet

Tutustuu graafisten käyttöliittymien ohjelmointiin. Tuntee muutamia käyttöliittymäkomponentteja, käyttöliittymäkomponenttien asettelun mekanismeja, sekä osaa käsitellä käyttöliittymän tapahtumia. Osaa tehdä käyttöliittymiä, jotka sisältävät useamman näkymän (esimerkiksi kirjautumisnäkymä, tiedon listaukseen käytettävä näkymä, tiedon muokkaukseen käytettävä näkymä).

Graafiset käyttöliittymät

Tutustutaan seuraavaksi graafisten käyttöliittymien luomiseen. Graafiset käyttöliittymät poikkeavat aiemmin toteuttamistamme tekstikäyttöliittymistä usealla tavalla. Graafisia käyttöliittymiä luodessa hyödynnämme ensisijaisesti käyttöliittymäkirjastoja, jotka tarjoavat valmiita käyttöliittymäkomponentteja kuten tekstikenttäelementtejä ja nappeja.

Käyttäjän syötteiden käsittely poikkeaa myös tekstikäyttöliittymistä. Siinä missä tekstikäyttöliittymissä toiminnallisuus kytketään tietyn muotoiseen syötteeseen, graafisissa käyttöliittymissä toiminnallisuus lisätään käyttöliittymäkomponentteihin. Ohjelmoija esimerkiksi lisää käyttöliittymän nappiin metodin, joka käsittelee napin painallukseen liittyvän tapahtuman.

Käytämme graafisten käyttöliittymien luomiseen Javan JavaFX käyttöliittymäkirjastoa.

Graafiset käyttöliittymät ja tarvittavat kirjastot

Graafisten käyttöliittymien luomiseen käytetään JavaFx-nimistä kirjastoa. Linux-koneilla joudut -- riippuen Javan asennuksesta -- asentamaan myös openjfx-kirjaston. Tämän asentaminen onnistuu Ubuntussa (komentoriviltä) komennolla:

  sudo apt-get install openjfx
  

Tehtäväpohjissa käytetään JavaFx-ohjelmien testaamiseen TestFX-nimistä apukirjastoa. Kirjasto tulee tehtäväpohjien mukana.

Yksinkertaisen ikkunan luominen onnistuu JavaFX:n avulla seuraavanlaisella ohjelmalla.

package sovellus;

import javafx.application.Application;
import javafx.stage.Stage;

public class JavaFxSovellus extends Application {

    @Override
    public void start(Stage ikkuna) {
        ikkuna.setTitle("Hei Maailma!");
        ikkuna.show();
    }

    public static void main(String[] args) {
        launch(JavaFxSovellus.class);
    }
}

Kun ohjelman käynnistää, sovellus näyttää seuraavalta.

Tyhjä ikkuna, jonka otsikko on 'Hei Maailma!'

 

Omien JavaFX-sovellusten laatiminen

Voit tehdä omia JavaFx-projekteja NetBeansissa valitsemalla File -> New Project. Valitse tämän jälkeen projektilistauksesta JavaFx ja sieltä JavaFx Application. Tämän jälkeen edessäsi on projektien luomiseen käytetty näkymä, missä voit nimetä projektin.

Mitä ohjelmassa oikein tapahtuu? Luokkamme JavaFxSovellus perii luokan Application, joka tarjoaa rungon graafisten käyttöliittymien luomiseen. Sovellus käynnistetään Application-luokalta perittävällä metodilla launch, jolle annetaan parametrina käynnistettävän luokan nimi muodossa LuokanNimi.class.

Kun metodia launch kutsutaan, Application-luokassa sijaitseva metodi luo parametrina annetusta luokasta (tässä JavaFxSovellus) uuden olion ja kutsuu sen init-metodia. Metodi init periytyy luokasta Application, ja sitä käytetään esimerkiksi ohjelmassa käytettävien olioiden alustamiseen. Jätimme sen tässä toteuttamatta, sillä ohjelmamme on melko yksinkertainen. Metodin init kutsumisen jälkeen ohjelma kutsuu metodia start, joka saa parametrinaan ikkunaa kuvaavan Stage-olion. Yllä tehdyssä start-metodin toteutuksessa parametrina saadulle stage-oliolle asetetaan otsikko metodilla setTitle, jonka jälkeen kutsutaan ikkunan näyttämiseen johtavaa metodia show. Lopulta ohjelma jää kuuntelemaan käyttöliittymässä tapahtuvia tapahtumia kuten ikkunan sulkemista, joka johtaa sovelluksen sammumiseen.

Luo tehtäväpohjassa olevaan luokkaan graafinen käyttöliittymä, jonka otsikkona on "Sovellukseni". Sovelluksen tulee käynnistyä kun main-metodi suoritetaan.

Huom! Sekä tässä että tulevassa tehtävässä testit käynnistävät sovelluksen. Käytössä olevissa testeissä on huomattu ongelmia Windows-käyttöjärjestelmissä silloin, kun käyttöjärjestelmä skaalaa ruutua (tapahtuu isoilla resoluutioilla). Vaikkei testit toimisi paikallisesti oikein, voit palauttaa tehtävän kuitenkin TMC:lle, joka antaa testeistä tarkoitetun palautteen.

Käyttöliittymän rakenne

Graafiset käyttöliittymät koostuvat oleellisesti kolmesta osasta. Stage-olio toimii ohjelman ikkunana. Stage-oliolle asetetaan Scene-olio, joka kuvastaa ikkunassa olevaa näkymää. Scene-olio taas sisältää näkymään liittyvien komponenttien asettelusta vastaavan olion (esim. FlowPane), joka taas sisältää konkreettiset käyttöliittymäkomponentit.

Alla oleva ohjelma luo käyttöliittymän, jossa on yksittäinen nappi.

package sovellus;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;

public class JavaFxSovellus extends Application {

    @Override
    public void start(Stage ikkuna) {
        Button nappi = new Button("Tämä on nappi");

        FlowPane komponenttiryhma = new FlowPane();
        komponenttiryhma.getChildren().add(nappi);

        Scene nakyma = new Scene(komponenttiryhma);

        ikkuna.setScene(nakyma);
        ikkuna.show();
    }

    public static void main(String[] args) {
        launch(JavaFxSovellus.class);
    }
}

Sovellus näyttää seuraavalta.

Ikkuna, jossa on nappi. Napissa on teksti 'Tämä on nappi'.

 

Käyttöliittymäkomponentit lisätään niiden asettelusta vastaavan olion -- edellä FlowPane -- "lapsiksi". Tämä liittyy JavaFx:n suunnittelussa tehtyyn päätökseen, missä jokainen käyttöliittymäkomponenttien asetteluun käytettävä olio voi sisältää muita käyttöliittymäkomponenttien asetteluun käytettäviä olioita sekä luonnollisesti myös käyttöliittymäkomponentteja. Tämä mahdollistaa graafiset käyttöliittymät, joissa käyttöliittymäkomponenttien asettelutapa riippuu niiden paikasta käyttöliittymässä. Esimerkiksi käyttöliittymässä ylhäällä olevan valikon vaihtoehdot asetetaan yleensä vierekkäin, kun taas listattavat asiat allekkain.

Käyttöliittymän rakenne on siis lyhyesti seuraava. Ikkuna sisältää Scene-olion. Scene-olio sisältää käyttöliittymäkomponenttien asettelusta vastaavan olion. Käyttöliittymäkomponenttien asettelusta vastaava olio voi sisältää sekä käyttöliitymäkomponentteja, että käyttöliittymäkomponenttien asettelusta vastaavia olioita.

Käyttöliittymäkomponentit

Graafisia käyttöliittymiä luodessa ohjelmoijat tyypillisesti hyödyntävät valmiiden käyttöliittymäkirjastojen tarjoamia osia sovellusten laatimiseen. Ohjelmoijan ei esimerkiksi kannata toteuttaa käyttöliittymän nappia tyhjästä (eli luoda luokkaa, joka piirtää napin sekä mahdollistaa siihen liittyvien toiminnallisuuksien käsittelyn), sillä vastaava komponentti löytyy yleensä käyttöliittymäkirjastoista valmiina. Tutustutaan seuraavaksi muutamaan käyttöliittymäkomponenttiin.

Tekstin näyttäminen tapahtuu Label-luokan avulla. Label tarjoaa käyttöliittymäkomponentin, jolle voi asettaa tekstiä ja jonka sisältämää tekstiä voi muokata metodien avulla. Näytettävä teksti asetetaan joko konstruktorissa tai erillisellä setText-metodilla.

package sovellus;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;

public class JavaFxSovellus extends Application {

    @Override
    public void start(Stage ikkuna) {
        Label teksti = new Label("Tekstielementti");

        FlowPane komponenttiryhma = new FlowPane();
        komponenttiryhma.getChildren().add(teksti);

        Scene nakyma = new Scene(komponenttiryhma);

        ikkuna.setScene(nakyma);
        ikkuna.show();
    }

    public static void main(String[] args) {
        launch(JavaFxSovellus.class);
    }
}
Ikkuna, jossa on tekstielementti. Ikkunassa näkyy teksti 'Tekstielementti'.

 

Käyttöliittymään saa painikkeita Button-luokan avulla. Napin lisääminen käyttöliittymään tapahtuu aivan kuten tekstielementin lisääminen.

package sovellus;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;

public class JavaFxSovellus extends Application {

    @Override
    public void start(Stage ikkuna) {
        Button nappi = new Button("Tämä on nappi");

        FlowPane komponenttiryhma = new FlowPane();
        komponenttiryhma.getChildren().add(nappi);

        Scene nakyma = new Scene(komponenttiryhma);

        ikkuna.setScene(nakyma);
        ikkuna.show();
    }

    public static void main(String[] args) {
        launch(JavaFxSovellus.class);
    }
}
Ikkuna, jossa on nappi. Napissa on teksti 'Tämä on nappi'.

 

Sovellukseen voi lisätä myös useampia käyttöliittymäelementtejä samaan aikaan. Alla käytössä on sekä nappi että tekstielementti.

package sovellus;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;

public class JavaFxSovellus extends Application {

    @Override
    public void start(Stage ikkuna) {
        Button nappi = new Button("Tämä on nappi");
        Label teksti = new Label("Tekstielementti");

        FlowPane komponenttiryhma = new FlowPane();
        komponenttiryhma.getChildren().add(nappi);
        komponenttiryhma.getChildren().add(teksti);

        Scene nakyma = new Scene(komponenttiryhma);

        ikkuna.setScene(nakyma);
        ikkuna.show();
    }

    public static void main(String[] args) {
        launch(JavaFxSovellus.class);
    }
}

Sovellus näyttää seuraavalta.

Ikkuna, jossa on nappi sekä tekstielementti. Napissa on teksti 'Tämä on nappi' ja tekstielementti sisältää tekstin 'Tekstielementti'.

 

Osoitteessa https://docs.oracle.com/javase/8/javafx/user-interface-tutorial/ on listattuna joukko valmiita käyttöliittymäkomponentteja. Sivu tarjoaa myös esimerkkejä käyttöliittymäkomponenttien käytöstä.

Käyttöliittymäkomponentteja on huomattava määrä. Niiden opiskeluun kannattaa käyttää verkossa olevia valmiita oppaita kuten edellä mainittua osoitetta https://docs.oracle.com/javase/8/javafx/user-interface-tutorial/. Käyttöliittymäkomponentteja kannattaa kokeilla aluksi erikseen siten, että kokeilee yhden komponentin lisäämistä ja tarkastelee sen toimintaa.

Kun yksittäiset komponentit tulevat tutuksi, on niiden käyttäminen suoraviivaisempaa. Lähes kaikille komponenteille yhteistä on se, miten ne lisätään sovellukseen. Kun osaat lisätä yhden komponentin käyttöliittymään, osaat lisätä käytännössä lähes kaikki komponentit käyttöliittymään.

Ainoa merkittävä ero käyttöliittymäkomponenttien lisäämisessä liittyy siihen, että mihin kohtaan käyttöliittymää komponentin haluaa lisätä. Tarkastellaan käyttöliittymäkomponenttien asettelua kohta.

Luo edellistä esimerkkiä seuraten tehtäväpohjassa olevaan luokkaan käyttöliittymä, jossa on nappi (Button) ja tekstielementti (Label). Napin tulee olla tekstielementin vasemmalla puolella tai yläpuolella.

Luo tehtäväpohjassa olevaan luokkaan graafinen käyttöliittymä, jossa on nappi ja tekstikenttä. Tekstikentän saa toteutettua luokalla TextField. Napin tulee olla tekstikentän vasemmalla puolella tai yläpuolella.

Käyttöliittymäkomponenttien asettelu

Jokaisella käyttöliittymäkomponentilla on käyttöliittymässä sijainti. Komponentin sijainnin määrää käytössä oleva käyttöliittymäkomponenttien asetteluun käytettävä luokka.

Edellisissä esimerkeissä käytimme käyttöliittymäkomponenttien asetteluun FlowPane-nimistä luokkaa. FlowPanen avulla käyttöliittymään lisättävät komponentit tulevat vierekkäin. Jos ikkunan koko pienenee siten, että kaikki komponentit eivät mahdu vierekkäin, rivitetään komponentit automaattisesti. Alla olevassa kuvassa edellisen esimerkin tuottamaa sovellusta on kavennettu, jolloin elementit ovat rivittyneet automaattisesti.

Ikkuna, jossa on nappi sekä tekstielementti. Napissa on teksti 'Tämä on nappi' ja tekstielementti sisältää tekstin 'Tekstielementti'. Ikkunan leveys on niin pieni, että elementit ovat rivitetty omille riveilleen.

 

BorderPane

BorderPane-luokan avulla käyttöliittymäkomponentit voidaan asetella viiteen pääkohtaan käyttöliittymässä: ylälaita, oikea laita, alalaita, vasen laita ja keskikohta. Perinteiset sovellukset, kuten käyttämäsi web-selain hyödyntävät tätä asettelua. Ylälaidassa on valikko sekä osoiterivi, ja keskellä on sivun sisältö.

package sovellus;

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

public class JavaFxSovellus extends Application {

    @Override
    public void start(Stage ikkuna) {
        BorderPane asettelu = new BorderPane();
        asettelu.setTop(new Label("ylälaita"));
        asettelu.setRight(new Label("oikea laita"));
        asettelu.setBottom(new Label("alalaita"));
        asettelu.setLeft(new Label("vasen laita"));
        asettelu.setCenter(new Label("keskikohta"));

        Scene nakyma = new Scene(asettelu);

        ikkuna.setScene(nakyma);
        ikkuna.show();
    }

    public static void main(String[] args) {
        launch(JavaFxSovellus.class);
    }
}
BorderPane-asettelua käyttävä käyttöliittymä, jossa jokaiseen pääkohtaan on asetettu tekstielementti.

 

Luo tehtäväpohjassa olevaan luokkaan graafinen käyttöliittymä, joka käyttää BorderPane-luokkaa elementtien asetteluun. Lisää ylälaitaan tekstielementti, jossa on teksti "NORTH", oikeaan laitaan tekstielementti, jossa on teksti "EAST", ja alalaitaan tekstielementti, jossa on teksti "SOUTH".

HBox

HBox-luokan avulla käyttöliittymäkomponentit asetellaan riviin.

@Override
public void start(Stage ikkuna) {
    HBox asettelu = new HBox();

    asettelu.getChildren().add(new Label("eka"));
    asettelu.getChildren().add(new Label("toka"));
    asettelu.getChildren().add(new Label("kolmas"));

    Scene nakyma = new Scene(asettelu);

    ikkuna.setScene(nakyma);
    ikkuna.show();
}
Tekstielementit on asetettu riviin HBox-asettelun avulla. Elementit ovat kiinni toisissaan.

 

Kuten edellisestä esimerkistä huomaa, HBox asettaa käyttöliittymäelementit oletuksena täysin toisiinsa kiinni. Metodin setSpacing avulla elementtien väliin saa tyhjää.

@Override
public void start(Stage ikkuna) {
    HBox asettelu = new HBox();
    asettelu.setSpacing(10);

    asettelu.getChildren().add(new Label("eka"));
    asettelu.getChildren().add(new Label("toka"));
    asettelu.getChildren().add(new Label("kolmas"));

    Scene nakyma = new Scene(asettelu);

    ikkuna.setScene(nakyma);
    ikkuna.show();
}
Tekstielementit on asetettu riviin HBox-asettelun avulla. Elementtien välillä on 10 pikseliä eroa.

 

Luokka VBox toimii vastaavasti, mutta asettelee käyttöliittymäkomponentit allekkain.

Tekstielementit on asetettu allekkain VBox-asettelun avulla. Elementtien välillä on 10 pikseliä eroa.

 

GridPane

GridPane-luokan avulla käyttöliittymäkomponentit asetellaan ruudukkoon. Alla olevassa esimerkissä luodaan 3x3-kokoinen ruudukko, jossa jokaisessa ruudussa on nappi.

@Override
public void start(Stage ikkuna) {
    GridPane asettelu = new GridPane();

    for (int x = 1; x <= 3; x++) {
        for (int y = 1; y <= 3; y++) {
            asettelu.add(new Button("" + x + ", " + y), x, y);
        }
    }

    Scene nakyma = new Scene(asettelu);

    ikkuna.setScene(nakyma);
    ikkuna.show();
}
3 kertaa 3 ruudukkoon asetetut 9 nappia.

 

Useampi asettelija samassa

Käyttöliittymäkomponenttien asettelijoita voi myös yhdistellä. Tyypillinen ratkaisu on BorderPane-asettelun käyttäminen pohjalla, jonka sisälle asetetaan muita asetteluja. Alla olevassa esimerkissä BorderPanen ylälaidassa on samalle riville asetteluun käytetty HBox ja vasemmassa laidassa allekkain asetteluun käytetty VBox. Keskelle on laitettu tekstikenttä.

package sovellus;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class JavaFxSovellus extends Application {

    @Override
    public void start(Stage ikkuna) {
	BorderPane asettelu = new BorderPane();

	HBox napit = new HBox();
	napit.setSpacing(10);
	napit.getChildren().add(new Button("Eka"));
	napit.getChildren().add(new Button("Toka"));
	napit.getChildren().add(new Button("Kolmas"));

	VBox tekstit = new VBox();
	tekstit.setSpacing(10);
	tekstit.getChildren().add(new Label("Eka"));
	tekstit.getChildren().add(new Label("Toka"));
	tekstit.getChildren().add(new Label("Kolmas"));

	asettelu.setTop(napit);
	asettelu.setLeft(tekstit);

	asettelu.setCenter(new TextArea(""));

	Scene nakyma = new Scene(asettelu);

	ikkuna.setScene(nakyma);
	ikkuna.show();
    }

    public static void main(String[] args) {
	launch(JavaFxSovellus.class);
    }
}
Samassa käyttöliittymässä käytetty useampaa asettelijaa. BorderPane luo rungon, ylälaidassa on HBox ja vasemmassa laidassa VBox. Keskellä olevaan tekstilaatikkoon on kirjoitettu tekstiä.

 

Luo tehtäväpohjassa olevaan luokkaan sovellus, joka käyttää BorderPane-luokkaa asetteluun. Keskellä tulee olla TextArea-luokasta luotu tekstikenttä, ja alalaidassa kolme tekstielementtiä. Aseta alalaidan tekstielementit HBox-olion sisään. Ensimmäisessä tekstielementissä tulee olla teksti "Kirjaimia: 0". Toisessa tekstielementissä tulee olla teksti "Sanoja: 0". Kolmannessa tekstielementissä tulee olla teksti "Pisin sana on:".

Tapahtumien käsittely

Edellä toteuttamamme käyttöliittymät eivät reagoi käyttöliittymässä tehtyihin tapahtumiin. Reagoimattomuus ei johdu käyttöliittymäkomponenteista, vaan siitä että emme ole lisänneet käyttöliittymäkomponentteihin tapahtumien käsittelyyn tarvittavaa toiminnallisuutta.

Nappien painaminen käsitellään EventHandler-rajapinnan toteuttavalla luokalla. Tapahtuman tyyppi on tällöin ActionEvent. Rajapinnan toteutukseen määritellään mitä tehdään silloin, kun käyttäjä painaa nappia.

Button nappi = new Button("Tämä on nappi");
nappi.setOnAction(new EventHandler<ActionEvent>() {
    @Override
    public void handle(ActionEvent event) {
        System.out.println("Painettu!");
    }
});

Rajapinnan eksplisiittisen toteutuksen voi korvata halutessaan Lambda-lausekkeella.

Button nappi = new Button("Tämä on nappi");
nappi.setOnAction((event) -> {
    System.out.println("Painettu!");
});

Kun edellä olevaa nappi painetaan, konsoliin tulostetaan teksti "Painettu!".

Käyttöliittymäkomponentteihin liitetyt tapahtumankäsittelijät kuten edellä käytetty EventHandler liittyvät aina tiettyihin käyttöliittymäkomponentteihin. Aina kun käyttöliittymäkomponentille tehdään toiminto, esimerkiksi napille napin painaminen, jokaista kyseiseen käyttöliittymäkomponenttiin liittyvää tapahtumankäsittelijää kutsutaan, ja niihin liittyvä ohjelmakoodi suoritetaan.

Haluamme tyypillisesti että tapahtumankäsittelijä muokkaa jonkun olion tilaa. Päästäksemme olioon käsiksi, tulee tapahtumankäsittelijällä olla viite käsiteltävään olioon. Pohditaan seuraavaa käyttöliittymää jossa on kaksi tekstikenttää sekä nappi.

@Override
public void start(Stage ikkuna) {
    TextField vasenTeksti = new TextField();
    TextField oikeaTeksti = new TextField();
    Button nappi = new Button("Kopioi");

    HBox komponenttiryhma = new HBox();
    komponenttiryhma.setSpacing(20);
    komponenttiryhma.getChildren().addAll(vasenTeksti, nappi, oikeaTeksti);

    Scene nakyma = new Scene(komponenttiryhma);

    ikkuna.setScene(nakyma);
    ikkuna.show();
}

Käyttöliittymän vasemmassa ja oikeassa laidassa on tekstikenttä. Tekstikenttien lisäksi käyttöliittymän keskellä on nappi, jossa on teksti "Kopioi".

Kaksi tekstikenttää sekä nappi, jossa on teksti 'Kopioi'.

 

Haluaisimme luoda tilanteen, missä vasemman tekstikentän sisältö kopioituu oikean kentän sisällöksi kun käyttäjä painaa nappia. Tämä onnistuu EventHandler-rajapinnan toteuttavan olion avulla. Toteutettava metodi pystyy käyttämään metodin edellä määriteltyjä olioita, jos olioiden arvoa ei aseteta ohjelmassa uudestaan yhtäsuuruusmerkillä (eli viitteet eivät muutu).

@Override
public void start(Stage ikkuna) {
    TextField vasenTeksti = new TextField();
    TextField oikeaTeksti = new TextField();
    Button nappi = new Button("Kopioi");

    nappi.setOnAction((event) -> {
        oikeaTeksti.setText(vasenTeksti.getText());
    });

    HBox komponenttiryhma = new HBox();
    komponenttiryhma.setSpacing(20);
    komponenttiryhma.getChildren().addAll(vasenTeksti, nappi, oikeaTeksti);

    Scene nakyma = new Scene(komponenttiryhma);

    ikkuna.setScene(nakyma);
    ikkuna.show();
}

Nyt nappia painettaessa vasemman tekstikentän sisältö kopioituu oikealla olevaan tekstikenttään.

Kaksi tekstikenttää sekä nappi, jossa on teksti 'Kopioi'.

 

Toteuta tehtäväpohjassa olevaan luokkaan käyttöliittymä, jossa on kolme käyttöliittymäkomponenttia. Ylin komponentti on tekstikenttä (TextField), keskimmäinen komponentti nappi (Button), ja alin komponentti tekstielementti (Label). Asettele komponentit VBox-luokan avulla. Käyttöliittymän tulee näyttää (esimerkiksi) seuraavalta.

.

 

Lisää tämän jälkeen sovellukseen toiminnallisuus, missä napin painaminen johtaa siihen, että tekstikentän teksti kopioituu tekstielementin arvoksi. Alla olevassa esimerkissä tekstikenttään on kirjoitettu teksti "hei mualima!", jonka jälkeen nappia on painettu.

.

 

Tässä tehtävässä toteutat 3x3-kokoisen ristinollapelin. Tehtävä on jaettu kolmeen osaan: ensin luodaan käyttöliittymän osat, sitten nappien toiminnallisuus, ja lopuksi mahdollisuus pelin loppuun pelaamiseen.

3x3 ristinolla.

Huom! Saat nappien merkit pysymään saman kokoisina sopivalla fontilla. Kuvakaappauksessa nappien käyttämä fontti on asetettu seuraavalla tavalla:

Button nappi = new Button(" ");
nappi.setFont(Font.font("Monospaced", 40));

Käyttöliittymä

Muokkaa luokkaa RistinollaSovellus siten, että se käynnistää graafisen käyttöliittymän. Käytä käyttöliittymäkomponenttien asetteluun ensin BorderPane-luokkaa. Aseta BorderPanen ylälaitaan tekstielementti, joka sisältää tiedon vuorosta sekä pelin loppuessa tiedon pelin loppumisesta. Aseta BorderPanen keskelle GridPane, joka sisältää 9 nappia. GridPanessa tulee olla 3 riviä ja 3 saraketta, jolloin napit muodostavat 3x3-ruudukon.

Vuorojen vaihtaminen ja reilu peli

Pelissä pelaavat X ja O. Pelin aloittaa aina X. Lisää peliin toiminnallisuus, missä nappia painamalla tilanne päivittyy siten, että nappiin asetetaan vuorossa olevan pelaajan merkki (jos on X:n vuoro, nappiin tulee teksti X). Tämän jälkeen vuoro siirtyy seuraavalle pelaajalle.

Pelin ylälaidassa olevan tekstikentän tulee kertoa aina vuorossa oleva pelaaja. Teksti on aluksi "Vuoro: X". Kun X pelaa vuoronsa, eli painaa jotain nappia, tekstiksi asetetaan "Vuoro: O". Tämän jälkeen kun O pelaa vuoronsa, tekstiksi asetetaan taas "Vuoro: X".

Huom! Jos pelaaja on jo pelannut tietyn ruudun, ei toinen pelaaja saa enää pelata sitä. Varmista, ettei vuoro muutu tilanteessa, missä pelaaja yrittää pelata jo pelatun ruudun.

Huom2! Mahdollisesti kohtaamasi virhe "local variables referenced from a lambda expression must be final or effectively final" johtuu siitä, että rajapinnoista tehdyt oliot eivät voi käyttää metodin ulkopuolella määriteltyjä muuttujia. Voit "kiertää" virheen luomalla uudet muuttujat, joihin asetat ongelmalliset arvot juuri ennen niiden käyttöönottoa metodissa.

Pelin loppuun vieminen

Lisää peliin toiminnallisuus, missä pelin voi pelata loppuun. Peli loppuu jos toinen pelaajista saa kolme samaa merkkiä riviin (pysty, vaaka, vino). Pelin loppuminen tulee ilmaista siten, että ylälaidassa on teksti "Loppu!". Tämän jälkeen pelin jatkaminen ei enää onnistu.

Käytettävä tapahtumankäsittelijä riippuu käyttöliittymäkomponentista, johon tapahtumankäsittelijä kytketään. Jos haluaisimme seurata tekstikenttään tapahtuvia muutoksia merkki merkiltä, käyttäisimme rajapintaa ChangeListener. Alla olevassa esimerkissä vasempaan tekstikenttään on kytketty rajapinnan ChangeListener toteuttava olio, joka sekä tulostaa muutokset tekstikonsoliin että asettaa aina uuden arvon oikealla olevaan tekstikenttään.

vasenTeksti.textProperty().addListener(new ChangeListener<String>() {
    @Override
    public void changed(ObservableValue<? extends String> muutos,
            String vanhaArvo, String uusiArvo) {

        System.out.println(vanhaArvo + " -> " + uusiArvo);
        oikeaTeksti.setText(uusiArvo);
    }
});

Edellä muutoksia havainnoidaan tekstikenttään liittyvästä tekstistä. Koska teksti on merkkijonomuotoista, on muutoksia käsittelevälle rajapinnalle annettu tyypiksi merkkijono. Kuten edellä, myös tässäkin esimerkissä ohjelmakoodi voidaan esittää lyhyemmässä muodossa.

vasenTeksti.textProperty().addListener((muutos, vanhaArvo, uusiArvo) -> {
    System.out.println(vanhaArvo + " -> " + uusiArvo);
    oikeaTeksti.setText(uusiArvo);
});

Ohjelma voi tehdä myös tilastointia. Edellisessä tehtävässä luotujen tekstikenttien arvot saa laskettua melko suoraviivaisesti. Alla olevaa esimerkkiä noudattaen arvot päivittyisivät aina kun käyttäjä muuttaa tekstikentän sisältöä.

vasenTeksti.textProperty().addListener((muutos, vanhaArvo, uusiArvo) -> {
    int merkkeja = uusiArvo.length();
    String[] palat = uusiArvo.split(" ");
    int sanoja = palat.length;
    String pisin = Arrays.stream(palat)
        .sorted((s1, s2) -> s2.length() - s1.length())
        .findFirst()
        .get();

    // asetetaan arvot tekstielementteihin
});

Kopioi tehtävässä Tekstitilastointia tekemäsi toteutus tehtäväpohjassa olevaan luokkaan ja liitä mukaan yllä olevassa esimerkissä oleva toiminnallisuus tilastojen laskemiseen. Lopputuloksena ohjelman pitäisi laskea kirjoitetusta tekstistä tilastoja, jotka päivittyvät automaattisesti sovellukseen.

Esimerkki tekstitilastointiin tarkoitetun ohjelman toiminnasta.

Sovelluslogiikan ja käyttöliittymälogiikan eriyttäminen

Sovelluslogiikan (esimerkiksi ristinollan rivien tarkastamiseen tai vuorojen ylläpitoon liittyvä toiminnallisuus) ja käyttöliittymän pitäminen samassa luokassa tai samoissa luokissa on yleisesti ottaen huono asia. Se vaikeuttaa ohjelman testaamista ja muokkaamista huomattavasti ja tekee lähdekoodista myös vaikeammin luettavaa. Motto "Jokaisella luokalla pitäisi olla vain yksi selkeä vastuu" pätee hyvin tässäkin.

Tarkastellaan sovelluslogiikan erottamista käyttöliittymälogiikasta. Oletetaan, että käytössämme on seuraavan rajapinnan toteuttava olio ja haluamme toteuttaa käyttöliittymän henkilöiden tallentamiseen.

public interface Henkilovarasto {
    void talleta(Henkilo henkilo);
    Henkilo hae(String henkilotunnus);

    void poista(Henkilo henkilo);
    void poista(String henkilotunnus);
    void poistaKaikki();

    Collection<Henkilo> haeKaikki();
}

Käyttöliittymää toteutettaessa hyvä aloitustapa on ensin käyttöliittymän piirtäminen, jota seuraa sopivien käyttöliittymäkomponenttien lisääminen käyttöliittymään. Henkilöiden tallennuksessa tarvitsemme kentät nimelle ja henkilötunnukselle sekä napin jolla henkilö voidaan lisätä. Käytetään luokkaa TextField nimen ja henkilötunnuksen syöttämiseen ja luokkaa Button napin toteuttamiseen. Luodaan käyttöliittymään lisäksi selventävät Label-tyyppiset selitystekstit.

Käytetään käyttöliittymän asetteluun GridPane-asettelijaa. Rivejä käyttöliittymässä on 3, sarakkeita 2. Lisätään tapahtumien käsittelytoiminnallisuus myöhemmin. Käyttöliittymän alustusmetodi näyttää seuraavalta.

@Override
public void start(Stage ikkuna) {

    Label nimiTeksti = new Label("Nimi: ");
    TextField nimiKentta = new TextField();
    Label hetuTeksti = new Label("Hetu: ");
    TextField hetuKentta = new TextField();

    Button lisaaNappi = new Button("Lisää henkilö!");

    GridPane komponenttiryhma = new GridPane();
    komponenttiryhma.add(nimiTeksti, 0, 0);
    komponenttiryhma.add(nimiKentta, 1, 0);
    komponenttiryhma.add(hetuTeksti, 0, 1);
    komponenttiryhma.add(hetuKentta, 1, 1);
    komponenttiryhma.add(lisaaNappi, 1, 2);

    // tyylittelyä: lisätään tyhjää tilaa reunoille ym
    komponenttiryhma.setHgap(10);
    komponenttiryhma.setVgap(10);
    komponenttiryhma.setPadding(new Insets(10, 10, 10, 10));

    Scene nakyma = new Scene(komponenttiryhma);

    ikkuna.setScene(nakyma);
    ikkuna.show();
}
Kaksi tekstikenttää sekä nappi, jossa on teksti 'Kopioi'.

 

Luodaan seuraavaksi ohjelmaan ActionEvent-rajapinnan toteuttava olio, joka lisää kenttien arvot Henkilovarasto-rajapinnalle.

@Override
public void start(Stage ikkuna) {
    // ...

    lisaaNappi.setOnAction((event) -> {
        henkilovarasto.talleta(new Henkilo(nimiTeksti.getText(), hetuTeksti.getText());
    });
    // ...
}

Mutta. Mistä saamme konkreettisen Henkilovarasto-olion? Se luodaan esimerkiksi start-metodin alussa. Alla annettuna koko sovelluksen runko.

// pakkaus

import javafx.application.Application;
import javafx.geometry.Insets;
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.stage.Stage;

public class HenkiloSovellus extends Application {

    @Override
    public void start(Stage ikkuna) {
        Henkilovarasto henkilovarasto = new OmaHenkilovarasto();

        Label nimiTeksti = new Label("Nimi: ");
        TextField nimiKentta = new TextField();
        Label hetuTeksti = new Label("Hetu: ");
        TextField hetuKentta = new TextField();

        Button lisaaNappi = new Button("Lisää henkilö!");
        lisaaNappi.setOnAction((event) -> {
            Henkilo lisattava = new Henkilo(nimiTeksti.getText(), hetuTeksti.getText());
            henkilovarasto.talleta(new Henkilo(lisattava);
        });

        GridPane komponenttiryhma = new GridPane();
        komponenttiryhma.add(nimiTeksti, 0, 0);
        komponenttiryhma.add(nimiKentta, 1, 0);
        komponenttiryhma.add(hetuTeksti, 0, 1);
        komponenttiryhma.add(hetuKentta, 1, 1);
        komponenttiryhma.add(lisaaNappi, 1, 2);

        // tyylittelyä: lisätään tyhjää tilaa reunoille ym
        komponenttiryhma.setHgap(10);
        komponenttiryhma.setVgap(10);
        komponenttiryhma.setPadding(new Insets(10, 10, 10, 10));

        Scene nakyma = new Scene(komponenttiryhma);

        ikkuna.setScene(nakyma);
        ikkuna.show();
    }

    public static void main(String[] args) {
        launch(HenkiloSovellus.class);
    }
}

Sovelluksen käynnistäminen luokan ulkopuolelta

Esimerkkien JavaFx-sovellukset on tähän mennessä käynnistetty Application-luokan perivän luokan sisällä olevasta pääohjelmametodista. Tarkastellaan tässä lyhyesti sovellusten käynnistämistä Application-luokan ulkopuolelta. Oletetaan, että käytössämme on seuraava yksinkertainen JavaFx-sovellus.

package sovellus;

import javafx.application.Application;
import javafx.stage.Stage;

public class JavaFxSovellus extends Application {

    @Override
    public void start(Stage ikkuna) {
        ikkuna.setTitle("Hei Maailma!");
        ikkuna.show();
    }
}

Sovelluksen voi käynnistää luokan ulkopuolelta toisesta luokasta Application-luokan tarjoaman launch-metodin avulla. Alla olevassa esimerkissä erillinen luokka Main käynnistää sovelluksen.

package sovellus;

import javafx.application.Application;

public class Main {

    public static void main(String[] args) {
        Application.launch(JavaFxSovellus.class);
    }
}

Kun yllä olevan luokan main-metodi suoritetaan, käynnistyy JavaFxSovellus-luokan määrittelemä käyttöliittymä.

Sovellukselle voi antaa myös käynnistyksenaikaisia parametreja osana launch-metodia. Metodi launch saa käynnistettävän luokan lisäksi rajattoman määrän merkkijonoja, joita voi käyttää osana käynnistystä. Nämä merkkijonot on saatavilla sovelluksen käynnistyksen yhteydessä getParameters-metodikutsulla.

Metodi getParameters() palauttaa Parameters-tyyppisen olion, jonka metodilla getNamed saa käyttöönsä avain-arvo -pareja sisältävän hajautustaulun. Avain-arvo -parit annetaan launch-metodille muodossa --avain=arvo. Alla olevassa esimerkissä otsikko muodostetaan kahdesta parametrista: organisaatio ja kurssi.

package sovellus;

import javafx.application.Application;
import javafx.application.Application.Parameters;
import javafx.stage.Stage;

public class JavaFxSovellus extends Application {

    @Override
    public void start(Stage ikkuna) {
        Parameters params = getParameters();
        String organisaatio = params.getNamed().get("organisaatio");
        String kurssi = params.getNamed().get("kurssi");

        ikkuna.setTitle(organisaatio + ": " + kurssi);
        ikkuna.show();
    }
}

Nyt sovelluksen käynnistäminen seuraavalla luokalla asettaa sovelluksen otsikoksi "Olipa kerran: Otsikko".

package sovellus;

import javafx.application.Application;

public class Main {

    public static void main(String[] args) {
        Application.launch(JavaFxSovellus.class,
            "--organisaatio=Olipa kerran",
            "--kurssi=Otsikko");
    }
}

Kirjoita ohjelma, joka kysyy tekstikäyttöliittymässä käyttäjältä sovelluksen otsikkoa. Kun käyttäjä syöttää otsikon tekstikäyttöliittymään ja painaa enter, käyttäjälle näytetään graafinen käyttöliittymä, jonka otsikkona on käyttäjän syöttämä otsikko.

Huomaa, että tässä tehtävässä ei ole automaattisia testejä. Palauta sovellus kun se toimii toivotulla tavalla.

Useampi näkymä sovelluksessa

Tähän mennessä toteuttamamme graafiset käyttöliittymät ovat sisältäneet aina yhden näkymän. Tutustutaan seuraavaksi useampia näkymiä sisältäviin käyttöliittymiin.

Yleisesti ottaen näkymät luodaan Scene-olion avulla, joiden välillä siirtyminen tapahtuu sovellukseen kytkettyjen tapahtumien avulla. Alla olevassa esimerkissä on luotu kaksi erillistä Scene-oliota, joista kummallakin on oma sisältö sekä sisältöön liittyvä tapahtuma. Alla Scene-olioihin ei ole erikseen liitetty käyttöliittymän asetteluun käytettyä komponenttia (esim. BorderPane), vaan kummassakin Scene-oliossa on täsmälleen yksi käyttöliittymäkomponentti.

package sovellus;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.stage.Stage;

public class EdesTakaisinSovellus extends Application {

    @Override
    public void start(Stage ikkuna) {

        Button edes = new Button("Edes ..");
        Button takaisin = new Button(".. takaisin.");

        Scene eka = new Scene(edes);
        Scene toka = new Scene(takaisin);

        edes.setOnAction((event) -> {
            ikkuna.setScene(toka);
        });

        takaisin.setOnAction((event) -> {
            ikkuna.setScene(eka);
        });

        ikkuna.setScene(eka);
        ikkuna.show();
    }

    public static void main(String[] args) {
        launch(EdesTakaisinSovellus.class);
    }
}

Edellä olevan sovelluksen käynnistäminen luo käyttöliittymän, jossa siirtyminen näkymästä toiseen onnistuu nappia painamalla.

Luo tehtäväpohjassa olevaan luokkaan UseampiNakyma sovellus, joka sisältää kolme erillistä näkymää. Näkymät ovat seuraavat:

  • Ensimmäinen näkymä on aseteltu BorderPane-luokan avulla. Ylälaidassa on teksti "Eka näkymä!". Keskellä on nappi, jossa on teksti "Tokaan näkymään!", ja jota painamalla siirrytään toiseen näkymään.
  • Toinen näkymä on aseteltu VBox-luokan avulla. Asettelussa tulee ensin nappi, jossa on teksti "Kolmanteen näkymään!", ja jota painamalla siirrytään kolmanteen näkymään. Nappia seuraa teksti "Toka näkymä!".
  • Kolmas näkymä on aseteltu GridPane-luokan avulla. Asettelussa tulee koordinaatteihin (0,0) teksti "Kolmas näkymä!". Koordinaatteihin (1,1) tulee nappi, jossa on teksti "Ekaan näkymään!", ja jota painamalla siirrytään ensimmäiseen näkymään.

Sovelluksen tulee käynnistyessään näyttää ensimmäinen näkymä.

Oma asettelu jokaista näkymää varten

Tutustutaan seuraavaksi kaksi erillistä näkymää sisältävään esimerkkiin. Ensimmäisessä näkymässä käyttäjää pyydetään syöttämään salasana. Jos käyttäjä kirjoittaa väärän salasanan, väärästä salasanasta ilmoitetaan. Jos käyttäjä kirjoittaa oikean salasanan, ohjelma vaihtaa seuraavaan näkymään. Ohjelman toiminta on seuraavanlainen.

 

Näkymien välillä vaihtaminen tapahtuu kuten edellisessä esimerkissä. Konkreettinen vaihtotapahtuma on määritelty kirjautumisnappiin. Nappia painettaessa ohjelma tarkastaa salasanakenttään kirjoitetun salasanan -- tässä toivotaan, että käyttäjä kirjoittaa "salasana". Jos salasana on oikein, ikkunan näyttämä näkymä vaihdetaan. Esimerkissämme näkymä sisältää vain tekstin "Tervetuloa, tästä se alkaa!".

package sovellus;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class SalattuSovellus extends Application {

    @Override
    public void start(Stage ikkuna) throws Exception {

        // 1. Luodaan salasanan kysymiseen käytetty näkymä

        // 1.1 luodaan käytettävät komponentit
        Label ohjeteksti = new Label("Kirjoita salasana ja paina kirjaudu");
        PasswordField salasanakentta = new PasswordField();
        Button aloitusnappi = new Button("Kirjaudu");
        Label virheteksti = new Label("");

        // 1.2 luodaan asettelu ja lisätään komponentit siihen
        GridPane asettelu = new GridPane();

        asettelu.add(ohjeteksti, 0, 0);
        asettelu.add(salasanakentta, 0, 1);
        asettelu.add(aloitusnappi, 0, 2);
        asettelu.add(virheteksti, 0, 3);

        // 1.3 tyylitellään asettelua
        asettelu.setPrefSize(300, 180);
        asettelu.setAlignment(Pos.CENTER);
        asettelu.setVgap(10);
        asettelu.setHgap(10);
        asettelu.setPadding(new Insets(20, 20, 20, 20));

        // 1.4 luodaan itse näkymä ja asetetaan asettelu siihen
        Scene salasanaNakyma = new Scene(asettelu);


        // 2. Luodaan tervetuloa-tekstin näyttämiseen käytetty näkymä
        Label tervetuloaTeksti = new Label("Tervetuloa, tästä se alkaa!");

        StackPane tervetuloaAsettelu = new StackPane();
        tervetuloaAsettelu.setPrefSize(300, 180);
        tervetuloaAsettelu.getChildren().add(tervetuloaTeksti);
        tervetuloaAsettelu.setAlignment(Pos.CENTER);

        Scene tervetuloaNakyma = new Scene(tervetuloaAsettelu);


        // 3. Lisätään salasanaruudun nappiin tapahtumankäsittelijä
        //    näkymää vaihdetaan jos salasana on oikein
        aloitusnappi.setOnAction((event) -> {
            if (!salasanakentta.getText().trim().equals("salasana")) {
                virheteksti.setText("Tuntematon salasana!");
                return;
            }

            ikkuna.setScene(tervetuloaNakyma);
        });

        ikkuna.setScene(salasanaNakyma);
        ikkuna.show();
    }

    public static void main(String[] args) {
        launch(SalattuSovellus.class);
    }
}

Esimerkissä on hyödynnetty sekä GridPanen että StackPanen asettelussa niiden tarjoamia setPrefSize ja setAlignment-metodeja. Metodilla setPrefSize annetaan asettelulle toivottu koko, ja metodilla setAlignment kerrotaan miten asettelun sisältö tulee ryhmittää. Parametrilla Pos.CENTER toivotaan asettelua näkymän keskelle.

Luo tehtäväpohjassa olevaan luokkaan TervehtijaSovellus sovellus, jossa on kaksi näkymää. Ensimmäisessä näkymässä on tekstikenttä, jolla kysytään käyttäjän nimeä. Toisessa näkymässä käyttäjälle näytetään tervehdysteksti. Tervehdystekstin tulee olla muotoa "Tervetuloa nimi!", missä nimen paikalle tulee käyttäjän kirjoittama nimi.

Esimerkki sovelluksen toiminnasta:

Tekstikenttään syötetään nimi, jonka jälkeen nappia painetaan. Näkymä vaihtuu toiseksi, jossa lukee 'Tervetuloa nimi!'

 

Sama pääasettelu näkymillä

Riippuen sovelluksen käyttötarpeesta, joskus sovellukselle halutaan pysyvä näkymä, jonka osia vaihdetaan tarvittaessa. Jonkinlaisen valikon tarjoavat ohjelmat toimivat tyypillisesti tällä tavalla.

Alla olevassa esimerkissä on luotu sovellus, joka sisältää päävalikon sekä vaihtuvasisältöisen alueen. Vaihtuvasisältöisen alueen sisältö vaihtuu päävalikon nappeja painamalla.

package sovellus;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class EsimerkkiSovellus extends Application {

    @Override
    public void start(Stage ikkuna) throws Exception {

        // 1. Luodaan päätason asettelu
        BorderPane asettelu = new BorderPane();

        // 1.1. Luodaan päätason asettelun valikko
        HBox valikko = new HBox();
        valikko.setPadding(new Insets(20, 20, 20, 20));
        valikko.setSpacing(10);

        // 1.2. Luodaan valikon napit
        Button eka = new Button("Eka");
        Button toka = new Button("Toka");

        // 1.3. Lisätään napit valikkoon
        valikko.getChildren().addAll(eka, toka);

        asettelu.setTop(valikko);


        // 2. Luodaan alinäkymät ja kytketään ne valikon nappeihin
        // 2.1. Luodaan alinäkymät -- tässä asettelut
        StackPane ekaAsettelu = luoNakyma("Eka näkymä!");
        StackPane tokaAsettelu = luoNakyma("Toka näkymä!");

        // 2.2. Liitetään alinäkymät nappeihin. Napin painaminen vaihtaa alinäkymää.
        eka.setOnAction((event) -> asettelu.setCenter(ekaAsettelu));
        toka.setOnAction((event) -> asettelu.setCenter(tokaAsettelu));

        // 2.3. Näytetään aluksi ekaAsettelu
        asettelu.setCenter(ekaAsettelu);


        // 3. Luodaan päänäkymä ja asetetaan päätason asettelu siihen
        Scene nakyma = new Scene(asettelu);


        // 4. Näytetään sovellus
        ikkuna.setScene(nakyma);
        ikkuna.show();
    }

    private StackPane luoNakyma(String teksti) {

        StackPane asettelu = new StackPane();
        asettelu.setPrefSize(300, 180);
        asettelu.getChildren().add(new Label(teksti));
        asettelu.setAlignment(Pos.CENTER);

        return asettelu;
    }

    public static void main(String[] args) {
        launch(EsimerkkiSovellus.class);
    }
}

Sovellus toimii seuraavalla tavalla:

Sovellus, joka sisältää valikon. Valikossa olevia nappeja painamalla voidaan vaihtaa sovelluksessa näkyvää sisältöä.

 

Luo tehtäväpohjassa olevaan luokkaan VitsiSovellus sovellus, jota käytetään yhden vitsin selittämiseen. Sovellus tarjoaa kolme nappia sisältävän valikon sekä näitä nappeja painamalla näytettävät sisällöt. Ensimmäinen nappi (teksti "Vitsi") näyttää vitsiin liittyvän kysymyksen, toinen nappi (teksti "Vastaus") näyttää vitsin kysymykseen liittyvän vastauksen, ja kolmas nappi (teksti "Selitys") näyttää vitsin selityksen.

Oletuksena (kun sovellus käynnistyy) sovelluksen tulee näyttää vitsiin liittyvä kysymys. Käytä kysymyksenä merkkijonoa "What do you call a bear with no teeth?" ja vastauksena merkkijonoa "A gummy bear.". Saat päättää selityksen vapaasti.

Hieman suurempi sovellus: Sanaston harjoittelua

Hahmotellaan vieraiden sanojen harjoitteluun tarkoitettua sovellusta. Sovellus tarjoaa käyttäjälle kaksi toimintoa: sanojen ja niiden käännösten syöttämisen sekä harjoittelun. Luodaan sovellusta varten neljä erillistä luokkaa: ensimmäinen luokka tarjoaa sovelluksen ydinlogiikkatoiminnallisuuden eli sanakirjan ylläpidon, toinen ja kolmas luokka sisältävät syöttönäkymän ja harjoittelunäkymän, ja neljäs luokka sovelluksen päävalikon sekä sovelluksen käynnistämiseen tarvittavan toiminnallisuuden.

Sanakirja

Sanakirja toteutetaan hajautustaulun ja listan avulla. Hajautustaulu sisältää sanat ja niiden käännökset, ja listaa käytetään satunnaisesti kysyttävän sanan arpomiseen. Luokalla on metodit käännösten lisäämiseen, käännöksen hakemiseen sekä käännettävän sanan arpomiseen.

package sovellus;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;

public class Sanakirja {

    private List<String> sanat;
    private Map<String, String> kaannokset;

    public Sanakirja() {
        this.sanat = new ArrayList<>();
        this.kaannokset = new HashMap<>();

        lisaa("sana", "word");
    }

    public String hae(String sana) {
        return this.kaannokset.get(sana);
    }

    public void lisaa(String sana, String kaannos) {
        if (!this.kaannokset.containsKey(sana)) {
            this.sanat.add(sana);
        }

        this.kaannokset.put(sana, kaannos);
    }

    public String arvoSana() {
        Random satunnainen = new Random();
        return this.sanat.get(satunnainen.nextInt(this.sanat.size()));
    }
}

Sanakirjan voisi toteuttaa myös niin, että sanan arpominen loisi aina uduen listan kaannokset-hajautustaulun avaimista. Tällöin sanat-listalle ei olisi erillistä tarvetta. Tämä vaikuttaisi kuitenkin sovelluksen tehokkuuteen (tai, olisi ainakin vaikuttanut ennen vuosituhannen vaihdetta -- nykyään koneet ovat jo hieman nopeampia..).

Sanojen syöttäminen

Luodaan seuraavaksi sanojen syöttämiseen tarvittava toiminnallisuus. Sanojen syöttämistä varten tarvitsemme viitteen sanakirja-olioon sekä tekstikentät sanalle ja käännökselle. GridPane-asettelu sopii hyvin kenttien asetteluun. Luodaan luokka Syottonakyma, joka tarjoaa metodin getNakyma, joka luo sanojen syöttämiseen tarvittavan näkymän. Metodi palauttaa viitteen Parent-tyyppiseen olioon. Parent on muunmuassa asetteluun käytettävien luokkien yläluokka, joten mitä tahansa asetteluun käytettävää luokkaa voidaan esittää Parent-oliona.

Luokka määrittelee myös käyttöliittymään liittyvän napinpainallustoiminnallisuuden. Kun käyttäjä painaa nappia, sanapari lisätään sanakirjaan. Samalla myös tekstikentät tyhjennetään seuraavan sanan syöttämistä varten.

package sovellus;

import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;

public class Syottonakyma {

    private Sanakirja sanakirja;

    public Syottonakyma(Sanakirja sanakirja) {
        this.sanakirja = sanakirja;
    }

    public Parent getNakyma() {
        GridPane asettelu = new GridPane();

        Label sanaohje = new Label("Sana");
        TextField sanakentta = new TextField();
        Label kaannosohje = new Label("Käännös");
        TextField kaannoskentta = new TextField();

        asettelu.setAlignment(Pos.CENTER);
        asettelu.setVgap(10);
        asettelu.setHgap(10);
        asettelu.setPadding(new Insets(10, 10, 10, 10));

        Button lisaanappi = new Button("Lisää sanapari");

        asettelu.add(sanaohje, 0, 0);
        asettelu.add(sanakentta, 0, 1);
        asettelu.add(kaannosohje, 0, 2);
        asettelu.add(kaannoskentta, 0, 3);
        asettelu.add(lisaanappi, 0, 4);

        lisaanappi.setOnMouseClicked((event) -> {
            String sana = sanakentta.getText();
            String kaannos = kaannoskentta.getText();

            sanakirja.lisaa(sana, kaannos);

            sanakentta.clear();
            kaannoskentta.clear();
        });

        return asettelu;
    }
}

Sanaharjoittelu

Luodaan tämän jälkeen harjoitteluun tarvittava toiminnallisuus. Harjoittelua varten tarvitsemme myös viitteen sanakirja-olioon, jotta voimme hakea harjoiteltavia sanoja sekä tarkastaa käyttäjän syöttämien käännösten oikeellisuuden. Sanakirjan lisäksi tarvitsemme tekstin, jonka avulla kysytään sanaa, sekä tekstikentän, johon käyttäjä voi syöttää käännöksen. Myös tässä GridPane sopii hyvin kenttien asetteluun.

Kullakin hetkellä harjoiteltava sana on luokalla oliomuuttujana. Oliomuuttujaa voi käsitellä ja muuttaa myös tapahtumankäsittelijän yhteyteen määrittelyssä metodissa.

package sovellus;

import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Parent;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;

public class Harjoittelunakyma {

    private Sanakirja sanakirja;
    private String sana;

    public Harjoittelunakyma(Sanakirja sanakirja) {
        this.sanakirja = sanakirja;
        this.sana = sanakirja.arvoSana();
    }

    public Parent getNakyma() {
        GridPane asettelu = new GridPane();

        Label sanaohje = new Label("Käännä sana '" + this.sana + "'");
        TextField kaannoskentta = new TextField();

        asettelu.setAlignment(Pos.CENTER);
        asettelu.setVgap(10);
        asettelu.setHgap(10);
        asettelu.setPadding(new Insets(10, 10, 10, 10));

        Button lisaanappi = new Button("Tarkista");

        Label palaute = new Label("");

        asettelu.add(sanaohje, 0, 0);
        asettelu.add(kaannoskentta, 0, 1);
        asettelu.add(lisaanappi, 0, 2);
        asettelu.add(palaute, 0, 3);

        lisaanappi.setOnMouseClicked((event) -> {
            String kaannos = kaannoskentta.getText();
            if (sanakirja.hae(sana).equals(kaannos)) {
                palaute.setText("Oikein!");
            } else {
                palaute.setText("Väärin! Sanan '" + sana + "' käännös on '" + sanakirja.hae(sana) + "'.");
                return;
            }

            this.sana = this.sanakirja.arvoSana();
            sanaohje.setText("Käännä sana '" + this.sana + "'");
            kaannoskentta.clear();
        });

        return asettelu;
    }
}

Harjoittelusovellus

Harjoittelusovellus sekä nitoo edellä toteutetut luokat yhteen että tarjoaa sovelluksen valikon. Harjoittelusovelluksen rakenne on seuraava.

package sovellus;

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class HarjoitteluSovellus extends Application {

    private Sanakirja sanakirja;

    @Override
    public void init() throws Exception {
        // 1. Luodaan sovelluksen käyttämä sanakirja
        this.sanakirja = new Sanakirja();
    }

    @Override
    public void start(Stage ikkuna) throws Exception {
        // 2. Luodaan näkymät ("alinäkymät")
        Harjoittelunakyma harjoittelunakyma = new Harjoittelunakyma(sanakirja);
        Syottonakyma syottonakyma = new Syottonakyma(sanakirja);

        // 3. Luodaan päätason asettelu
        BorderPane asettelu = new BorderPane();

        // 3.1. Luodaan päätason asettelun valikko
        HBox valikko = new HBox();
        valikko.setPadding(new Insets(20, 20, 20, 20));
        valikko.setSpacing(10);

        // 3.2. Luodaan valikon napit
        Button lisaanappi = new Button("Lisää sanoja");
        Button harjoittelenappi = new Button("Harjoittele");

        // 3.3. Lisätään napit valikkoon
        valikko.getChildren().addAll(lisaanappi, harjoittelenappi);
        asettelu.setTop(valikko);

        // 4. Liitetään alinäkymät nappeihin. Napin painaminen vaihtaa alinäkymää.
        lisaanappi.setOnAction((event) -> asettelu.setCenter(syottonakyma.getNakyma()));
        harjoittelenappi.setOnAction((event) -> asettelu.setCenter(harjoittelunakyma.getNakyma()));

        // 5. Näytetään ensin syöttönäkymä
        asettelu.setCenter(syottonakyma.getNakyma());

        // 6. Luodaan päänäkymä ja asetetaan päätason asettelu siihen
        Scene nakyma = new Scene(asettelu, 400, 300);

        // 7. Näytetään sovellus
        ikkuna.setScene(nakyma);
        ikkuna.show();
    }

    public static void main(String[] args) {
        launch(HarjoitteluSovellus.class);
    }
}

Tässä tehtävässä laadit edellä olevaa materiaalia noudattaen sanojen harjoitteluun tarkoitetun sovelluksen. Sovelluksen tulee käynnistyä kun luokan SanaharjoitteluSovellus main-metodi suoritetaan.

Luo edellistä esimerkkiä noudattaen sanojen harjoitteluun tarkoitettu sovellus. Sanojen harjoitteluun tarkoitetun sovelluksen tulee tarjota kaksi näkymää. Ensimmäisessä näkymässä käyttäjä voi syöttää alkuperäisiä sanoja ja niiden käännöksiä. Toisessa näkymässä käyttäjältä kysytään sanojen käännöksiä. Harjoiteltavat sanat tulee aina arpoa kaikista syötetyistä sanoista.

Käyttöliittymästä tarkemmin. Sanojen syöttämisnäkymän näyttävän napin tekstin tulee olla "Lisää sanoja". Sanojen harjoittelunäkymän näyttävän napin tekstin tulee olla "Harjoittele". Sanoja syötettäessä ensimmäisen tekstikentän tulee olla sana alkuperäiskielellä, ja toisen tekstikentän tulee olla sana käännettynä. Syöttämiseen käytetyn napin tekstin tulee olla "Lisää sanapari". Harjoittelutilassa käyttäjältä kysytään aina sanoja alkuperäiskielellä ja hänen tulee kirjoittaa sanojen käännöksiä. Vastauksen tarkistamiseen käytetyn napin tekstin tulee olla "Tarkista". Jos vastaus on oikein, käyttöliittymässä näytetään teksti "Oikein!". Jos taas vastaus on väärin, käyttöliittymässä näytetään teksti "Väärin!" sekä tieto oikeasta vastausksesta.

 

Sovelluksessa ei ole automaattisia testejä -- palauta tehtävä kun sovellus toimii oikein.

Harjoittelimme aiemmin vaalipuheen luomista Ylen kuntavaalidatan perusteella. Tässä tehtävässä jatketaan kyseisen datan parissa ja luodaan ohjelma, joka pyrkii ennustamaan tuleeko kirjoitetun vaalipuheen perusteella valituksi.

Tehtäväpohjassa on mukana dokumentti vaalidata.csv, joka sisältää osajoukon Ylen tarjoamasta vaalidatasta. Tekstidokumentti on jaettu sarakkeisiin puolipisteiden perusteella. Ensimmäisessä sarakkeessa on tieto siitä, tuliko ehdokas valituksi (0 = ei, 1 = kyllä), toisessa sarakkeessa on perustelu kysymykseen "Miksi juuri sinut kannattaisi valita kunnanvaltuustoon?", ja kolmannessa sarakkeessa on perustelu kysymykseen "Mitä asioita haluat edistää tai ajaa tulevalla vaalikaudella?". Tässä tehtävässä käytetty vaalidatatiedosto on lisensoitu Creative Commons CC BY-SA-lisenssillä alkuperäisen tiedoston tavoin.

Tiedoston ensimmäiset kolme riviä ovat seuraavat:

1;olen aikaansaava ja aktiivinen luottamushenkilö joka pitää kuntalaisten puolia kuuntelen selvitän ja vien asioita eteenpäin hoidan koko sydämelläni kotiseutuni asioita ;edistää elinvointia kunnan tehtävä on huolehtia asukkaidensa hyvinvoinnista haluan tehdä edelleen aloitteita joissa on vahva terveyttä edistävä näkökulma haluan olla edelleen kaikkien kuntalaisten käytettävissä tiedän että kykenen vaikuttamaan asioihin oma alotteisesti sekä viemään kuntalaisten viestiä eteenpäin aina päätöksentekoon saakka tulevalla vaalikaudella meidän on panostettava ympäristönhoitoon jotta kunnassamme viihtyisi ja se houkuttelisi myös uusia asukkaita
0;olen ratkaisukeskeinen yhteistyökykyinen ja avoin henkilö teen aina päätöksiin vaadittavan taustatyön huolellisesti arvostan tasa arvoa ja vastuullisuutta yhteisten asioiden hoidossa ;päätösten pitkäjänteisyyttä helsinkiläisten aitoa kuuntelua kulttuuritarjonnan monipuolisuutta ja saatavuutta tietoisuutta jo olemassaolevasta tapahtumakirjosta koko kaupungin pitää olla turvallinen paikka asua syrjäytyneet nuoret tarvitsevat pikaisesti apua meillä ei ole varaa menettää yhtään sukupolvea kaikki ovat arvokkaita helsinki on koko suomen käyntikortti hyvä talous koulutus kulttuuri ja joukkoliikenne näiden turvaaminen ja säilyminen ovat kaikille tärkeitä
0;uuden salon aikaiset kaksi valtuustokautta ovat tuoneet hyvän perehtyneisyyden ja hyvät verkostot kaupungin asioiden hoitoon nykyinen elämäntilanne antaa hyvin aikaa yhteisten asioiden hoitoon ;yrittäjänä näen kaupungin elinvoimapolitiikan edistämisen ensisijaiseksi toinen tärkeä asia tulevaisuuden kunnassa on sivistys siinä perusasioita ovat tietysti hyvä varhaiskasvatus ja perusopetus ja muu koulutus sivistyksen sektorilla erityisesti lähinnä sydäntäni on kulttuuri ja kirjasto säilytetään kattavat kirjastopalvelut kaupungin resurssit ovat rajalliset ja vastuun kulttuuriasioista hoitaa pääasiassa kolmas sektori kaupungin tuki sellaiselle tasolle että tekijöiden työn ilo säilyy
  

Valinnan ennustajaan luodaan kaksi erillistä ennustustoiminnallisuutta. Ensimmäinen ennustustoiminnallisuus perustuu sanojen lukumäärien laskemiseen, ja toinen mystiseen ennustukseen. Täydennämme myös ohjelman mukana olevaa graafista käyttöliittymää, joka tulee näyttämään tuleeko puheella valituksi.

Valinnan ennustaja, osa 1

Ensimmäinen ennustustoiminnallisuus perustuu sanojen esiintymislukumääriin. Täydennä luokkaa Ennustaja siten, että metodi public int valitaanSanojenLukumaariinPerustuen(String lause) tarkastelee lauseen jokaista sanaa ja laskee kunkin sanan esiintymislukumäärän valintaan johtaneista ja valintaan johtamattomista lauseista.

Mikäli lauseen sanat ovat esiintyneet useammin valintaan johtaneissa lauseissa kuin valintaan johtamattomissa lauseissa, tulee metodin public int valitaanSanojenLukumaariinPerustuen(String lause) palauttaa arvo 1. Mikäli taas lauseen sanat ovat esiintyneet useammin valintaan johtamattomissa lauseissa kuin valintaan johtaneissa lauseissa, tulee metodin public int valitaanSanojenLukumaariinPerustuen(String lause) palauttaa arvo -1. Muulloin metodi palauttaa arvon 0. Sama sana voi esiintyä lauseessa useampaan kertaan -- tällöin sana myös lasketaan useampaan otteeseen.

Täydennä luokkaa muilta osin sopivasti. Tarvitset todennäköisesti muutamia oliomuuttujia, jonka lisäksi metodia public void lisaa(boolean valitaan, String lause) tulee toteuttaa sopivasti. Metodi saa parametrinaan tiedon valinnasta (true = valittu, false=ei valittu) sekä ehdokkaan käyttämän lauseen.

Voit pilkkoa merkkijonon osiin välilyöntien perusteella seuraavasti.

String lause = " olipa   kerran     elämä  ";
String[] palat = lause.trim().split("\\s+");
// palat[0] sisältää merkkijonon olipa
// palat[1] sisältää merkkijonon kerran
// palat[2] sisältää merkkijonon elämä

Alla on esimerkki toivotusta toiminnallisuudesta.

Ennustaja ennustaja = new Ennustaja();
ennustaja.lisaa(true, "minulla on rohkeutta");
ennustaja.lisaa(true, "olen positiivisesti ajatteleva ja yhteistyökykyinen");
ennustaja.lisaa(true, "olen valmis valtuustoon");
ennustaja.lisaa(false, "olen ollut aktiivinen kuntapolitiikassa");
ennustaja.lisaa(false, "olen ollut yhteiskunnan asioissa aina aktiivinen toimija");
ennustaja.lisaa(false, "haluan olla valtuustossa");

System.out.println(ennustaja.valitaanSanojenLukumaariinPerustuen("haluan valtuustoon"));
System.out.println(ennustaja.valitaanSanojenLukumaariinPerustuen("olen aktiivinen toimija"));
System.out.println(ennustaja.valitaanSanojenLukumaariinPerustuen("olen yhteistyökykyinen"));
0
-1
1

Tarkastellaan vielä miten ennusteet muodostuvat. Yllä olevassa esimerkissä lisaa-kutsujen jälkeen ohjelman sisäinen sanakohtainen tieto valinnasta on seuraava.

Sana Esiintymisiä valintaan johtaneissa lauseissa Esiintymisiä valintaan johtamattomissa lauseissa
aina 0 1
ajatteleva 1 0
aktiivinen 0 1
asioissa 0 1
haluan 0 1
ja 1 0
kuntapolitiikassa 0 1
minulla 1 0
on 1 0
olen 2 2
ollut 0 2
positiivisesti 1 0
rohkeutta 1 0
toimija 0 1
yhteiskunnan 0 1
yhteistyökykyinen 1 0
valmis 1 0
valtuustoon 1 0
valtuustossa 0 1

Nyt kutsu ennustaja.valitaanSanojenLukumaariinPerustuen("haluan valtuustoon") laskee sanojen "haluan" ja "valtuustoon" esiintymislukumäärät valintaan johtaneista sekä ei valintaan johtaneista lauseista. Sana "haluan" ei ole esiintynyt kertaakaan valintaan johtaneissa lauseissa ja kerran kerran valintaan johtamattomissa lauseissa. Sana "valtuustoon" on esiintynyt kerran valintaan johtaneissa lauseissa ja ei kertaakaan valintaan johtamattomissa lauseissa. Sekä valintaan johtamattomien että valintaan johtaneiden sanojen summa lauseessa on tässä 1, eli ennustaja ei suosittele kumpaakaan. Metodi palauttaa arvon 0.

Huom! Vaikka yllä tieto esitetään taulukkona, kannattanee harkita ainakin yhden hajautustaulun käyttöä luokan Ennustaja sisäisen tilan esittämiseen.

Graafinen käyttöliittymä

Sovelluksen graafinen käyttöliittymä näyttää tällä hetkellä seuraavalta.

Tyhjä ikkuna, jossa on tekstikenttä.

 

Muokkaa käyttöliittymää siten, että lisäät tekstikentän alapuolelle VBox-elementin, joka sisältää kaksi Label-elementtiä. Ensimmäisen Label-elementin tulee sisältää teksti "Sanojen esiintymien perusteella: ???" ja toisen Label-elementin tulee sisältää teksti "Mystisen ennustajan perusteella: ???". Saat VBox-elementin metodilla setPadding asetettua elementille "ilmaa" reunoille.

Muokkauksen jälkeen sovelluksen tulee näyttää seuraavalta.

Tyhjä ikkuna, jossa on tekstikenttä. Tekstikentän alapuolella on odotetut merkkijonot.

 

Täydennä tämän jälkeen käyttöliittymää siten, että sovelluksen käynnistyksen yhteydessä graafiseen käyttöliittymään luodaan Ennustaja-olio, ja ennustajaoliolle lisätään tehtäväpohjassa olevasta vaalidata.csv-tiedostosta rivit.

Käytä ennustajalle syötettävinä lauseina vastausta kysymykseen "Miksi juuri sinut kannattaisi valita kunnanvaltuustoon?". Saat tiedoston rivit pilkottua osiin komennolla rivi.split(";") -- metodin palauttaman taulukon indeksissä 0 on tieto valinnasta (1 = valittu, 0 = ei valittu), ja indeksissä 1 vastaus kysymykseen "Miksi juuri sinut kannattaisi valita kunnanvaltuustoon?".

Muokkaa ohjelmaa lopulta siten, että tekstin "Sanojen esiintymien perusteella: ???" sisältämän tekstikentän arvo muuttuu lennossa kun käyttäjä kirjoittaa puhetta. Mikäli ennustajan palauttama arvo on "0" on ennustus "???", mikäli arvo on "1" on ennustus "Kyllä", mikäli arvo on "-1", on ennustus "Ei". Kun tehtävän tämä osa on valmis, sovellus toimii seuraavasti.

Käyttöliittymä näyttää ennusteen kirjoitetun tekstin perusteella.

 

Käyttöliittymä näyttää nyt jonkinlaisen reaaliaikaisen ennustuksen vaalipuheen toimivuudesta.

Valinnan ennustaja, osa 2

Tehdään toinen ennustaja. Kukin sana esiintyy jollain todennäköisyydellä valituiksi johtaneissa lauseissa ja jollain todennäköisyydellä lauseissa, jotka eivät johtaneet valintaan. Tämä "tietty todennäköisyys" tai "jokin todennäköisyys" voidaan laskea havainnoista eli aiemmasta datasta.

Tarkastellaan tätä seuraavan esimerkkidatan perusteella. Datan ensimmäinen merkki kuvaa valintaa (1 = kyllä, 0 = ei) ja seuraavat sanoja. Datassa on yhteensä yhdeksän riviä, joista ensimmäiset neljä johtavat valintaan ja seuraavat viisi eivät johda valintaan. Valintaa seuraa merkkejä -- erilaisia merkkejä on esimerkissä yhteensä 4 (a, b, c ja d).

1 a b
1 a c d
1 b c
1 a a
0 c c d
0 a b d
0 a b
0 c d
0 c d d d

Seuraava taulukko kuvaa kunkin merkin esiintymislukumääriä valintaan johtaneissa ja valintaan johtamattomissa lauseissa.

Merkki Valittu Ei valittu
a 4 2
b 2 2
c 2 4
d 1 6

Tarkastellaan dataa.

Nopeasti katsoen vastaus kysymykseen "Millä todennäköisyydellä merkki 'c' johtaa valintaan?" on (merkin 'c' esiintymislukumäärät valintaan johtaneissa lauseissa / merkin 'c' esiintymislukumäärät yhteensä), eli 2/6, eli noin 33.33%. Vastaavasti vastaus kysymykseen "Millä todennäköisyydellä merkki 'd' ei johda valintaan?" on (merkin 'd' esiintymislukumäärät valintaan johtamattomissa lauseissa / merkin 'd' esiintymislukumäärät yhteensä), eli 6/7, eli noin 85.71%.

Entäpä kysymys "Millä todennäköisyydellä merkit 'a' ja 'd' johtavat valintaan?". Oletetaan, että merkit ovat riippumattomia toisistaan (lisää aiheesta Wikipedian artikkelissa todennäköisyydestä), jolloin voimme tarkastella kysymystä muodossa "Millä todennäköisyydellä merkki 'a' johtaa valintaan, ja, millä todennäköisyydellä merkki 'd' johtaa valintaan?", eli todennäköisyys "Millä todennäköisyydellä merkki 'a' johtaa valintaan" kerrottuna todennäköisyydellä "Millä todennäköisyydellä merkki 'd' johtaa valintaan?". Tämä olisi 4/6 * 1/7 eli noin 9.52%.

Asia ei ole kuitenkaan ihan näin yksinkertainen. Yllä oletamme, että valinnan todennäköisyys on aina fifti-fifti, eli joka toinen tulee valituksi ja joka toinen ei tule valituksi. Tämä ei kuitenkaan pidä paikkaansa. Datassa neljä riviä johti valintaan ja viisi riviä ei johtanut valintaan. Valinnan todennäköisyys riippumatta käytetyistä merkeistä tulee ottaa myös huomioon.

Tarkastellaan nyt kysymystä "Millä todennäköisyydellä merkki 'b' johtaa valintaan?". Merkki 'b' esiintyy valituissa ja valitsematta jääneissä lauseissa yhtä usein, eli kaksi kertaa neljästä. Valinnan todennäköisyys on datassa olevien rivien perusteella neljä yhdeksästä, eli noin 44.44%. Nyt merkin 'b' johtaminen valintaan on 4/9 * 2/4, eli noin 22.22%. Vastaavasti todennäköisyys sille, että merkki 'b' johtaa valitsematta jäämiseen on noin 5/9 * 2/4 eli 27.78%.

Kun haluamme tarkastella johtavatko tietyt merkit valintaan vai johtavatko ne valitsematta jäämiseen, tarkastelemme kummankin vaihtoehdon todennäköisyyttä ja valitsemme niistä suuremman.

Sama ajatus toimii merkkien lisäksi myös lauseilla. Ohjelmallisesti mystinen algoritmi on seuraavanlainen.

double valitaan = valintaan johtaneiden lauseiden lukumäärä / lauseita yhteensä;
double hylataan = valintaan johtamattomien lauseiden lukumäärä / lauseita yhteensä;

jokaiselle sanalle sana annetussa lauseessa:

    jos sanan sana esiintymiä on alle 5, jätä sana huomiotta

    valitaan = valitaan * sanan sana esiintymiskerrat valituissa lauseissa / sanan esiintymiskerrat yhteensä

    hylataan = hylataan * sanan sana esiintymiskerrat ei valituissa lauseissa / sanan esiintymiskerrat yhteensä


jos valitaan on suurempi kuin hylataan, palauta arvo 1
jos valitaan on pienempi kuin hylataan, palauta arvo -1
muulloin, palauta arvo 0

Toteuta metodi public int valitaanMystisellaEnnustajalla(String lause) edellisen kuvauksen perusteella. Joudut mahdollisesti myös täydentämään luokkaan määriteltyjä oliomuuttujia sekä muokkaamaan metodia public void lisaa(boolean valitaan, String lause).

Voit kokeilla ohjelmasi toimintaa seuraavalla esimerkillä. Alla olevassa esimerkissä rajaus "jos sanan sana esiintymiä on alle 5, jätä sana huomiotta" on poissa. Palautettavassa versiossa rajauksen tulee kuitenkin esiintyä.

Ennustaja ennustaja = new Ennustaja();
ennustaja.lisaa(true, "a b");
ennustaja.lisaa(true, "a c d");
ennustaja.lisaa(true, "b c");
ennustaja.lisaa(true, "a a");
ennustaja.lisaa(false, "c c d");
ennustaja.lisaa(false, "a b d");
ennustaja.lisaa(false, "a b");
ennustaja.lisaa(false, "c d");
ennustaja.lisaa(false, "c d d d");

System.out.println(ennustaja.valitaanSanojenLukumaariinPerustuen("olen aktiivinen toimija"));
System.out.println(ennustaja.valitaanSanojenLukumaariinPerustuen(""));
System.out.println(ennustaja.valitaanSanojenLukumaariinPerustuen("a"));
System.out.println(ennustaja.valitaanSanojenLukumaariinPerustuen("b"));
System.out.println(ennustaja.valitaanSanojenLukumaariinPerustuen("a b c"));

System.out.println();

System.out.println(ennustaja.valitaanMystisellaEnnustajalla("a"));
System.out.println(ennustaja.valitaanMystisellaEnnustajalla("b"));
System.out.println(ennustaja.valitaanMystisellaEnnustajalla("a b c"));
0
0
1
0
0

1
-1
-1

Muokkaa lopulta vielä graafista käyttöliittymää siten, että graafinen käyttöliittymä näyttää myös mystisen ennustajan tarjoaman ehdotuksen. Mikäli metodi valitaanMystisellaEnnustajalla palauttaa arvon 1, tulee tekstin olla "Mystisen ennustajan perusteella: Kyllä". Mikäli arvo on -1, tulee tekstin olla "Mystisen ennustajan perusteella: Ei". Muulloin tekstin tulee olla "Mystisen ennustajan perusteella: ???".

Ennustusmekanismi ei vieläkään ole kovin hyvä. Ohjelma ei esimerkiksi osaa käsitellä pilkkuja, pisteitä, tai muita erikoismerkkejä. Ohjelma käsittelee myös sanat kuten "kuntavaali" ja "kuntavaalit" täysin erillisinä sanoina. Edellä käsitelty "mystinen ennustaja" tulee tarkemmin tutuksi mm. tekoälyn johdatuskursseilla, missä ongelmaan opitaan myös parempia menetelmiä. Aiheesta opitaan lisää myös muunmuassa luonnollisen kielen käsittelyyn liittyvillä kursseilla sekä laajemmin digitaalisten ihmistieteiden tutkimuksen parissa.

Sisällysluettelo