Tämä materiaali on lisensoitu Creative Commons BY-NC-SA-lisenssillä, joten voit käyttää ja levittää sitä vapaasti, kunhan alkuperäisten tekijöiden nimiä ei poisteta. Jos teet muutoksia materiaaliin ja haluat levittää muunneltua versiota, se täytyy lisensoida samanlaisella vapaalla lisenssillä. Materiaalien käyttö kaupalliseen tarkoitukseen on ilman erillistä lupaa kielletty.
Tekijät: Arto Vihavainen ja Matti Luukkainen
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.
Saat tehtävärungon mukana luokan Tiedostonkasittelija
joka sisältää metodirungot tiedoston lukemista ja tiedostoon kirjoittamista varten.
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
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.
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.
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
.
Tee sanakirjalle parametriton konstruktori sekä metodit:
public void lisaa(String sana, String kaannos)
public String kaanna(String sana)
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.
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ä.
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
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.
Tässä tehtävässä nivotaan yhteen aiemmin harjoiteltuja hajautustauluja, tiedoston lukemista sekä tehdään pieni askel koneoppimisen suuntaan. Koneoppiminen on tietojenkäsittelytieteen osa-alue, missä tutkitaan ja rakennetaan ohjelmia, jotka voivat oppia muunmuassa niille annetusta datasta.
Käytössämme oleva data (kansiossa src oleva tiedosto arviot.txt) sisältää yli 8000 englanninkielistä elokuva-arviota, joihin on valmiiksi lisätty tunnearvio. Tunnearviot on annettu skaalalla nollasta neljään, missä arvot ovat seuraavat:
Teemme seuraavaksi ohjelman, joka pyrkii arvioimaan liittyykö tekstimuotoiseen elokuva-arvioon negatiivinen, positiivinen vai neutraali tunne.
Toteuta tehtäväpohjassa annettuun luokkaan TunteikkaatArviot
metodi public int sanojenLukumaara(String sana)
. Metodin tulee kertoa sille parametrina annetun merkkijonon sana
esiintymislukumäärä luokan konstruktorille annetussa merkkijonolistassa.
Kannattanee tässä jo miettiä minkälainen tietorakenne olisi hyvä sanojen lukumäärän tallentamiseen. Saat merkkijonoon liittyvät yksittäiset sanat selville esimerkiksi String-luokan split-metodin avulla:
String merkkijono = "hei kaikki siellä"; String[] palat = merkkijono.split(" ");
Voit kokeilla ohjelmasi toimintaa esimerkiksi seuraavalla koodilla:
List<String> rivit = lueRivit("src/arviot.txt"); TunteikkaatArviot arviot = new TunteikkaatArviot(rivit); System.out.println(arviot.sanojenLukumaara("what")); System.out.println(arviot.sanojenLukumaara("is")); System.out.println(arviot.sanojenLukumaara("love")); System.out.println(arviot.sanojenLukumaara("chuck")); System.out.println(arviot.sanojenLukumaara("norris")); System.out.println(arviot.sanojenLukumaara("mikkihiiri"));
Ylläoleva esimerkki tuottaa seuraavanlaisen tulostuksen.
338 2538 172 2 1 0
Huom! Käsittele pienellä ja isolla kirjoitetut sanat samoina sanoina!
Tehtäväpohjassa annetut tiedostot arviot-lyhyt-1.txt
, arviot-lyhyt-2.txt
ja arviot.txt
sisältävät elokuva-arvioita. Tiedostojen muoto on seuraavanlainen, missä jokaisen rivin ensimmäinen arvo on arvioon liitetty tunnearvo skaalalla nollasta neljään. Tätä seuraa konkreettinen tekstimuotoinen arvio. Esimerkiksi:
1 Simply put , there should have been a more compelling excuse to pair Susan Sarandon and Goldie Hawn . 3 Definitely in the guilty pleasure B-movie category , Reign of Fire is so incredibly inane that it is laughingly enjoyable . 3 It 's an experience in understanding a unique culture that is presented with universal appeal . 0 The French director has turned out nearly 21\/2 hours of unfocused , excruciatingly tedious cinema that , half an hour in , starts making water torture seem appealing .
Yllä on kuvattu neljän eri elokuvan saamat arviot, sekä niihin liitetyt tunnearvot. Ensimmäinen arvio on hieman negatiivinen, kaksi seuraavaa hieman positiivisia, ja viimeinen on negatiivinen.
Toteuta tässä osiossa metodiin public double sananTunne(String sana)
toiminnallisuus, joka palauttaa parametrina annetulle sanalle keskimääräisen tunnearvon.
Keskimääräinen tunnearvo lasketaan niiden arvioiden keskiarvona, joissa sana esiintyy. Jos sana esiintyy useaan kertaan arviossa, tulee arvio ottaa useampaan kertaan huomioon. Jos sanaan ei esiinny kertaakaan, palauta neutraali arvo, eli 2.0.
Ylläolevassa esimerkissä sana "it" esiintyy kahdesti, kummassakin lauseessa arvio on 3. Sanan "it" keskimääräiseksi tunnearvoksi tulee siis (3+3) / 2 = 3. Vastaavasti sana "that" esiintyy kolmesti, ja tunnearvoksi tulee (3+3+0) / 3 = 2.
Voit kokeilla ohjelmasi toimintaa esimerkiksi seuraavalla koodilla:
List<String> rivit = lueRivit("src/arviot.txt"); TunteikkaatArviot arviot = new TunteikkaatArviot(rivit); System.out.println(arviot.sananTunne("poor")); System.out.println(arviot.sananTunne("is")); System.out.println(arviot.sananTunne("love")); System.out.println(arviot.sananTunne("damme")); System.out.println(arviot.sananTunne("norris"));
Ylläoleva esimerkki tuottaa seuraavanlaisen tulostuksen.
0.8235294117647058 2.0260047281323876 2.645348837209302 2.5 2.0
Toteuta lisäksi myös metodi public String sananTunneMerkkijonona(String sana)
, joka tarkastelee sanaan liittyvää tunnearvoa ja palauttaa tunnearvoon liittyvän merkkijonon. Jos tunnearvo on pienempi tai yhtäsuuri kuin 1.9, tulee palauttaa merkkijono "negatiivinen". Jos taas tunnearvo on pienempi tai yhtäsuuri kuin 2.1, tulee palauttaa merkkijono "neutraali". Muulloin palautetaan merkkijono "positiivinen".
List<String> rivit = lueRivit("src/arviot.txt"); TunteikkaatArviot arviot = new TunteikkaatArviot(rivit); System.out.println(arviot.sananTunneMerkkijonona("poor")); System.out.println(arviot.sananTunneMerkkijonona("is")); System.out.println(arviot.sananTunneMerkkijonona("love")); System.out.println(arviot.sananTunneMerkkijonona("damme")); System.out.println(arviot.sananTunneMerkkijonona("norris"));
negatiivinen neutraali positiivinen positiivinen neutraali
Huom! Käsittele pienellä ja isolla kirjoitetut sanat samoina sanoina!
Toteuta seuraavaksi metodi public double lauseenTunne(String lause)
, joka palauttaa lauseen tunteen. Laske lauseen tunnearvo lauseeseen liittyvien sanojen tunnearvojen keskiarvona.
List<String> rivit = lueRivit("src/arviot.txt"); TunteikkaatArviot arviot = new TunteikkaatArviot(rivit); System.out.println(arviot.lauseenTunne("unicorn is a mythical creature")); System.out.println(arviot.lauseenTunne("chuck norris made a happy meal cry")); System.out.println(arviot.lauseenTunne("the movie was an utter and complete failure"));
2.181146685022733 2.104368086244505 1.73662040170538
Toteuta vielä lopuksi metodi public String lauseenTunneMerkkijonona(string lause)
, joka palauttaa lauseen tunteen merkkijonomuodossa. Käytä tässä samaa muunnosta kuin edellisessä osassa.
List<String> rivit = lueRivit("src/arviot.txt"); TunteikkaatArviot arviot = new TunteikkaatArviot(rivit); System.out.println(arviot.lauseenTunneMerkkijonona("unicorn is a mythical creature")); System.out.println(arviot.lauseenTunneMerkkijonona("chuck norris made a happy meal cry")); System.out.println(arviot.lauseenTunneMerkkijonona("the movie was an utter and complete failure"));
positiivinen positiivinen negatiivinen
Huom! Käsittele pienellä ja isolla kirjoitetut sanat samoina sanoina!
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.
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.
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.
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ä.
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?
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
.
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); }
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ä.
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!
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.
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.
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ää 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); }
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.
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
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
.
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.
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ä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.
Käyttöliittymän pohjana olevassa JFrame
ssa tulee käyttää asettelijana GridLayout
ia 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 JButton
ia, joissa tekstit "+", "-" ja "Z".
Laskimen käyttöliittymän koon on oltava vähintään 300*150.
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 ...
Laajennetaan vielä ohjelmaa seuraavilla ominaisuuksilla:
setEnabled(false)
. Muissa tilanteissa napin tulee olla päällä.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ä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.
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.
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ö.
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.
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.
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);
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.
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);
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ä.
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.
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:
Abstrakti luokka GameOfLifeAlusta
tarjoaa seuraavat toiminnot
Luokassa GameOfLifeAlusta
on lisäksi määritelty seuraavat abstraktit metodit, jotka sinun tulee toteuttaa.
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
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!
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!
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
Jäljellä on vielä metodin hoidaSolu(int x, int y, int elossaOleviaNaapureita) toteuttaminen. GameOfLife-pelin säännöthän olivat seuraavat:
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(); } }