Sisällysluettelo

Tehtävät

Kysely: Minäpystyvyys

Tutkimus

Käytämme tietoja kurssiemme kehittämiseen ja opetuksen tutkimukseen.

Tällä kyselyllä kartoitetaan erilaisissa tilanteissa selviytymistä. Jos olet sitä mieltä, että väite pätee sinuun erittäin hyvin, valitse 7; jos olet sitä mieltä, että väite ei päde sinuun lainkaan, valitse 1. Jos väite on enemmän tai vähemmän totta kohdallasi, valitse sinua parhaiten kuvaava arvo lukujen 1 ja 7 väliltä.

 

Väite ei päde minuun lainkaan.
1
2
3
4
5
6
7
Väite pätee minuun erittäin hyvin.

 

52 Tiedostoon kirjoittaminen

Opimme aiemmin, että tekstitiedostojen lukeminen onnistuu Scanner- ja File-luokkien avulla. Luokka FileWriter tarjoaa toiminnallisuuden tiedostoon kirjoittamiseen. Luokan FileWriter konstruktorille annetaan parametrina kohdetiedoston sijaintia kuvaava merkkijono.

FileWriter kirjoittaja = new FileWriter("tiedosto.txt");
kirjoittaja.write("Hei tiedosto!\n"); // rivinvaihto tulee myös kirjoittaa tiedostoon!
kirjoittaja.write("Lisää tekstiä\n");
kirjoittaja.write("Ja vielä lisää");
kirjoittaja.close(); // sulkemiskutsu sulkee tiedoston ja varmistaa että kirjoitettu teksti menee tiedostoon

Esimerkissä kirjoitetaan tiedostoon "tiedosto.txt" merkkijono "Hei tiedosto!", jota seuraa rivinvaihto, ja vielä hieman lisää tekstiä. Huomaa että tiedostoon kirjoitettaessa metodi write ei lisää rivinvaihtoja, vaan ne tulee lisätä itse.

Sekä FileWriter-luokan konstruktori että write-metodi heittää mahdollisesti poikkeuksen, joka tulee joko käsitellä tai siirtää kutsuvan metodin vastuulle. Metodi, jolle annetaan parametrina kirjoitettavan tiedoston nimi ja kirjoitettava sisältö voisi näyttää seuraavalta.

public class Tallentaja {

    public void kirjoitaTiedostoon(String tiedostonNimi, String teksti) throws Exception {
        FileWriter kirjoittaja = new FileWriter(tiedostonNimi);
        kirjoittaja.write(teksti);
        kirjoittaja.close();
    }
}

Yllä olevassa kirjoitaTiedostoon-metodissa luodaan ensin FileWriter-olio, joka kirjoittaa parametrina annetussa sijainnissa sijaitsevaan tiedostoon tiedostonNimi. Tämän jälkeen kirjoitetaan tiedostoon write-metodilla. Konstruktorin ja write-metodin mahdollisesti heittämä poikkeus tulee käsitellä joko try-catch-lohkolla tai siirtämällä poikkeuksen käsittelyvastuuta eteenpäin. Metodissa kirjoitaTiedostoon käsittelyvastuu on siirretty eteenpäin.

Luodaan main-metodi jossa kutsutaan Tallentaja-olion kirjoitaTiedostoon-metodia. Poikkeusta ei ole pakko käsitellä main-metodissakaan, vaan se voi ilmoittaa heittävänsä mahdollisesti poikkeuksen määrittelyllä throws Exception.

public static void main(String[] args) throws Exception {
    Tallentaja tallentaja = new Tallentaja();
    tallentaja.kirjoitaTiedostoon("paivakirja.txt", "Rakas päiväkirja, tänään oli kiva päivä.");
}

Yllä olevaa metodia kutsuttaessa luodaan tiedosto "paivakirja.txt" johon kirjoitetaan teksti "Rakas päiväkirja, tänään oli kiva päivä.". Jos tiedosto on jo olemassa, pyyhkiytyy vanhan tiedoston sisältö uutta kirjoittaessa. FileWriter-luokan avulla voidaan lisätä olemassaolevan tiedoston perään tekstiä antamalla sen konstruktorille ylimääräinen parametri boolean append, jolloin olemassaolevaa tekstiä ei poisteta. Lisätään Tallentaja-luokalle metodi lisaaTiedostoon(), joka lisää parametrina annetun tekstin tiedoston loppuun.

public class Tallentaja {
    public void kirjoitaTiedostoon(String tiedostonNimi, String teksti) throws Exception {
        FileWriter kirjoittaja = new FileWriter(tiedostonNimi);
        kirjoittaja.write(teksti);
        kirjoittaja.close();
    }

    public void lisaaTiedostoon(String tiedostonNimi, String teksti) throws Exception {
        FileWriter kirjoittaja = new FileWriter(tiedostonNimi, true);
        kirjoittaja.write(teksti);
        kirjoittaja.close();
    }
}

Useimmiten tiedoston perään metodilla append kirjoittamisen sijasta on helpompi kirjoittaa koko tiedosto uudelleen.

Tehtävä 159: Tiedostonkäsittelijä

Saat tehtävärungon mukana luokan Tiedostonkasittelija joka sisältää metodirungot tiedoston lukemista ja tiedostoon kirjoittamista varten.

159.1 Tiedoston lukeminen

Täydennä metodi public ArrayList<String> lue(String tiedosto) sellaiseksi, että se palauttaa parametrina annetun tekstitiedoston sisältämät rivit ArrayList:ina siten, että tiedoston jokainen rivi on omana merkkijononaan listalla.

Projektissa on testaamista varten kaksi tekstitiedostoa src/koesyote1.txt ja src/koesyote2.txt. Metodia on tarkoitus käyttää seuraavalla tavalla:

public static void main(String[] args) throws FileNotFoundException, IOException {
    TiedostonKasittelija t = new TiedostonKasittelija();

    for (String rivi : t.lue("src/koesyote1.txt")) {
        System.out.println(rivi);
    }
}

Tulostuksen pitäisi olla

eka
toka

159.2 Rivin kirjoittaminen tiedostoon

Täydennä projektipohjan mukana tuleva metodi public void tallenna(String tiedosto, String teksti) sellaiseksi, että se tallentaa ensimmäisen parametrin määrittelemään tiedostoon toisena parametrina annetun merkkijonon. Jos tiedosto on jo olemassa, kirjoitetaan vanhan version päälle.

159.3 Rivin kirjoittaminen tiedostoon

Täydennä projektipohjan mukana tuleva metodi public void tallenna(String tiedosto, ArrayList<String> tekstit) sellaiseksi, että se tallentaa ensimmäisen parametrin määrittelemään tiedostoon toisena parametrina annetun listan siten, että jokainen merkkijono tulee omalle rivilleen. Jos tiedosto on jo olemassa, kirjoitetaan vanhan version päälle.

Tehtävä 160: Muistava kahteen suuntaan kääntävä sanakirja

Tässä tehtävässä laajennetaan aiemmin toteutettua sanakirjaa siten, että sanat voidaan lukea tiedostosta ja kirjoittaa tiedostoon. Sanakirjan tulee myös osata kääntää molempiin suuntiin, suomesta vieraaseen kieleen sekä toiseen suuntaan (tehtävässä oletetaan hieman epärealistisesti, että suomen kielessä ja vieraassa kielessä ei ole yhtään samalla tavalla kirjoitettavaa sanaa). Tehtävänäsi on luoda sanakirja luokkaan MuistavaSanakirja. Toteuta luokka pakkaukseen sanakirja.

160.1 Muistiton perustoiminnallisuus

Tee sanakirjalle parametriton konstruktori sekä metodit:

  • public void lisaa(String sana, String kaannos)
  • lisää sanan sanakirjaan. Jokaisella sanalla on vain yksi käännös ja jos sama sana lisätään uudelleen, ei tapahdu mitään.
  • public String kaanna(String sana)
  • palauttaa käännöksen annetulle sanalle. Jos sanaa ei tunneta, palautetaan null.

Sanakirjan tulee tässä vaiheessa toimia seuraavasti:

MuistavaSanakirja sanakirja = new MuistavaSanakirja();
sanakirja.lisaa("apina", "monkey");
sanakirja.lisaa("banaani", "banana");
sanakirja.lisaa("apina", "apfe");

System.out.println(sanakirja.kaanna("apina"));
System.out.println(sanakirja.kaanna("monkey"));
System.out.println(sanakirja.kaanna("ohjelmointi"));
System.out.println(sanakirja.kaanna("banana"));

Tulostuu

monkey
apina
null
banaani

Kuten tulostuksesta ilmenee, käännöksen lisäämisen jälkeen sanakirja osaa tehdä käännöksen molempiin suuntiin.

Huom: metodit lisaa ja kaanna eivät lue tiedostoa tai kirjoita tiedostoon! Myöskään konstruktori ei koske tiedostoon.

160.2 Sanojen poistaminen

Lisää sanakirjalle metodi public void poista(String sana) joka poistaa annetun sanan ja sen käännöksen sanakirjasta.

Kannattanee kerrata aiemmilta viikoilta materiaalia, mikä liittyy olioiden poistamiseen ArrayListista.

HUOM2: metodi poista ei kirjoita tiedostoon.

Sanakirjan tulee tässä vaiheessa toimia seuraavasti:

MuistavaSanakirja sanakirja = new MuistavaSanakirja();
sanakirja.lisaa("apina", "monkey");
sanakirja.lisaa("banaani", "banana");
sanakirja.lisaa("ohjelmointi", "programming");
sanakirja.poista("apina");
sanakirja.poista("banana");

System.out.println(sanakirja.kaanna("apina"));
System.out.println(sanakirja.kaanna("monkey"));
System.out.println(sanakirja.kaanna("banana"));
System.out.println(sanakirja.kaanna("banaani"));
System.out.println(sanakirja.kaanna("ohjelmointi"));

Tulostuu

null
null
null
null
programming

Poisto siis toimii myös molemmin puolin, alkuperäisen sanan tai sen käännöksen poistamalla, poistuu sanakirjasta tieto molempien suuntien käännöksestä.

160.3 Lataaminen tiedostosta

Tee sanakirjalle konstruktori public MuistavaSanakirja(String tiedosto) ja metodi public boolean lataa(), joka lataa sanakirjan konstruktorin parametrina annetun nimisestä tiedostosta. Jos tiedoston avaaminen tai lukeminen ei onnistu, palauttaa metodi false ja muuten true.

Huom: parameterillinen konstruktori ainoastaan kertoo sanakirjalle käytetävän tiedoston nimen. Konstruktori ei lue tiedostoa, tiedoston lukeminen tapahtuu ainoastaan metodissa lataa.

Sanakirjatiedostossa yksi rivi sisältää sanan ja sen käännöksen merkillä ":" erotettuna. Tehtäväpohjan mukana tuleva testaamiseen tarkoitettu sanakirjatiedosto src/sanat.txt on sisällöltään seuraava:

apina:monkey
alla oleva:below
olut:beer

Lue sanakirjatiedosto rivi riviltä lukijan metodilla nextLine. Voit pilkkoa rivin String metodilla split seuraavasti:

Scanner tiedostonLukija = new ...
while (tiedostonLukija.hasNextLine()) {
    String rivi = tiedostonLukija.nextLine();
    String[] osat = rivi.split(":");   // pilkotaan rivi :-merkkien kohdalta

    System.out.println(osat[0]);     // ennen :-merkkiä ollut osa rivistä
    System.out.println(osat[1]);     // :-merkin jälkeen ollut osa rivistä
}

Sanakirjaa käytetään seuraavasti:

MuistavaSanakirja sanakirja = new MuistavaSanakirja("src/sanat.txt");
boolean onnistui = sanakirja.lataa();

if (onnistui) {
  System.out.println("sanakirjan lataaminen onnistui");
}

System.out.println(sanakirja.kaanna("apina"));
System.out.println(sanakirja.kaanna("ohjelmointi"));
System.out.println(sanakirja.kaanna("alla oleva"));

Tulostuu

sanakirjan lataaminen onnistui
monkey
null
below

160.4 Tallennus tiedostoon

Tee sanakirjalle metodi public boolean tallenna(), jota kutsuttaessa sanakirjan sisältö kirjoitetaan konstruktorin parametrina annetun nimiseen tiedostoon. Jos tallennus ei onnistu, palauttaa metodi false ja muuten true. Sanakirjatiedostot tulee tallentaa ylläesitellyssä muodossa, eli ohjelman on osattava lukea itse kirjoittamiaan tiedostoja.

Huom1: mikään muu metodi kuin tallenna ei kirjoita tiedostoon. Jos teit edelliset kohdat oikein, sinun ei tulisi tarvita muuttaa mitään olemassaolevaa koodia.

Huom2: vaikka sanakirja osaa käännökset molempiin suuntiin, ei sanakirjatiedostoon tule kirjoittaa kuin toinen suunta. Eli jos sanakirja tietää esim. käännöksen tietokone = computer, tulee rivi:

tietokone:computer

tai rivi

computer:tietokone

mutta ei molempia!

Talletus kannattanee hoitaa siten, että koko käännöslista kirjoitetaan uudelleen vanhan tiedoston päälle, eli materiaalissa esiteltyä append-metodia ei kannata käyttää.

Sanakirjan lopullista versiota on tarkoitus käyttää seuraavasti:

MuistavaSanakirja sanakirja = new MuistavaSanakirja("src/sanat.txt");
sanakirja.lataa();

// käytä sanakirjaa

sanakirja.tallenna();

Eli käytön aluksi ladataan sanakirja tiedostosta ja lopussa tallennetaan se takaisin tiedostoon jotta sanakirjaan tehdyt muutokset pysyvät voimassa seuraavallekin käynnistyskerralle.

53 Käyttöliittymät


Huom! Osa käyttöliittymätehtävien testeistä avaa käyttöliittymän ja käyttää hiirtäsi käyttöliittymäkomponenttien klikkailuun. Kun suoritat käyttöliittymätehtävien testejä, älä käytä hiirtäsi!


Ohjelmamme ovat tähän mennessä koostuneet lähinnä sovelluslogiikasta ja sovelluslogiikkaa käyttävästä tekstikäyttöliittymästä. Muutamissa tehtävissä on ollut myös graafinen käyttöliittymä, mutta ne on yleensä luotu puolestamme. Tutustutaan seuraavaksi graafisten käyttöliittymien luomiseen Javalla.

Käyttöliittymät ovat ikkunoita, jotka sisältävät erilaisia osia kuten nappeja, tekstikenttiä ja valikkoja. Käyttöliittymien ohjelmoinnissa käytetään Javan Swing-komponenttikirjastoa, joka tarjoaa luokkia käyttöliittymäkomponenttien luomiseen ja käsittelyyn.

Käyttöliittymien peruselementti on luokka JFrame, jonka sisältämään komponenttiosioon käyttöliittymäkomponentit luodaan. Oikeaoppisesti luodut käyttöliittymät toteuttavat rajapinnan Runnable, ja ne käynnistetään pääohjelmasta. Käytämme kurssilla seuraavanlaista käyttöliittymärunkoa:

import java.awt.Container;
import java.awt.Dimension;
import javax.swing.JFrame;
import javax.swing.WindowConstants;

public class Kayttoliittyma implements Runnable {

    private JFrame frame;

    public Kayttoliittyma() {
    }

    @Override
    public void run() {
        frame = new JFrame("Otsikko");
        frame.setPreferredSize(new Dimension(200, 100));

        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        luoKomponentit(frame.getContentPane());

        frame.pack();
        frame.setVisible(true);
    }

    private void luoKomponentit(Container container) {
    }

    public JFrame getFrame() {
        return frame;
    }
}

Tarkastellaan ylläolevan käyttöliittymäluokan koodia hieman tarkemmin.

public class Kayttoliittyma implements Runnable {

Luokka Kayttoliittyma toteuttaa Javan rajapinnan Runnable, joka tarjoaa mahdollisuuden säikeistettyyn ohjelman suorittamiseen. Säikeistetyllä suorittamisella voidaan suorittaa useita ohjelman osia rinnakkain. Emme tutustu säikeisiin tarkemmin, lisää tietoa säikeistä tulee muunmuassa toisen vuoden kurssilla Käyttöjärjestelmät.

private JFrame frame;

Käyttöliittymä sisältää oliomuuttujana JFrame-olion, joka on näkyvän käyttöliittymän pohjaelementti. Kaikki käyttöliittymäkomponentit lisätään JFrame-olion sisältämään komponenttialueeseen. Huomaa että oliomuuttujia ei saa alustaa metodien ulkopuolella. Esimerkiksi oliomuuttujan JFrame alustus luokkamäärittelyssä "private JFrame frame = new JFrame()" kiertää käyttöliittymäsäikeiden suoritusjärjestyksen, ja voi johtaa ydintuhoon. Tai ohjelmasi kaatumiseen.

@Override
public void run() {
    frame = new JFrame("Otsikko");
    frame.setPreferredSize(new Dimension(200, 100));

    frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

    luoKomponentit(frame.getContentPane());

    frame.pack();
    frame.setVisible(true);
}

Rajapinta Runnable määrittelee metodin public void run(), joka jokaisen Runnable-rajapinnan toteuttajan tulee toteuttaa. Metodissa public void run() luodaan ensin uusi JFrame-ikkuna, jonka otsikoksi asetetaan "Otsikko". Tämän jälkeen asetetaan ikkunan toivotuksi kooksi 200, 100 eli ikkunan leveydeksi tulee 200 pikseliä, korkeudeksi 100 pikseliä. Komento frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); kertoo JFrame-oliolle, että käyttöliittymän käynnistäneen ohjelman suoritus tulee lopettaa, kun käyttäjä painaa käyttöliittymässä olevaa ruksia.

Tämän jälkeen kutsutaan luokassa myöhemmin määriteltyä metodia luoKomponentit. Metodille annetaan parametrina JFrame-olion Container-olio, johon voi lisätä käyttöliittymäkomponentteja.

Lopuksi kutsutaan metodia frame.pack(), joka asettaa JFrame-olion aiemmin määritellyn kokoiseksi ja järjestää JFrame-olion sisältämän Container-olion sisällä olevat käyttöliittymäkomponentit. Lopuksi kutsutaan metodia frame.setVisible(true), joka näyttää käyttöliittymän käyttäjälle.

private void luoKomponentit(Container container) {
}

Metodissa luoKomponentit lisätään JFrame-olion sisältämään komponenttialueeseen käyttöliittymäkomponentteja. Esimerkissämme ei ole yhtäkään käyttöliittymäkomponenttia JFrame-ikkunan lisäksi. Luokalla Kayttoliittyma on myös sen käyttöä helpottava metodi getFrame, jolla päästään käsiksi luokan kapseloimaan JFrame-olioon.

Swing-käyttöliittymät käynnistetään SwingUtilities-luokan tarjoaman invokeLater-metodin avulla. Metodi invokeLater saa parametrinaan Runnable-rajapinnan toteuttavan olion. Metodi asettaa Runnable-olion suoritusjonoon, ja kutsuu sitä kun ehtii. Luokan SwingUtilities avulla voimme käynnistää uusia säikeitä tarvittaessa.

import javax.swing.SwingUtilities;

public class Main {

    public static void main(String[] args) {
        Kayttoliittyma kayttoliittyma = new Kayttoliittyma();
        SwingUtilities.invokeLater(kayttoliittyma);
    }
}

Kun ylläoleva pääohjelmametodi suoritetaan, näemme luokassa Kayttoliittyma määrittellyn käyttöliittymän.

53.1 Käyttöliittymäkomponentit

Käyttöliittymä koostuu taustaikkunan (JFrame) sisältämästä komponenttipohjasta (Container), ja siihen asetetuista käyttöliittymäkomponenteista. Käyttöliittymäkomponentteja ovat erilaiset painikkeet, tekstit ym. Jokaiselle komponentille on oma luokka. Kannattaa tutustua Oraclen kuvasarjaan erilaisista komponenteista osoitteessa http://docs.oracle.com/javase/tutorial/uiswing/components/index.html.

Teksti

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

Muokataan käyttöliittymäpohjaamme siten, että siinä näkyy tekstiä. Luodaan uusi JLabel-tekstikomponentti metodissa luoKomponentit. Tämän jälkeen lisätään se JFrame-oliolta saatuun Container-olioon add-metodia käyttäen.

private void luoKomponentit(Container container) {
    JLabel teksti = new JLabel("Tekstikenttä!");
    container.add(teksti);
}

Kuten yllä olevasta lähdekoodista näemme, JLabel-käyttöliittymäkomponentti tulee näyttämään tekstin "Tekstikenttä!". Kun suoritamme käyttöliittymän, näemme seuraavanlaisen ikkunan.

Tehtävä 161: Tervehtijä

Toteuta käyttöliittymä, joka näyttää tekstin "Moi!". Käyttöliittymän (eli JFrame-olion) leveyden tulee olla vähintään 400 ja korkeuden vähintään 200 ja otsikkona teksti "Swing on". Tehtävä tulee toteuttaa tehtäväpohjassa tulevaan käyttöliittymärunkoon. JFrame-olion luominen ja näkyväksi asettamisen tulee tapahtua metodissa run(), tekstikomponentti lisätään käyttöliittymälle metodissa luoKomponentit(Container container).

HUOM: Käyttöliittymien oliomuuttujia saa alustaa vain metodeissa tai konstruktorissa! Älä alusta oliomuuttujia suoraan määrittelyn yhteydessä.

Painikkeet

Käyttöliittymään saa painikkeita JButton-luokan avulla. JButton-olion lisääminen käyttöliittymään tapahtuu aivan kuin JLabel-olion lisääminen.

    private void luoKomponentit(Container container) {
        JButton nappi = new JButton("Click!");
        container.add(nappi);
    }

Yritetään seuraavaksi lisätä käyttöliittymään sekä tekstiä, että nappi.

private void luoKomponentit(Container container) {
    JButton nappi = new JButton("Click!");
    container.add(nappi);
    JLabel teksti = new JLabel("Tekstiä.");
    container.add(teksti);
}

Ohjelmaa suorittaessa näemme seuraavanlaisen käyttöliittymän.

Vain viimeiseksi lisätty käyttöliittymäkomponentti on näkyvillä, eikä ohjelma toimi toivotusti. Mistä tässä oikein on kyse?

53.2 Käyttöliittymäkomponenttien asettelu

Jokaisella käyttöliittymäkomponentilla on oma sijainti käyttöliittymässä. Komponentin sijainnin määrää käytössä oleva käyttöliittymän asettelija (Layout Manager). Yrittäessämme aiemmin lisätä useampia käyttöliittymäkomponentteja Container-olioon käyttöliittymässä oli vain yksi komponentti näkyvillä. Jokaisessa Container-oliossa on oletuksena käyttöliittymäasettelija BorderLayout.

BorderLayout asettelee käyttöliittymäkomponentit viiteen alueeseen: käyttöliittymän keskikohdan lisäksi käytössä ovat ilmansuunnat. Voimme antaa Container-olion add-metodille ylimääräisenä parametrina lisätoiveen kohdasta, johon haluamme asettaa käyttöliittymäkomponentin. BorderLayout-luokassa on käytössä luokkamuuttujat BorderLayout.NORTH, BorderLayout.EAST, BorderLayout.SOUTH, BorderLayout.WEST, ja BorderLayout.CENTER.

Käytettävä käyttöliittymäasettelija asetetaan Container-oliolle metodin setLayout-parametrina. Metodille add voidaan antaa käyttöliittymäkomponentin lisäksi paikka, johon komponentti lisätään. Alla on esimerkki, jossa jokaiseen BorderLayoutin tarjoamaan paikkaan asetetaan käyttöliittymäkomponentti.

private void luoKomponentit(Container container) {
    // seuraava rivi siis ei tässä tilanteessa pakollinen, sillä BorderLayout on JFramessa joka tapauksessa oletuksena
    container.setLayout(new BorderLayout());

    container.add(new JButton("Pohjoinen (North)"), BorderLayout.NORTH);
    container.add(new JButton("Itä (East)"), BorderLayout.EAST);
    container.add(new JButton("Etelä (South)"), BorderLayout.SOUTH);
    container.add(new JButton("Länsi (West)"), BorderLayout.WEST);
    container.add(new JButton("Keski (Center)"), BorderLayout.CENTER);

    container.add(new JButton("Oletuspaikka (Center)"));
}

Huomaa, että nappi "Keski (Center)" ei tule näkymään käyttöliittymässä, sillä nappi "Oletuspaikka (Center)" asetetaan oletuksena sen paikalle. Käyttöliittymässäpohjassa yllä oleva koodi näyttää seuraavalta.

Kuten käyttöliittymäkomponentteja, myös käyttöliittymän asettelijoita on useita. Oraclella on käyttöliittymäasettelijoihin visuaalinen opas osoitteessa http://docs.oracle.com/javase/tutorial/uiswing/layout/visual.html. Tutustutaan seuraavaksi käyttöliittymäasettelijaan BoxLayout.

BoxLayout

BoxLayoutia käytettäessä käyttöliittymäkomponentit asetetaan käyttöliittymään joko vaakasuunnassa tai pystysuunnassa. BoxLayoutin konstruktorille annetaan parametrina Container-olio, johon käyttöliittymäkomponentteja ollaan asettamassa, ja käyttöliittymäkomponenttien asettelusuunta. Asettelusuunta on joko BoxLayout.X_AXIS, eli komponentit vaakasuunnassa, tai BoxLayout.Y_AXIS, eli komponentit pystysuunnassa. Toisin kuin BorderLayout-asettelijaa käytettäessä, BoxLayoutilla ei ole rajattua määrää paikkoja. Container-olioon voi siis lisätä niin monta käyttöliittymäkomponenttia kuin haluaa.

Käyttöliittymän asettelu BoxLayout-asettelijaa käyttäen toimii kuten BorderLayout-asettelijan käyttö. Luomme ensin asettelijan, jonka asetamme Container-oliolle sen metodilla setLayout. Tämän jälkeen voimme lisätä käyttöliittymäkomponentteja Container-olion add-metodilla. Emme tarvitse erillistä sijaintia ilmaisevaa parametria. Alla esimerkki vaakasuunnassa asetetuista käyttöliittymäkomponenteista.

private void luoKomponentit(Container container) {
    BoxLayout layout = new BoxLayout(container, BoxLayout.X_AXIS);
    container.setLayout(layout);

    container.add(new JLabel("Eka!"));
    container.add(new JLabel("Toka!"));
    container.add(new JLabel("Kolmas!"));
}

Käyttöliittymäkomponenttien asettelu pystysuunnassa ei vaadi suurta muutosta. Vaihdamme BoxLayout-olion konstruktorille annettavaksi suuntaparametriksi BoxLayout.Y_AXIS.

private void luoKomponentit(Container container) {
    BoxLayout layout = new BoxLayout(container, BoxLayout.Y_AXIS);
    container.setLayout(layout);

    container.add(new JLabel("Eka!"));
    container.add(new JLabel("Toka!"));
    container.add(new JLabel("Kolmas!"));
}

Käyttöliittymäasettelijoita käyttämällä voimme luoda käyttöliittymiä, joissa käyttöliittymäkomponentit ovat aseteltu sopivasti. Alla on esimerkkikäyttöliittymä, jossa komponentit asetetaan pystysuuntaan. Ensin teksti, ja sitten vaihtoehtoinen valinta. Vaihtoehtoisen valinnan, eli valinnan jossa vain yksi vaihtoehto on aina voimassa, voi tehdä käyttämällä ButtonGroup-ryhmittelijää ja JRadioButton-painikkeita.

private void luoKomponentit(Container container) {
    BoxLayout layout = new BoxLayout(container, BoxLayout.Y_AXIS);
    container.setLayout(layout);

    container.add(new JLabel("Valitse ruokavalio:"));

    JRadioButton liha = new JRadioButton("Liha");
    JRadioButton kala = new JRadioButton("Kala");

    ButtonGroup buttonGroup = new ButtonGroup();
    buttonGroup.add(liha);
    buttonGroup.add(kala);

    container.add(liha);
    container.add(kala);
}

Tehtävä 162: Kysely

Toteuta tehtäväpohjaan käyttöliittymä, joka näyttää seuraavalta:

Käytä käyttöliittymän asettelijana luokkaa BoxLayout, komponentteina luokkia JLabel, JRadioButton, JCheckBox ja JButton.

Käytä ButtonGroup-luokkaa varmistamaan että vaihtoehdot "Siksi" ja "Koska se on kivaa" eivät voi olla valittuina samaan aikaan.

Varmista että käyttöliittymä on niin iso, että käyttäjä voi klikata nappeja muuttamatta sen kokoa. Voit käyttää esimerkiksi leveytenä 200 pikseliä, korkeutena 300 pikseliä.

53.3 Tapahtumien käsittely

Tähänastiset graafiset käyttöliittymämme ovat, vaikkakin hienoja, hieman tylsiä: ne eivät reagoi millään tavalla käyttöliittymässä tehtyihin tapahtumiin. Reagoimattomuus ei johdu käyttöliittymäkomponenteista, vaan siitä että emme ole lisänneet käyttöliittymäkomponentteihin tapahtumia käsitteleviä kuuntelijoita.

Tapahtumankuuntelijat kuuntelevat käyttöliittymäkomponentteja joihin ne on liitetty. Aina kun käyttöliittymäkomponentille tehdään joku toiminto, esimerkiksi napille napin painaminen, käyttöliittymäkomponentti kutsuu jokaisen siihen liitetyn tapahtumakuuntelijan tiettyä metodia. Käytännössä tapahtumankuuntelijat ovat tietyn rajapinnan toteuttavia luokkia, joiden ilmentymiä käyttöliittymäkomponentille voi lisätä. Tapahtuman tapahtuessa käyttöliittymäkomponentti käy jokaisen siihen liitetyn tapahtumankuuntelijan läpi, ja kutsuu rajapinnassa määriteltyä metodia.

Swing-käyttöliittymissä eniten käytetty tapahtumankuuntelurajapinta on ActionListener. Rajapinta ActionListener määrittelee metodin void actionPerformed(ActionEvent e), joka saa parametrinaan tapahtumasta kertovan ActionEvent-olion.

Toteutetaan ensimmäinen oma tapahtumankuuntelija, jonka tarkoituksena on vain tulostaa viesti standarditulostusvirtaan nappia painettaessa. Luokka ViestiKuuntelija toteuttaa rajapinnan ActionListener ja tulostaa viestin "Viesti vastaanotettu!" kun metodia actionPerformed kutsutaan.

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class ViestiKuuntelija implements ActionListener {

    @Override
    public void actionPerformed(ActionEvent ae) {
        System.out.println("Viesti vastaanotettu!");
    }
}

Luodaan seuraavaksi käyttöliittymään JButton-tyyppinen nappi, ja lisätään siihen ViestiKuuntelija-luokan ilmentymä. Luokalle JButton voi lisätä tapahtumankuuntelijan käyttämällä sen yläluokassa AbstractButton määriteltyä metodia public void addActionListener(ActionListener actionListener).

    private void luoKomponentit(Container container) {
        JButton nappi = new JButton("Viestitä!");
        nappi.addActionListener(new ViestiKuuntelija());

        container.add(nappi);
    }

Käyttöliittymässä olevaa nappia painettaessa näemme standarditulostusvirrassa seuraavan viestin.

Viesti vastaanotettu!

Olioiden käsittely tapahtumankuuntelijoissa

Haluamme usein että tapahtumankuuntelija muokkaa jonkun olion tilaa. Päästäksemme olioon käsiksi tapahtumankuuntelijassa, tulee meidän antaa viite käsiteltävään olioon tapahtumankuuntelijalle sen konstruktorissa. Tapahtumankuuntelijat ovat täysin samanlaisia luokkia kuin muutkin Javan luokat, eli pääsemme ohjelmoimaan kaiken haluamamme toiminnallisuuden.

Pohditaan seuraavaa käyttöliittymää jossa on kaksi JTextArea-tyyppistä tekstikenttää, eli tekstikenttää johon käyttäjä voi syöttää tekstiä, ja JButton-tyyppinen nappi. Käyttöliittymä käyttää GridLayout-asettelijaa, jonka avulla käyttöliittymän voi rakentaa taulukkomaiseksi. GridLayout-luokan konstruktorille määriteltiin yksi rivi ja kolme saraketta.

private void luoKomponentit(Container container) {
    GridLayout layout = new GridLayout(1, 3);
    container.setLayout(layout);

    JTextArea textAreaVasen = new JTextArea("Le Kopioija");
    JTextArea textAreaOikea = new JTextArea();
    JButton kopioiNappi = new JButton("Kopioi!");

    container.add(textAreaVasen);
    container.add(kopioiNappi);
    container.add(textAreaOikea);
}

Haluamme lisätä käyttöliittymään toiminnallisuuden, jossa JButton-nappia painettaessa vasemman tekstikentän sisältö kopioituu oikeaan tekstikenttään. Tämä onnistuu toteuttamalla tapahtumankuuntelija. Luodaan rajapinnan ActionListener toteuttava luokka KenttienKopioija, joka kopioi JTextArean sisällön kentästä toiseen.

import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JTextArea;

public class KenttienKopioija implements ActionListener {

    private JTextArea lahde;
    private JTextArea kohde;

    public KenttienKopioija(JTextArea lahde, JTextArea kohde) {
        this.lahde = lahde;
        this.kohde = kohde;
    }

    @Override
    public void actionPerformed(ActionEvent ae) {
        this.kohde.setText(this.lahde.getText());
    }
}

Tapahtumankuuntelijan rekisteröinti JButton-oliolle onnistuu metodilla addActionListener.

    private void luoKomponentit(Container container) {
        GridLayout layout = new GridLayout(1, 3);
        container.setLayout(layout);

        JTextArea textAreaVasen = new JTextArea("Le Kopioija");
        JTextArea textAreaOikea = new JTextArea();
        JButton kopioiNappi = new JButton("Kopioi!");

        KenttienKopioija kopioija = new KenttienKopioija(textAreaVasen, textAreaOikea);
        kopioiNappi.addActionListener(kopioija);

        container.add(textAreaVasen);
        container.add(kopioiNappi);
        container.add(textAreaOikea);
    }

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

Tehtävä 163: Ilmoitin

Toteuta tehtäväpohjaan käyttöliittymä, joka näyttää seuraavalta.

Ohjelman tulee koostua seuraavista pakkauksessa ilmoitin olevista luokista. Luokka Ilmoitin on käyttöliittymäluokka, joka käynnistetään Main-luokasta. Ilmoittimessa on käyttöliittymäkomponentteina JTextField, JButton, ja JLabel. Voit asetella käyttöliittymäkomponentit GridLayout-asettelijan avulla: kutsu new GridLayout(3, 1) luo uuden asettelijan, joka asettelee kolme käyttöliittymäelementtiä pystysuunnassa.

Sovelluksessa tulee olla lisäksi luokka TapahtumanKuuntelija, joka toteuttaa rajapinnan ActionListener. Tapahtumankuuntelija liitetään nappiin ja sen tulee kopioida käyttöliittymässä olevan JTextField-kentän sisältö JLabel-kenttään napin painalluksen yhteydessä ja samalla tyhjentää JTextField asettamalla sen sisällöksi "".

Varmista että käyttöliittymä käynnistyy niin isona että jokaista nappulaa voi klikata.

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

Sovelluslogiikan (esimerkiksi tallennus- tai lukutoiminnallisuuden) ja käyttöliittymän sekoittaminen samoihin luokkiin on yleisesti ottaen huono asia. Se vaikeuttaa ohjelman testaamista ja muokkaamista huomattavasti, ja tekee koodista myös paljon vaikeammin luettavaa. Single responsibility principlen sanoin "Jokaisella luokalla pitäisi olla vain yksi selkeä vastuu". Sovelluslogiikan erottaminen käyttöliittymälogiikasta onnistuu sopivan rajapintasuunnittelun kautta. Oletetaan, että käytössämme on rajapinta HenkiloVarasto, 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än toteutus

Käyttöliittymää toteutettaessa hyvä aloitustapa on 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 Javan JTextField-luokkaa tekstin syöttämiseen, ja JButton-luokkaa napin toteuttamiseen. Luodaan käyttöliittymään lisäksi selventävät JLabel-tyyppiset selitystekstit.

Käytetään käyttöliittymän asetteluun GridLayout-asettelijaa. Rivejä käyttöliittymässä on 3, sarakkeita 2. Lisätään tapahtumankuuntelija myöhemmin. Käyttöliittymäluokan metodi luoKomponentit näyttää nyt seuraavalta.

private void luoKomponentit(Container container) {
    GridLayout layout = new GridLayout(3, 2);
    container.setLayout(layout);

    JLabel nimiTeksti = new JLabel("Nimi: ");
    JTextField nimiKentta = new JTextField();
    JLabel hetuTeksti = new JLabel("Hetu: ");
    JTextField hetuKentta = new JTextField();

    JButton lisaaNappi = new JButton("Lisää henkilö!");
    // tapahtumankuuntelija

    container.add(nimiTeksti);
    container.add(nimiKentta);
    container.add(hetuTeksti);
    container.add(hetuKentta);
    container.add(new JLabel(""));
    container.add(lisaaNappi);
}

Käyttöliittymä näyttää seuraavalta kun siihen on lisätty tietoa.

Tapahtumankuuntelijan tulee tietää tallennustoiminnallisuudesta eli HenkiloVarasto-rajapinnasta sekä kentistä, joita se käyttää. Luodaan ActionListener-rajapinnan toteuttava luokka HenkilonLisaysKuuntelija. Luokka saa konstruktorissaan parametrina HenkiloVarasto-rajapinnan toteuttavan olion sekä kaksi JTextField-oliota, jotka ovat kentät nimelle ja henkilötunnukselle. Metodissa actionPerformed luodaan uusi Henkilo-olio ja tallennetaan se HenkiloVarasto-olion tarjoamalla talleta-metodilla.

public class HenkilonLisaysKuuntelija implements ActionListener {

    private HenkiloVarasto henkiloVarasto;
    private JTextField nimiKentta;
    private JTextField hetuKentta;

    public HenkilonLisaysKuuntelija(HenkiloVarasto henkiloVarasto, JTextField nimiKentta, JTextField hetuKentta) {
        this.henkiloVarasto = henkiloVarasto;
        this.nimiKentta = nimiKentta;
        this.hetuKentta = hetuKentta;
    }

    @Override
    public void actionPerformed(ActionEvent ae) {
        Henkilo henkilo = new Henkilo(nimiKentta.getText(), hetuKentta.getText());
        this.henkiloVarasto.talleta(henkilo);
    }
}

Jotta saamme HenkiloVarasto-viitteen HenkilonLisaysKuuntelija-oliolle, tulee sen olla käyttöliittymän tiedossa. Lisätään käyttöliittymälle oliomuuttuja private HenkiloVarasto henkiloVarasto, joka asetetaan konstruktorissa. Luokan Kayttoliittyma konstruktoria muokataan siten, että sille annetaan HenkiloVarasto-rajapinnan toteuttava luokka.

public class Kayttoliittyma implements Runnable {

    private JFrame frame;
    private HenkiloVarasto henkiloVarasto;

    public Kayttoliittyma(HenkiloVarasto henkiloVarasto) {
        this.henkiloVarasto = henkiloVarasto;
    }
    // ...

}

Voimme nyt luoda tapahtumankuuntelijan HenkilonLisaysKuuntelija, jolle annetaan sekä HenkiloVarasto-viite, että kentät.

private void luoKomponentit(Container container) {
    GridLayout layout = new GridLayout(3, 2);
    container.setLayout(layout);

    JLabel nimiTeksti = new JLabel("Nimi: ");
    JTextField nimiKentta = new JTextField();
    JLabel hetuTeksti = new JLabel("Hetu: ");
    JTextField hetuKentta = new JTextField();

    JButton lisaaNappi = new JButton("Lisää henkilö!");
    HenkilonLisaysKuuntelija kuuntelija = new HenkilonLisaysKuuntelija(henkiloVarasto, nimiKentta, hetuKentta);
    lisaaNappi.addActionListener(kuuntelija);

    container.add(nimiTeksti);
    container.add(nimiKentta);
    container.add(hetuTeksti);
    container.add(hetuKentta);
    container.add(new JLabel(""));
    container.add(lisaaNappi);
}

Tehtävä 164: Axe Click Effect

Tässä tehtävässä toteutetaan laskuri klikkausten laskemiseen. Tehtävässä sovelluslogiikka, eli laskeminen, ja käyttöliittymälogiikka on erotettu toisistaan. Lopullisen sovelluksen tulee näyttää kutakuinkin seuraavalta.

164.1 OmaLaskuri

Toteuta pakkaukseen clicker.sovelluslogiikka rajapinnan Laskuri toteuttava luokka OmaLaskuri. Luokan OmaLaskuri metodin annaArvo palauttama luku on aluksi 0. Kun metodia kasvata kutsutaan, kasvaa arvo aina yhdellä.

Voit halutessasi testata luokan toimintaa seuraavan ohjelman avulla.

Laskuri laskuri = new OmaLaskuri();
System.out.println("Arvo: " + laskuri.annaArvo());
laskuri.kasvata();
System.out.println("Arvo: " + laskuri.annaArvo());
laskuri.kasvata();
System.out.println("Arvo: " + laskuri.annaArvo());
Arvo: 0
Arvo: 1
Arvo: 2

164.2 KlikkaustenKuuntelija

Toteuta pakkaukseen clicker.kayttoliittyma rajapinnan ActionListener toteuttava luokka KlikkaustenKuuntelija. Luokka KlikkaustenKuuntelija saa konstruktorin parametrina Laskuri-rajapinnan toteuttavan olion ja JLabel-olion.

Toteuta actionPerformed-metodi siten, että Laskuri-oliota kasvatetaan aluksi yhdellä, jonka jälkeen laskurin arvo asetetaan JLabel-olion tekstiksi. JLabel-olion tekstiä voidaan muuttaa metodilla setText.

164.3 Käyttöliittymä

Muokkaa luokkaa Kayttoliittyma siten, että käyttöliittymä saa konstruktorin parametrina Laskuri-olion. Tarvitset tätä varten uuden konstruktorin. Lisää käyttöliittymään tarvittavat käyttöliittymäkomponentit. Rekisteröi napille myös edellisessä osassa toteutettu tapahtumankuuntelija.

Käytä käyttöliittymäkomponenttien asetteluun BorderLayout-luokan tarjoamia toiminnallisuuksia. Muuta myös Main-luokkaa siten, että käyttöliittymälle annetaan OmaLaskuri-olio. Kun käyttöliittymässä olevaa "Click!"-nappia on painettu kahdesti, sovellus näyttää kutakuinkin seuraavalta.

53.5 Sisäkkäiset Container-oliot

Törmäämme silloin tällöin tilanteeseen, jossa JFrame-luokan tarjoama Container-olio ei riitä käyttöliittymän asetteluun. Saatamme tarvita erilaisia käyttöliittymänäkymiä tai mahdollisuutta käyttöliittymäkomponenttien ryhmittelyyn niiden käyttötarkoituksen mukaan. Esimerkiksi alla olevan käyttöliittymän rakentaminen ei olisi kovin helppoa vain JFrame-luokan tarjoamalla Container-oliolla.

Voimme asettaa Container-tyyppisiä olioita toistensa sisään. Luokka JPanel (katso myös How to Use Panels) mahdollistaa sisäkkäiset Container-oliot. JPanel-luokan ilmentymään voi lisätä käyttöliittymäkomponentteja samalla tavalla kuin JFrame-luokasta saatuun Container-ilmentymään. Tämän lisäksi JPanel-luokan ilmentymän voi lisätä Container-olioon. Tämä mahdollistaa useamman Container-olion käyttämisen käyttöliittymän suunnittelussa.

Yllä olevan käyttöliittymän luominen on helpompaa JPanel-luokan avulla.. Luodaan käyttöliittymä, jossa on kolme nappia "Suorita", "Testaa", ja "Lähetä", sekä tekstialue joka sisältää tekstiä. Napit ovat oma joukkonsa, joten tehdään niille erillinen JPanel-olio joka asetetaan JFrame-luokasta saadun Container-olion eteläosaan. Tekstialue tulee keskelle.

    private void luoKomponentit(Container container) {
        container.add(new JTextArea());
        container.add(luoValikko(), BorderLayout.SOUTH);
    }

    private JPanel luoValikko() {
        JPanel panel = new JPanel(new GridLayout(1, 3));
        panel.add(new JButton("Suorita"));
        panel.add(new JButton("Testaa"));
        panel.add(new JButton("Lähetä"));
        return panel;
    }

JPanel-luokalle annetaan konstruktorin parametrina käytettävä asettelutyyli. Jos asettelutyyli tarvitsee konstruktorissaan viitteen käytettyyn Container-olioon (kuten BoxLayout), on JPanel-luokalla myös metodi setLayout.

Jos käyttöliittymässämme on selkeät erilliset kokonaisuudet, voimme myös periä JPanel luokan. Esimerkiksi yllä olevan valikon voisi toteuttaa myös seuraavasti.

import java.awt.GridLayout;
import javax.swing.JButton;
import javax.swing.JPanel;

public class ValikkoPanel extends JPanel {

    public ValikkoPanel() {
        super(new GridLayout(1, 3));
        luoKomponentit();
    }

    private void luoKomponentit() {
        add(new JButton("Suorita"));
        add(new JButton("Testaa"));
        add(new JButton("Lähetä"));
    }
}

Nyt käyttöliittymäluokassa voidaan luoda ValikkoPanel-luokan ilmentymä.

private void luoKomponentit(Container container) {
    container.add(new JTextArea());
    container.add(new ValikkoPanel(), BorderLayout.SOUTH);
}

Huomaa, että tapahtumankäsittelyä tarvittaessa luokalle ValikkoPanel tulee antaa parametrina kaikki tarvittavat oliot.

Tehtävä 165: Laskin

Tehtävässä on tarkoitus toteuttaa yksinkertainen laskin. Laskimen käyttöliittymän tulee olla seuraavanlainen:

Tehtäväpohjan mukana tulee käynnistyksen suorittava pääohjelma sekä graafisen käyttöliittymän sisältävä luokka GraafinenLaskin. Käyttöliittymän on oltava täsmälleen seuraavassa osassa kuvaillulla tavalla tehty, muuten saat vapaasti suunnitella ohjelman rakenteen.

165.1 Layout kuntoon

Käyttöliittymän pohjana olevassa JFramessa tulee käyttää asettelijana GridLayoutia jossa on kolme riviä ja yksi sarake. Ylimpänä on tuloskenttänä toimiva JTextField, joka täytyy asettaa "pois päältä" metodikutsulla setEnabled(false). Toisena on syötekenttänä toimiva JTextField. Tuloskentässä on aluksi teksti 0 ja syötekenttä on tyhjä.

Alimpana komponenttina sijaitsee JPanel, jonka asettelijana on GridLayout, jossa on yksi rivi ja kolme saraketta. JPanelissa on kolme JButtonia, joissa tekstit "+", "-" ja "Z".

Laskimen käyttöliittymän koon on oltava vähintään 300*150.

165.2 Perustoiminnallisuus

Laskimen toimintalogiikka on seuraava. Käyttäjän kirjoittaessa syötekenttään luvun n ja painaessa +, lisätään tuloskentässä olevaan arvoon n ja päivitetään tuloskenttä uuteen arvoon. Vastaavasti käyttäjän kirjoittaessa syötekenttään luvun n ja painaessa -, vähennetään tuloskentässä olevasta arvosta n ja päivitetään tuloskenttä uuteen arvoon. Jos käyttäjä painaa Z, nollautuu tuloskenttä.

Vihje: joskus on tarkoituksenmukaista hoitaa yhdellä tapahtumankuuntelijalla usean napin painallusten käsittely. Tämä onnistuu kysymällä actionPerformed-metodin parametrilta kuka oli tapahtuman aiheuttaja. Seuraava koodi olettaa, että tapahtumankäsittelijällä on oliomuuttujat JButton plus ja JButton miinus jotka viittaavat plus- ja miinus-nappeihin:

    @Override
    public void actionPerformed(ActionEvent ae) {
        if (ae.getSource() == plus) {
           // käsittele plus-painike
        } else if (ae.getSource() == miinus) {
           // käsittele miinus-painike
        } else ...

165.3 Hienosäätö

Laajennetaan vielä ohjelmaa seuraavilla ominaisuuksilla:

  • Jos tuloskentässä on 0, ei Z-nappia voi painaa, eli se tulee olla asetettu "pois päältä" metodikutsulla setEnabled(false). Muissa tilanteissa napin tulee olla päällä.
  • Kun käyttäjä painaa jotain napeista +, -, Z syötekenttä tyhjenee.
  • Jos syötekentässä oleva syöte ei ole kokonaisluku ja käyttäjä painaa jotain napeista+, -, Z syötekenttä tyhjenee ja tuloskentän tila ei muutu (paitsi napin ollessa Z).

54 Piirtäminen

Luokkaa JPanel käytetään Container-toiminnallisuuden lisäksi usein piirtoalustana siten, että käyttäjä perii luokan JPanel ja korvaa metodin protected void paintComponent(Graphics graphics). Käyttöliittymä kutsuu metodia paintComponent aina kun käyttöliittymäkomponentin sisältö halutaan piirtää ruudulle. Metodi paintComponent saa käyttöliittymältä parametrina abstraktin luokan Graphics toteuttavan olion. Luodaan luokan JPanel perivä luokka Piirtoalusta, joka korvaa paintComponent-metodin.

public class Piirtoalusta extends JPanel {

    public Piirtoalusta() {
        super.setBackground(Color.WHITE);
    }

    @Override
    protected void paintComponent(Graphics graphics) {
        super.paintComponent(graphics);
    }
}

Yllä oleva piirtoalusta ei sisällä konkreettista piirtämistoiminnallisuutta. Asetamme konstruktorissa piirtoalustan taustan valkoiseksi kutsumalla yläluokan metodia setBackground. Metodi setBackGround saa parametrina Color-luokan ilmentymän. Luokka Color sisältää yleisimmät värit luokkamuuttujina, esimerkiksi väri valkoinen löytyy luokkamuuttujasta Color.WHITE.

Huom: metodin paintComponent alussa tulee olla kutsu korvattuun metodiin, eli ensimmäisen rivin tulee olla super.paintComponent(g); muuten piirtäminen ei toimi halutulla tavalla.

Korvattu paintComponent metodi kutsuu yläluokan paintComponent-metodia eikä tee muuta. Lisätään piirtoalusta seuraavaksi käyttöliittymäluokan luoKomponentit-metodiin. Käytämme kappaleen 51. Käyttöliittymät alussa määriteltyä käyttöliittymäpohjaa.

    private void luoKomponentit(Container container) {
        container.add(new Piirtoalusta());
    }

Käynnistäessämme käyttöliittymän näemme tyhjän ruudun, jonka taustaväri on valkoinen. Alla olevan käyttöliittymän toivotuksi kooksi on asetettu setPreferredSize-metodilla 300, 300, ja sen otsikko on "Piirtoalusta".

Piirtoalustalle piirtäminen tapahtuu Graphics-olion tarjoamien metodien avulla. Muokataan Piirtoalusta-luokan metodia paintComponent siten, että siinä piirretään kaksi suorakulmiota Graphics-olion tarjoaman metodin fillRect avulla.

@Override
protected void paintComponent(Graphics graphics) {
    super.paintComponent(graphics);

    graphics.fillRect(50, 80, 100, 50);
    graphics.fillRect(200, 20, 50, 200);
}

Metodi fillRect saa parametrina suorakulmion x, ja y -koordinaatit, sekä suorakulmion leveyden ja korkeuden tässä järjestyksessä. Yllä siis piirretään ensin koordinaatista (50, 80) alkava 100 pikseliä leveä ja 50 pikseliä korkea suorakulmio. Tämän jälkeen piirretään koordinaatista (200, 20) alkava 50 pikseliä leveä ja 100 pikseliä korkea suorakulmio.

Kuten piirtotuloksesta huomaat, koordinaatisto ei toimi aivan kuten olemme tottuneet.

Javan Graphics-olio (ja useiden muiden ohjelmointikielten käyttöliittymäkirjastot) olettaa että y-akselin arvo kasvaa alaspäin mennessä. Koordinaatiston origo, eli piste (0, 0) on piirrettävän alueen vasemmassa yläkulmassa: Graphics-olio tietää aina käyttöliittymäkomponentin, johon piirretään, ja osaa sen perusteella päätellä piirtotapahtuman sijainnin. Käyttöliittymän origon sijainti selkeytyy seuraavalla ohjelmalla. Piirretään ensin pisteestä (0, 0) lähtevä 10 pikseliä leveä ja 200 pikseliä korkea vihreä suorakulmio. Tämän jälkeen piirretään pisteestä (0, 0) lähtevä 200 pikseliä leveä ja 10 pikseliä korkea musta. Seuraavana piirrettävän kuvion väri määritellään Graphics-oliolle metodilla setColor.

@Override
protected void paintComponent(Graphics graphics) {
    super.paintComponent(graphics);

    graphics.setColor(Color.GREEN);
    graphics.fillRect(0, 0, 10, 200);
    graphics.setColor(Color.BLACK);
    graphics.fillRect(0, 0, 200, 10);
}

Tämä koordinaatiston käänteisyys johtuu siitä, miten käyttöliittymien kokoa muokataan. Käyttöliittymän kokoa muutettaessa sitä pienennetään tai suurennetaan "oikeasta alakulmasta vetäen", jolloin ruudulla näkyvä piirros siirtyy kokoa muuttaessa. Kun koordinaatisto alkaa vasemmasta yläkulmasta, on piirroksen sijainti aina sama, mutta näkyvä osa muuttuu.

Tehtävä 166: Piirtoalusta ja Piirtäminen

Tehtäväpohjassa on valmiina käyttöliittymä, johon on kytketty JPanel-luokan perivä luokka Piirtoalusta. Muuta luokan Piirtoalusta metodin paintComponent toteutusta siten, että se piirtää seuraavanlaisen kuvion. Saat käyttää tehtävässä vain graphics-olion fillRect-metodia.

Huom! Älä käytä enempää kuin viittä fillRect-kutsua. Kuvion ei tarvitse olla täsmälleen samanlainen kuin ylläoleva, testit kertovat kun piirtämäsi kuva on tarpeeksi lähellä haluttua kuvaa.

Laajennetaan edellistä esimerkkiä siten, että piirrämme käyttöliittymässä erillisen hahmo-olion. Luodaan hahmon edustamiseen luokka Hahmo. Hahmolla on koordinaatteina ilmaistu sijainti, ja se piirretään ympyränä jonka halkaisija on 10 pikseliä. Hahmon sijaintia voi muuttaa kutsumalla sen siirry-metodia.

import java.awt.Graphics;

public class Hahmo {

    private int x;
    private int y;

    public Hahmo(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public void siirry(int xmuutos, int ymuutos) {
        this.x += xmuutos;
        this.y += ymuutos;
    }

    public void piirra(Graphics graphics) {
        graphics.fillOval(x, y, 10, 10);
    }
}

Muutetaan piirtoalustaa siten, että sille annetaan Hahmo-luokan ilmentymä konstruktorin parametrina. Luokan Piirtoalusta metodi paintComponent ei itse piirrä hahmoa, vaan delegoi piirtovastuun Hahmo-luokan ilmentymälle.

import java.awt.Color;
import java.awt.Graphics;
import javax.swing.JPanel;

public class Piirtoalusta extends JPanel {

    private Hahmo hahmo;

    public Piirtoalusta(Hahmo hahmo) {
        super.setBackground(Color.WHITE);
        this.hahmo = hahmo;
    }

    @Override
    protected void paintComponent(Graphics graphics) {
        super.paintComponent(graphics);
        hahmo.piirra(graphics);
    }
}

Annetaan hahmo myös käyttöliittymälle parametrina. Hahmo on siis käyttöliittymästä erillinen olio, joka vain halutaan piirtää käyttöliittymässä. Oleelliset muutokset käyttöliittymäluokassa ovat siis konstruktorin muuttaminen siten, että se saa parametrina Hahmo-olion. Tämän lisäksi metodissa luoKomponentit annetaan Hahmo-luokan ilmentymä parametrina luotavalle Piirtoalusta-oliolle.

public class Kayttoliittyma implements Runnable {

    private JFrame frame;
    private Hahmo hahmo;

    public Kayttoliittyma(Hahmo hahmo) {
        this.hahmo = hahmo;
    }

// ...

    private void luoKomponentit(Container container) {
        Piirtoalusta piirtoalusta = new Piirtoalusta(hahmo);
        container.add(piirtoalusta);
    }
// ...

Käyttöliittymän voi nyt käynnistää antamalla sen konstruktorille Hahmo-olion parametrina.

Kayttoliittyma kayttoliittyma = new Kayttoliittyma(new Hahmo(30, 30));
SwingUtilities.invokeLater(kayttoliittyma);

Yllä olevassa käyttöliittymässä näkyy huikea, pallonmuotoinen hahmo.

Lisätään seuraavaksi ohjelmaan hahmon siirtämistoiminnallisuus. Haluamme liikuttaa hahmoa näppäimistöllä. Kun käyttäjä painaa nuolta vasemmalle, hahmon pitäisi siirtyä vasemmalle. Oikealle osoittavaa nuolta painettaessa hahmon pitäisi siirtyä oikealle. Tarvitsemme siis tapahtumankuuntelijan, joka kuuntelee näppäimistöä. Rajapinta KeyListener määrittelee näppäimistönkuuntelijalta vaaditut toiminnallisuudet.

Rajapinta KeyListener vaatii metodien keyPressed, keyReleased, ja keyTyped toteuttamista. Olemme kiinnostuneita vain tapahtumasta, jossa näppäintä painetaan, joten jätämme metodit keyReleased ja keyTyped tyhjiksi. Luodaan luokka NappaimistonKuuntelija, joka toteuttaa rajapinnan KeyListener. Luokka saa parametrina Hahmo-olion, jota tapahtumankäsittelijän tulee liikuttaa.

import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

public class NappaimistonKuuntelija implements KeyListener {

    private Hahmo hahmo;

    public NappaimistonKuuntelija(Hahmo hahmo) {
        this.hahmo = hahmo;
    }

    @Override
    public void keyPressed(KeyEvent e) {
        if (e.getKeyCode() == KeyEvent.VK_LEFT) {
            hahmo.siirry(-5, 0);
        } else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
            hahmo.siirry(5, 0);
        }
    }

    @Override
    public void keyReleased(KeyEvent e) {
    }

    @Override
    public void keyTyped(KeyEvent ke) {
    }
}

Metodi keyPressed saa käyttöliittymältä parametrina KeyEvent-luokan ilmentymän. KeyEvent-oliolta saa tietoon painettuun nappiin liittyvän numeron sen getKeyCode()-metodilla. Eri näppäimille on luokkamuuttujat KeyEvent-luokassa, esimerkiksi nuoli vasemmalle on KeyEvent.VK_LEFT.

Haluamme kuunnella käyttöliittymään kohdistuvia näppäimen painalluksia (emme esimerkiksi ole kirjoittamassa tekstikenttään), joten lisätään näppäimistönkuuntelija JFrame-luokan ilmentymälle. Muokataan käyttöliittymäämme siten, että näppäimistönkuuntelija lisätään JFrame-oliolle.

private void luoKomponentit(Container container) {
    Piirtoalusta piirtoalusta = new Piirtoalusta(hahmo);
    container.add(piirtoalusta);

    frame.addKeyListener(new NappaimistonKuuntelija(hahmo));
}

Nyt sovelluksemme kuuntelee näppäimistöltä tulleita painalluksia, ja ohjaa ne luokan NappaimistonKuuntelija ilmentymälle.

Kokeillessamme käyttöliittymää se ei kuitenkaan toimi: hahmo ei siirry ruudulla. Mistä tässä oikein on kyse? Voimme tarkastaa että näppäimistön painallukset ohjautuvat NappaimistonKuuntelija-oliolle lisäämällä keyPressed-metodin alkuun testitulostuksen.

@Override
public void keyPressed(KeyEvent e) {
    System.out.println("Nappia " + e.getKeyCode() +  " painettu.");

    // ...
}

Käynnistäessämme ohjelman ja painaessamme näppäimiä näemme konsolissa tulostuksen.

Nappia 39 painettu.
Nappia 37 painettu.
Nappia 40 painettu.
Nappia 38 painettu.

Huomaamme että näppäimistön kuuntelija toimii, mutta piirtoalusta ei päivity.

54.1 Piirtoalustan uudelleenpiirtäminen

Käyttöliittymäkomponentit sisältävät yleensä toiminnallisuuden komponentin ulkoasun uudelleenpiirtämiseen tarvittaessa. Esimerkiksi nappia painettaessa JButton-luokan ilmentymä osaa piirtää napin "painettuna", jonka jälkeen nappi piirretään taas normaalina. Toteuttamassamme piirtoalustassa ei ole valmista päivitystoiminnallisuutta, vaan meidän tulee pyytää sitä piirtämään itsensä uudelleen tarvittaessa.

Jokaisella Component-luokan aliluokalla on metodi public void repaint(), jonka kutsuminen pakottaa komponentin uudelleenpiirtämisen. Haluamme että Piirtoalusta-olio piirretään uudestaan aina kun hahmoa siirretään. Hahmon siirtäminen tapahtuu luokassa NappaimistonKuuntelija, joten on loogista että uudelleenpiirtokutsu tapahtuu myös näppäimistönkuuntelijassa.

Uudelleenpiirtokutsua varten näppäimistönkuuntelija tarvitsee viitteen piirtoalustaan. Muutetaan luokkaa NappaimistonKuuntelija siten, että se saa parametrinaan Hahmo-olion lisäksi uudelleenpiirrettävän Component-olion. Kutsutaan Component-olion repaint-metodia jokaisen keyPressed tapahtuman lopussa.

import java.awt.Component;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

public class NappaimistonKuuntelija implements KeyListener {

    private Component component;
    private Hahmo hahmo;

    public NappaimistonKuuntelija(Hahmo hahmo, Component component) {
        this.hahmo = hahmo;
        this.component = component;
    }

    @Override
    public void keyPressed(KeyEvent e) {
        if (e.getKeyCode() == KeyEvent.VK_LEFT) {
            hahmo.siirry(-5, 0);
        } else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
            hahmo.siirry(5, 0);
        }

        component.repaint();
    }

    @Override
    public void keyReleased(KeyEvent e) {
    }

    @Override
    public void keyTyped(KeyEvent ke) {
    }
}

Muutetaan myös Kayttoliittyma-luokan luoKomponentit-metodia siten, että Piirtoalusta-luokan ilmentymä annetaan parametrina näppäimistönkuuntelijalle.

private void luoKomponentit(Container container) {
    Piirtoalusta piirtoalusta = new Piirtoalusta(hahmo);
    container.add(piirtoalusta);

    frame.addKeyListener(new NappaimistonKuuntelija(hahmo, piirtoalusta));
}

Nyt hahmon liikuttaminen myös näkyy käyttöliittymässä. Aina kun käyttäjä painaa näppäimistöä, käyttöliittymään liitetty näppäimistönkuuntelija käsittelee kutsun. Jokaisen kutsun lopuksi kutsutaan piirtoalustan repaint-metodia, joka aiheuttaa piirtoalustan uudelleenpiirtämisen.

Tehtävä 167: Liikkuva kuvio

Teemme ohjelman, jossa käyttäjä voi liikutella näppäimistön avulla ruudulle piirrettyjä kuvioita. Ohjelmassa tulee mukana käyttöliittymärunko, jota pääset muokkaamaan ohjelman edetessä.

Aluksi tehdään muutama luokka, joilla kuvioita hallitaan. Pääsemme myöhemmin piirtämään kuvioita ruudulle. Tee kaikki ohjelman luokat pakkaukseen liikkuvakuvio.

Tehtävässä käytetään perintää ja abstrakteja luokkia. Kertaa siis tarvittaessa niihin liittyvä sisältö.

167.1 Abstrakti luokka Kuvio

Tee abstrakti luokka Kuvio. Kuviolla on oliomuuttujat x ja y, jotka kertovat kuvion sijainnin ruudulla sekä metodi public void siirra(int dx, int dy), jonka avulla kuvion sijainti siirtyy parametrina olevien koordinaattisiirtymien verran. Esim. jos sijainti aluksi on (100,100), niin kutsun siirra(10,-50) jälkeen sijainti on (110, 50). Luokan konstruktorin public Kuvio(int x, int y) tulee asettaa kuviolle alkusijainti. Lisää luokalle myös metodit public int getX() ja public int getY().

Luokalla tulee olla myös abstrakti metodi public abstract void piirra(Graphics graphics), jolla kuvio piirretään piirtoalustalle. Kuvion piirtämismetodi toteutetaan luokan Kuvio perivissä metodeissa.

167.2 Ympyra

Tee luokka Ympyra joka perii Kuvion. Ympyrällä on halkaisija jonka arvon konstruktori public Ympyra(int x, int y, int halkaisija) asettaa. Sijainti tallennetaan yläluokassa määriteltyihin oliomuuttujiin.

Ympyra määrittelee metodin piirra siten, että oikean kokoinen ympyrä piirretään koordinaattien osoittamaan paikkaan parametrina olevan Graphics-olion fillOval-metodia käyttäen, ympyrän sijaintia tulee käyttää metodin kahtena ensimmäisenä parametrina. Ota mallia Hahmo-esimerkin vastaavasta metodista. Graphics-olion metodien toimintaa kannattaa tutkia Java API:sta.

167.3 Piirtoalusta

Luo luokka Piirtoalusta joka perii luokan JPanel, mallia voit ottaa esimerkiksi edellisen tehtävän mukana tulleesta piirtoalustasta. Piirtoalusta saa konstruktorin parametrina Kuvio-tyyppisen olion. Korvaa luokan JPanel metodi protected void paintComponent(Graphics g) siten, että siinä kutsutaan ensin yläluokan paintComponent-metodia ja sitten piirtoalustalle asetetun kuvion piirra-metodia.

Muokkaa luokkaa Kayttoliittyma siten, että se saa konstruktorin parametrina Kuvio-tyyppisen olion. Lisää käyttöliittymään Piirtoalusta luoKomponentit(Container container)-metodissa, anna piirtoalustalle konstruktorin parametrina käyttöliittymälle annettu kuvio.

Testaa lopuksi että seuraavalla esimerkkikoodilla ruudulle piirtyy ympyrä.

Kayttoliittyma kayttoliittyma = new Kayttoliittyma(new Ympyra(50, 50, 250));
SwingUtilities.invokeLater(kayttoliittyma);

167.4 Näppäimistöohjaus

Laajennetaan piirtoalustaa siten, että kuviota voi liikutella nuolinäppäinten avulla. Luo rajapinnan KeyListener toteuttava luokka NappaimistonKuuntelija. Luokan NappaimistonKuuntelija konstruktorin parametrit ovat luokan Component ilmentymä ja luokan Kuvio ilmentymä.

Luokan Component ilmentymä annetaan näppäimistönkuuntelijalle, jotta voimme päivittää halutun komponentin jokaisen näppäimenpainalluksen jälkeen uudestaan. Komponentin päivittäminen tapahtuu kutsumalla Component luokasta perityvää metodia repaint. Luokka Piirtoalusta on tyyppiä Component koska Component on luokan JPanel perivän luokan yläluokka.

Toteuta rajapinnan KeyListener määrittelemä metodi keyPressed(KeyEvent e) siten, että käyttäjän painaessa nuolta vasemmalle kuvio siirtyy yhden pykälän vasemmalle. Oikealle painettaessa yksi oikealle. Ylös painettaessa yksi ylös, ja alas painettaessa yksi alas. Huomaa että y-akseli kasvaa ikkunan yläosasta alaspäin. Näppäinkoodit nuolinäppäimille ovat KeyEvent.VK_LEFT, KeyEvent.VK_RIGHT, KeyEvent.VK_UP, ja KeyEvent.VK_DOWN. Jätä muut rajapinnan KeyListener vaatimat metodit tyhjiksi.

Kutsu aina Component-luokan repaint-metodia näppäimistönkuuntelutapahtuman lopussa.

Lisää näppäimistönkuuntelija Kayttoliittyma-luokan lisaaKuuntelijat-metodissa. Näppäimistönkuuntelija tulee liittää JFrame-olioon.

167.5 Nelio ja Laatikko

Peri luokasta Kuvio luokat Nelio ja Laatikko. Neliöllä on konstruktori public Nelio(int x, int y, int sivunPituus), laatikon konstruktori on muotoa public Laatikko(int x, int y, int leveys, int korkeus). Käytä piirtämisessä graphics-olion fillRect-metodia.

Varmista, että neliöt ja laatikot piirtyvät ja liikkuvat oikein Piirtoalustalla.

Kayttoliittyma kayttoliittyma = new Kayttoliittyma(new Nelio(50, 50, 250));
SwingUtilities.invokeLater(kayttoliittyma);

Kayttoliittyma kayttoliittyma = new Kayttoliittyma(new Laatikko(50, 50, 100, 300));
SwingUtilities.invokeLater(kayttoliittyma);

167.6 Koostekuvio

Peri luokasta Kuvio luokka Koostekuvio. Koostekuvio sisältää joukon muita kuvioita jotka se tallettaa ArrayList:iin. Koostekuviolla on metodi public void liita(Kuvio k) jonka avulla koostekuvioon voi liittää kuvio-olion. Koostekuviolla ei ole omaa sijaintia ja ei ole merkitystä mitä koostekuvio asettaa perimiensä x- ja y-koordinaatin arvoiksi. Koostekuvio piirtää itsensä pyytämällä osiaan piirtämään itsensä, koostekuvion siirtyminen tapahtuu samoin. Kuviolta peritty metodi siirra on siis ylikirjoitettava!

Koostekuvion konstruktorilla ei ole parametreja. Koostekuvion konstruktorista on kuitenkin pakko kutsua yliluokan konstruktoria jolla taas on parametrit. Koska koostekuviolla ei ole omaa sijaintia, voit käyttää yliluokan konstruktorikutsussa mitä tahansa parametrin arvoja.

Testaa että koostekuviosi piirtyy ja siirtyy oikein, esim. seuraavan koostekuvion avulla:

Koostekuvio rekka = new Koostekuvio();

rekka.liita(new Laatikko(220, 110, 75, 100));
rekka.liita(new Laatikko(80, 120, 200, 100));
rekka.liita(new Ympyra(100, 200, 50));
rekka.liita(new Ympyra(220, 200, 50));

Kayttoliittyma kayttoliittyma = new Kayttoliittyma(rekka);
SwingUtilities.invokeLater(kayttoliittyma);

Huomaa miten olioiden vastuut jakautuvat tehtävässä. Jokainen Kuvio on vastuussa itsensä piirtämisestä ja siirtämisestä. Yksinkertaiset kuviot siirtyvät kaikki samalla tavalla. Jokaisen yksinkertaisen kuvion on itse hoidettava piirtymisestään. Koostekuvio siirtää itsensä pyytämällä osiaan siirtymään, samoin hoituu koostekuvion piirtyminen. Piirtoalusta tuntee Kuvio-olion joka siis voi olla mikä tahansa yksinkertainen kuvio tai koostekuvio, kaikki piirretään ja siirretään samalla tavalla. Piirtoalusta siis toimii samalla tavalla kuvion oikeasta tyypistä huolimatta, piirtoalustan ei tarvitse tietää kuvion yksityiskohdista mitään. Kun piirtoalusta kutsuu kuvion metodia piirra tai siirra polymorfismin ansiosta kutsutuksi tulee kuvion todellista tyyppiä vastaava metodi.

Huomionarvoista tehtävässä on se, että Koostekuvio voi sisältää mitä tahansa Kuvio-olioita, siis myös koostekuvioita! Luokkarakenne mahdollistaakin mielivaltaisen monimutkaisen kuvion muodostamisen ja kuvion siirtely ja piirtäminen tapahtuu aina täsmälleen samalla tavalla.

Luokkarakennetta on myös helppo laajentaa, esim. perimällä Kuvio-luokasta uusia kuviotyyppejä: kolmio, piste, viiva, ym... Koostekuvio toimii ilman muutoksia myös uusien kuviotyyppien kanssa, samoin piirtoalusta ja käyttöliittymä.

54.2 Valmiit sovelluskehykset

Sovelluskehys on ohjelma, joka tarjoaa lähtökohdan ja joukon palveluita jonkin erityisen sovelluksen toteuttamiseen. Yksi tapa laatia sovelluskehys on laatia valmiita palveluita tarjoava luokka, jonka päälle luokan perivät luokat rakentavat erityisen sovelluksen. Sovelluskehykset ovat yleensä hyvin laajoja, ja tarkoitettu johonkin tiettyyn tarkoitukseen, esimerkiksi pelien ohjelmointiin tai web-sovelluskehitykseen. Tutustutaan seuraavasti pikaisesti valmiin sovelluskirjaston käyttöön luomalla sovelluslogiikka Game of Life -pelille.

Tehtävä 168: Game of Life

Tässä tehtäväsarjassa toteutetaan sovelluslogiikka Game of Life-pelille perimällä valmis sovellusrunko. Sovellusrunko on projektiin erikseen lisätyssä kirjastossa, joten sen lähdekoodit eivät ole nähtävissä.

HUOM: tehtävä ei ole erityisen vaikea, mutta tehtävänanto saattaa aluksi vaikuttaa sekavalta. Lue ohje tarkasti uudelleen tai kysy apua jos et pääse alkuun. Tehtävä kannattaa ehdottomasti tehdä, sillä lopputulos on hieno!

Game of Life on matemaatikko John Conway'n kehittelemä yksinkertainen "populaatiosimulaattori", kts. http://en.wikipedia.org/wiki/Conway%27s_Game_of_Life.

Game of Lifen säännöt ovat seuraavat:

  • Jokainen elossa oleva solu, jolla on alle kaksi elossa olevaa naapuria kuolee.
  • Jokainen elossa oleva solu, jolla on kaksi tai kolme elossa olevaa naapuria elää seuraavaan iteraatioon eli kierrokseen.
  • Jokainen elossa oleva solu, jolla on yli kolme naapuria kuolee.
  • Jokainen kuollut solu, jolla on tasan kolme elossa olevaa naapuria muuttuu eläväksi.

Abstrakti luokka GameOfLifeAlusta tarjoaa seuraavat toiminnot

  • public GameOfLifeAlusta(int leveys, int korkeus) luo määritellyn kokoisen pelialustan
  • public boolean[][] getAlusta() tarjoaa pääsyn pelialustaan, joka on totuusarvoista koostuva kaksiulotteinen taulukko – kuten metodin paluuarvosta voi havaita! Palaamme kaksiulotteiseen taulukkoon tarkemmin sitä tarvitessamme.
  • public int getLeveys() palauttaa alustan leveyden
  • public int getKorkeus() palauttaa alustan korkeuden
  • public void pelaaKierros() simuloi pelikierroksen

Luokassa GameOfLifeAlusta on lisäksi määritelty seuraavat abstraktit metodit, jotka sinun tulee toteuttaa.

  • public abstract void muutaElavaksi(int x, int y) muuttaa alkion koordinaatissa (x, y) eläväksi eli asettaa sille arvon true. Jos koordinaatit ovat alustan ulkopuolella ei tapahdu mitään.
  • public abstract void muutaKuolleeksi(int x, int y) muuttaa alkion koordinaatissa (x, y) kuolleeksi eli asettaa sille arvon false. Jos koordinaatit ovat alustan ulkopuolella ei tapahdu mitään.
  • public abstract boolean onElossa(int x, int y) kertoo onko koordinaatissa (x, y) oleva alkio elossa. Jos koordinaatit ovat alustan ulkopuolella, palautetaan false.
  • public abstract void alustaSatunnaisetPisteet(double todennakoisyysPisteelle) alustaa kaikki alustan alkiot siten, että kukin alkio on elävä todennäköisyydellä todennakoisyysPisteelle. Todennäköisyys annetaan double-arvona suljetulla välillä [0, 1]. Jos metodia kutsutaan arvolla 1, tulee jokaisen alkion olla elävä. Jos taas todennäköisyys on 0, tulee jokaisen alkion olla kuollut.
  • public abstract int getElossaOlevienNaapurienLukumaara(int x, int y) kertoo elossa olevien naapureiden lukumäärän solulle pisteessä (x, y).
  • public abstract void hoidaSolu(int x, int y, int elossaOleviaNaapureita) hoitaa solun (x, y) Game of Life -sääntöjen mukaan.

168.1 GameOfLife-toteutus, vaihe 1

Luo pakkaukseen game luokka OmaAlusta, joka perii pakkauksessa gameoflife olevan luokan GameOfLifeAlusta. Huomaa että pakkausta gameoflife ei ole näkyvillä omassa projektissasi, vaan se tulee mukana luokkakirjastona. Toteuta luokalle OmaAlusta konstruktori public OmaAlusta(int leveys, int korkeus), joka kutsuu yläluokan konstruktoria annetuilla parametreilla:

import gameoflife.GameOfLifeAlusta;

public class OmaAlusta extends GameOfLifeAlusta {

    public OmaAlusta(int leveys, int korkeus) {
        super(leveys, korkeus);
    }

    // ..

Voit ensin korvata kaikki abstraktit metodit ei-abstrakteilla metodeilla, jotka eivät kuitenkaan vielä tee mitään järkevää. Mutta koska ne eivät ole abstrakteja, tästä luokasta voi luoda ilmentymiä, toisin kuin abstraktista luokasta GameOfLifeAlusta.

Toteuta seuraavat metodit

  • public void muutaElavaksi(int x, int y) muuttaa alkion koordinaatissa (x, y) eläväksi eli asettaa sille arvon true
  • public void muutaKuolleeksi(int x, int y) muuttaa alkion koordinaatissa (x, y) kuolleeksi eli asettaa sille arvon false
  • public boolean onElossa(int x, int y) kertoo onko koordinaatissa (x, y) oleva alkio elossa. Jos koordinaatit ovat alustan ulkopuolella, palautetaan false.

Vihje: Pääset yläluokassa olevaan kaksiulotteiseen taulukkoon käsiksi yläluokan tarjoaman metodin getAlusta() avulla. Kaksiulotteisia taulukoita käytetään kuten yksiulotteisia taulukoita, mutta taulukoille annetaan kaksi indeksiä. Ensimmäinen indeksi kertoo leveyskohdan, toinen indeksi korkeuskohdan. Esimerkiksi seuraava ohjelmapätkä luo ensin 10 x 10 -kokoisen taulukon, ja tulostaa sitten taulukon indeksissä 3, 1 olevan arvon.

boolean[][] arvot = new boolean[10][10];
System.out.println(arvot[3][1]);

Vastaavasti OmaAlusta-luokassa voidaan tulostaa yläluokasta saadun taulukon arvo indeksissä x, y seuraavasti:

boolean[][] alusta = getAlusta();
System.out.println(alusta[x][y]);

Ja indeksiin x,y voidaan asettaa esim. arvo true seuraavasti:

boolean[][] alusta = getAlusta();
alusta[x][y] = true;

Tai suoraan käyttämättä apumuuttujaa:

getAlusta()[x][y] = true;

Testaa toteutustasi seuraavalla testiohjelmalla. Huom: jos projektissasi ei ole mukana luokkaa GameOfLifeTestaaja löydät sen täältä.

package game;

public class Main {
    public static void main(String[] args) {
        OmaAlusta alusta = new OmaAlusta(7, 5);

        alusta.muutaElavaksi(2, 0);
        alusta.muutaElavaksi(4, 0);

        alusta.muutaElavaksi(3, 3);
        alusta.muutaKuolleeksi(3, 3);

        alusta.muutaElavaksi(0, 2);
        alusta.muutaElavaksi(1, 3);
        alusta.muutaElavaksi(2, 3);
        alusta.muutaElavaksi(3, 3);
        alusta.muutaElavaksi(4, 3);
        alusta.muutaElavaksi(5, 3);
        alusta.muutaElavaksi(6, 2);

        GameOfLifeTestaaja gom = new GameOfLifeTestaaja(alusta);
        gom.pelaa();
    }
}

Tulostuksen pitäisi olla seuraavanlainen:

Paina enter jatkaaksesi, muut lopettaa: <enter>

  X X

X     X
 XXXXX

Paina enter jatkaaksesi, muut lopettaa: stop
Kiitos!

168.2 GameOfLife-toteutus, vaihe 2

Toteuta metodi alustaSatunnaisetPisteet(double todennakoisyysPisteelle), joka alustaa kaikki alkiot siten, että kukin alkio on elävä todennäköisyydellä todennakoisyysPisteelle. Todennäköisyys annetaan metodille suljetulla välillä [0, 1] olevana double-tyyppisenä parametrina.

Testaa metodia. Arvolla 0.0 ei pitäisi olla yhtään elossa olevaa solua, arvolla 1.0 kaikkien solujen tulisi olla elossa (eli näkyä X-merkkisinä). Arvolla 0.5 noin puolet soluista on eläviä.

OmaAlusta alusta = new OmaAlusta(3, 3);
alusta.alustaSatunnaisetPisteet(1.0);

GameOfLifeTestaaja gom = new GameOfLifeTestaaja(alusta);
gom.pelaa();
Paina enter jatkaaksesi, muut lopettaa: <enter>

XXX
XXX
XXX
Paina enter jatkaaksesi, muut lopettaa: stop
Kiitos!

168.3 GameOfLife-toteutus, vaihe 3

Toteuta metodi getElossaOlevienNaapurienLukumaara(int x, int y), joka laskee elossa olevien naapurien lukumäärän. Keskellä taulukkoa olevalla solulla on yhteensä kahdeksan naapuria, reunassa olevalla solulla 5, kulmassa olevalla 3.

Testaa metodia seuraavilla lauseilla (voit keksiä myös muita testitapauksia!):

OmaAlusta alusta = new OmaAlusta(7, 5);

alusta.muutaElavaksi(0, 1);
alusta.muutaElavaksi(1, 0);
alusta.muutaElavaksi(1, 2);
alusta.muutaElavaksi(2, 2);
alusta.muutaElavaksi(2, 1);

System.out.println("Elossa naapureita (0,0): " + alusta.getElossaOlevienNaapurienLukumaara(0, 0));
System.out.println("Elossa naapureita (1,1): " + alusta.getElossaOlevienNaapurienLukumaara(1, 1));

Tulostuksen pitäisi olla seuraavanlainen:

Elossa naapureita (0,0): 2
Elossa naapureita (1,1): 5

168.4 GameOfLife-toteutus, vaihe 4

Jäljellä on vielä metodin hoidaSolu(int x, int y, int elossaOleviaNaapureita) toteuttaminen. GameOfLife-pelin säännöthän olivat seuraavat:

  • Jokainen elossa oleva solu, jolla on alle kaksi elossa olevaa naapuria kuolee.
  • Jokainen elossa oleva solu, jolla on kaksi tai kolme elossa olevaa naapuria elää seuraavaan iteraatioon eli kierrokseen.
  • Jokainen elossa oleva solu, jolla on yli kolme naapuria kuolee.
  • Jokainen kuollut solu, jolla on tasan kolme elossa olevaa naapuria muuttuu eläväksi.

Toteuta metodi hoidaSolu(int x, int y, int elossaOleviaNaapureita) ylläolevien sääntöjen mukaan. Kannattaa ohjelmoida ja testata yksi sääntö kerrallaan!

Kun olet saanut kaikki valmiiksi, voit testata ohjelman toimintaa seuraavalla graafisella simulaattorilla.

package game;

import gameoflife.Simulaattori;

public class Main {

    public static void main(String[] args) {
        OmaAlusta alusta = new OmaAlusta(100, 100);
        alusta.alustaSatunnaisetPisteet(0.7);

        Simulaattori simulaattori = new Simulaattori(alusta);
        simulaattori.simuloi();
    }
}