Kurssin viimeisimmän version löydät täältä: O1: 2024
Luku 2.8: Vuorovaikutteista grafiikkaa
Tästä sivusta:
Pääkysymyksiä: Miten saan graafisen sovellukseni eloon? Miten saan sen reagoimaan käyttäjän klikkauksiin, napinpainalluksiin ja niin edelleen?
Mitä käsitellään? Erilaisia käyttöliittymätapahtumia. Tapahtumankäsittelijät graafisessa käyttöliittymässä.
Mitä tehdään? Konkreettisia asioita. Luku koostuu lähinnä esimerkkiohjelmien kuvauksista ja ohjelmointitehtävistä.
Suuntaa antava työläysarvio:? Tunti tai enemmän. Luku sisältää paljon täysin vapaaehtoisia lisätehtäviä.
Pistearvo: A65.
Oheisprojektit: Pikkusovelluksia, FlappyBug.
Johdanto
Tässä luvussa saadaan ötökkä liikkeelle ja muutakin mukavaa. Mutta aloitetaan yksinkertaisesta esimerkistä.
Lukumäärälaskuri
Osassa tämän luvun ohjelmista tarvitsemme yksinkertaista laskuria, jolla voi pitää kirjaa mistä tahansa yksittäin kasvavasta lukumäärästä, vaikkapa käyttäjän tekemistä klikkauksista.
Tässä luodaan nollasta alkava laskuri, jonka arvo kasvaa aina yhdellä kerrallaan, kun
laskurin vaikutuksellista etene
-metodia kutsutaan:
import o1.laskuri.Laskuriimport o1.laskuri.Laskuri val ekaLaskuri = new Laskuri(0)ekaLaskuri: o1.laskuri.Laskuri = arvo 0 ekaLaskuri.arvores0: Int = 0 ekaLaskuri.etene()ekaLaskuri.arvores1: Int = 1 ekaLaskuri.etene()ekaLaskuri.etene()ekaLaskuri.arvores2: Int = 3
Alkuarvo voi olla muukin kuin nolla:
val tokaLaskuri = new Laskuri(100)tokaLaskuri: o1.laskuri.Laskuri = arvo 100 tokaLaskuri.etene()tokaLaskuri.etene()tokaLaskuri.etene()tokaLaskuri.arvores3: Int = 103
Kuvatulla tavalla toimiva laskuriluokka on helppo toteuttaa aiemmista luvuista tutuin konstein:
class Laskuri(var arvo: Int) {
def etene() = {
this.arvo = this.arvo + 1
}
override def toString = "arvo " + this.arvo
}
Uusi muuttujan rooli: askeltaja
Laskuriluokan arvo
-ilmentymämuuttujalla on hieman erilainen rooli kuin millään
muulla tähän mennessä kohdatulla muuttujalla (vrt. luvun 2.6 roolit). Sen arvo
kasvaa alkuarvosta yksi kerrallaan. Sekvenssi on samalla alkuarvolla aina sama,
esimerkiksi 0, 1, 2, 3, jne.
Askeltaja (stepper) on muuttuja, jonka arvo "astelee" tiettyä sekvenssiä pitkin. Arvosekvenssi on ennalta määrätty, kunhan tiedetään lähtötilanne. Usein kyseessä ovat kokonaisluvut, joita käydään läpi nousevassa tai laskevassa järjestyksessä yksi kerrallaan, mutta askellettava sekvenssi voi olla jokin muukin.
Askeltajat ovat ohjelmissa yleisiä. Laskuriluokassa nähtiin hyvin tyypillinen käyttötarkoitus askeltajalle: halutaan pitää kirjaa jostakin kasvavasta lukumäärästä. Toinen tyypillinen käyttö — järjestysnumerojen käsittely — tulee vastaan myöhemmin kurssilla.
Kuten kokoojan (luku 2.6) myös askeltajan uusi arvo riippuu sen vanhasta arvosta. Kuitenkaan askeltajan seuraava arvo ei riipu ulkoisista tekijöistä (esim. annetuista syötteistä) vaan seuraa aina tiettyä ennalta määrättyä sekvenssiä.
Ohjelmani huomaa, kun klikkaan
Tehdään kokeeksi ohjelma, joka osaa laskea klikkauksia ja tehdä näkyviä muutoksia näkymään sitä mukaa kun klikkauksia kertyy.
Käytetään mallina yhtä Laskuri
-oliota. Kirjoitetaan käyttöliittymä, joka piirtää
ruudulle sitä suuremman ympyrän, mitä useammin käyttäjä on näpäyttänyt hiiren nappia.
Muodostetaan aluksi käynnistysolio käyttäen luvun 2.7 jo esittelemiä keinoja:
object Klikkausohjelma extends App {
val klikkauslaskuri = new Laskuri(5)
val tausta = rectangle(500, 500, Black)
val nakyma = new View(klikkauslaskuri) {
def makePic = tausta.place(circle(klikkauslaskuri.arvo, White), new Pos(100, 100))
}
nakyma.start()
}
makePic
-metodi muodostaa kuvan asettamalla taustan päälle
tiettyyn kohtaan valkoisen ympyrän, jonka halkaisija on laskurin
arvon mittainen (eli viisi pikseliä).Jotta saamme käyttöliittymämme reagoimaan hiiren näpäyksiin, meidän on määriteltävä
sille tapahtumankäsittelijä (event handler). Tapahtumankäsittelijä on koodin
osa, joka suoritetaan, kun havaitaan uusi tapahtuma (event); tapahtumia ovat
esimerkiksi hiiren klikkaukset ja liikkeet, näppäimen painallukset ja niin edelleen.
Lisätään View
-oliollemme metodi, joka toimii tapahtumankäsittelijänä:
object Klikkausohjelma extends App {
val klikkauslaskuri = new Laskuri(5)
val tausta = rectangle(500, 500, Black)
val nakyma = new View(laskuri) {
def makePic = tausta.place(circle(klikkauslaskuri.arvo, White), new Pos(100, 100))
override def onClick(klikkauskohta: Pos) = {
klikkauslaskuri.etene()
println("Klikkaus koordinaateissa " + klikkauskohta + "; " + klikkauslaskuri)
}
}
nakyma.start()
}
onClick
.
Voit ajatella asiaa niin, että View
-olio osaa kytätä itseensä
kohdistuvia tapahtumia ja kutsuu tätä omaa metodiaan klikkauksen
sattuessa. Katsotaan metodia kohta tarkemmin.Löydät ohjelman Pikkusovelluksia-projektin luokasta o1.laskuri.Klikkausohjelma
. Aja ohjelma,
klikkaile hiirellä ja havainnoi mitä tapahtuu. Katso sekä graafista ikkunaa että
tekstikonsolia.
Nyt kun olet kokeillut ohjelmaa, tutkitaan tapahtumankäsittelijämetodia:
override def onClick(klikkauskohta: Pos) = {
klikkauslaskuri.etene()
println("Klikkaus koordinaateissa " + klikkauskohta + "; " + klikkauslaskuri)
}
Pos
. Tällöin GUI-kirjastomme osaa kutsua oikeaa metodia
tapahtuman havaitessaan ja välittää sille parametriksi tiedon
koordinaateista, joissa klikkaus havaittiin.override
samaan tapaan kuin
olemme jo tottuneet tekemään toString
-metodille (luku 2.5).
View
-luokassa on nimittäin määritelty valmiiksi erilaisia
tapahtumankäsittelijämetodeita kuten onClick
. Ne tosin
jättävät tapahtumat huomiotta, koska tapa, jolla tapahtumiin
tulisi reagoida ei ole yleisesti määriteltävissä vaan riippuu
sovelluksesta. Tässä korvaamme tuon mitään tekemättömän
oletustoteutuksen sovelluskohtaisella metodillamme.Huomaa, että meidän ei erikseen tarvinnut kutsua onClick
-metodia missään, vaan se
tulee meidän näkökulmastamme automaattisesti kutsutuksi käyttäjän napsauttaessa hiirellä.
Tämän automatiikan meille tarjoaa View
-luokka: kukin View
-olio osaa toimia ns.
tapahtumankuuntelijana (event listener), joka havaitsee itseensä kohdistuvat
tapahtumat ja kutsuu tapahtumankäsittelijämetodeita kuten onClick
.
Ohjelmani huomaa, kun näppäilen
Tapahtumankäsittelijä näppäimille
Näppäimenpainallukseen voi reagoida ihan vastaavasti kuin hiirenklikkaukseen. Tarvitsemme vain hieman erilaisen tapahtumankäsittelijämetodin:
override def onKeyDown(painettu: Key) = {
println("Painettiin näppäintä " + painettu)
}
onKeyDown
.
Parametriksi saadaan Key
-tyyppinen arvo. Eri Key
-oliot
vastaavat näppäimistön eri näppäimiä.FlappyBug-tehtävä (osa 4/16: ötökkä liikkuu)
Ota esiin luvussa 2.7 tehty versio FlappyBugApp
-ohjelmasta. Käynnistysolion luoma
View
osaa jo tuottaa kuvan pelimaailmasta muttei muuta.
- Lisää tuolle
View
-oliolle yllä annettuonKeyDown
-tapahtumankäsittelijä. Aja ohjelma ja totea tekstikonsolin raportoivan näppäimenpainallukset.
- Huomasithan tehtävän edellisestä kohdassa, että vaikka
View
-olioon viittaavan muuttujan nimen voisi muuten valita itse, käytä nyt automaattisen arvioinnin vuoksi nimeägui
?
- Poista tulostuskäsky tapahtumankäsittelijästä. Kutsu sen sijaan
Game
-olionactivateBug
-metodia. Nyt siis (minkä tahansa) näppäimen painalluksen pitäisi aktivoida peliin sisältyvä ötökkä. - Aja muokattu ohjelma. Huomaa:
- Ötökkä liikkuu!
- Ötökkä lentää muutaman painalluksen jälkeen ulos näkyvältä pelialueelta.
- Ötökkä ei putoa lainkaan eikä este liiku.
Aika ei kulu pelimaailmassa. Et ole vielä
kutsunut
Game
-oliontimePasses
-metodia. (Tehdään se kohta.)
- Palauta:
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Ohjelmani tikittää
Tapahtumien ei välttämättä tarvitse seurata suoraan käyttäjän tekemisistä. Eräänlainen tapahtuma on sekin, että hetki kuluu.
Tikitysohjelma
o1.laskuri
-pakkauksesta löytyy klikkausohjelmaa muistuttava Tikitysohjelma
, jossa
laskemme laskurilla klikkausten sijaan "kellonviisarin naksahduksia". Aja se ja katso
alta selitys sen koodista.
object Tikitysohjelma extends App {
val tikityslaskuri = new Laskuri(0)
val tausta = rectangle(500, 500, Black)
val nakyma = new View(tikityslaskuri) {
def makePic = tausta.place(circle(tikityslaskuri.arvo, White), new Pos(250, 250))
override def onTick() = {
tikityslaskuri.etene()
}
}
nakyma.start()
}
onClick
-metodin sijaan kirjoitimme onTick
-metodin. Se on
parametriton.View
-olio käyttää ajastinta, joka synnyttää "kellon naksahduksia" suunnilleen 24 kertaa
sekunnissa. Naksahduksen havaitessaan se kutsuu onTick
-tapahtumankäsittelijää ja laskurimme
arvo kasvaa.
Tehtävä: tikitystä ja pyöritystä
Muokkaa äskeisen ohjelman makePic
-metodia siten, että:
- Käyttäjää "lähestyvä" kuvio ei olekaan laskurin kokoinen ympyrä vaan neliö, jonka sivut ovat laskurin mittaisia.
- Neliö paitsi kasvaa myös pyörii yhden asteen verran myötäpäivään per
tiksahdus. Käytä pyörittämiseen
clockwise
-metodia luvusta 2.3; anna sille parametriksi laskurin arvo.
Kokeile ohjelmaa. Kokeile sitten vielä muuttaa tikitysnopeutta: anna View
-oliolle
toiseksi konstruktoriparametriksi luku 50. Nyt onTick
-metodi tulee kutsutuksi noin
viisikymmentä kertaa sekunnissa, ja kuvio suurenee ja pyörii nopeammin.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Lisäkokeiluja
Kokeile muita nopeuksia. Isommalla vauhdittuu, pienemmällä hidastuu. Nollan ja ykkösen välisellä nopeudella kello tikittää harvemmin kuin kerran sekunnissa.
Kokeile vaihtaa neliön paikalle jokin muu kuva. Skaalaamalla pyörivän face.png
-kuvan
jatkuvasti suuremmaksi saa aikaan melko häiritsevän animaation. Tiedostosta ladatun
kuvan koon säätämiseen voit käyttää joko luvun 2.3 esittelemää scaleBy
-metodia tai
vaihtoehtoisesti scaleTo
-metodia, jolle annetaan parametreiksi kuvan haluttu leveys
ja korkeus pikseleinä.
FlappyBug-tehtävä (osa 5/16: ajankulua)
Toteuta FlappyBugApp
iin onTick
-metodi, joka kellon naksahtaessa yksinkertaisesti
kutsuu näkymän kuvaaman peliolion timePasses
-metodia.
Kokeile.
Palauta.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
FlappyBug-tehtävä (osa 6/16: jotain rajaa)
Tehtävänanto
FlappyBug alkaa jo muistuttaa peliä mutta kaipaa monenlaista parannusta. Tee nyt nämä parannukset:
- Estä ötökkää nousemasta pelialueen ylärajaa korkeammalle. Sen (keskipisteen) y-koordinaatti ei saa olla negatiivinen.
- Estä ötökkää putoamasta maan pintaa matalammalle. Sen (keskipisteen) y-koordinaatti saa olla korkeintaan 350.
Suosittelemme seuraavaa kätevää ja eleganttia ratkaisutapaa. Muitakin toki on, kuten ohjelmoinnissa yleensäkin.
Vaihe 1: uusi apumetodi
Lisää Bug
-luokkaan vaikutuksellinen metodi move
:
- Se ottaa parametrikseen
Double
-arvon, joka kertoo, paljonko ötökän y-koordinaattiin lisätään. - Se vaihtaa ötökän sijainniksi uuden koordinaattiparin, joka saadaan tekemällä vanhaan parametrin kokoinen lisäys. Positiivinen parametri siis siirtää ötökkää alas ja negatiivinen ylös.
Vaihe 2: refaktorointi
Toteuta aiemmin laatimasi metodit flap
ja fall
uusiksi siten, että kumpikin niistä
kutsuu move
-metodia antaen tarkoitukseen sopivan parametrin. Metodien on siis tarkoitus
saada aikaan ihan sama vaikutus kuin ennenkin, mutta ne toteutetaan nyt toisella
tavoin. Kumpikin metodeista on toteutettavissa erittäin yksinkertaisesti move
-metodia
kutsumalla.
Komeasti sanottuna se, mitä nyt teet, on refaktorointia (refactoring). Refaktoroinnilla tarkoitetaan ohjelman muokkaamista laadun parantamiseksi toiminnallisuutta muuttamatta. Refaktorointi voi esimerkiksi parantaa ohjelman muokattavuutta.
Refaktoroidessa on viisasta testata, ettei mikään mennyt rikki (ns. regressiotestaus). Tässä meille riittäköön testaukseksi ohjelman koekäyttö. Kokeile ohjelmaasi; sen pitäisi toimia niin kuin ennen.
Vaihe 3: tutustu clampY
-metodiin
Kaikilla Pos
-olioilla on clampY
-metodi. Sille annetaan parametreiksi kaksi lukua,
jotka määräävät y-koordinaatin ala- ja ylärajan:
val testi = new Pos(10, 50)testi: Pos = (10.0,50.0) testi.clampY(5, 30)res4: Pos = (10.0,30.0) testi.clampY(100, 200)res5: Pos = (10.0,100.0) testi.clampY(0, 100)res6: Pos = (10.0,50.0)
clampY
-palauttaa uuden sijainnin, jonka x-koordinaatti on sama
kuin alkuperäinen mutta jonka y-koordinaatti on liiskattu halutulle
välille. Tässä välinä oli 5–30, joten liian iso y-koordinaatti
on vaihdettu ylärajaksi 30.clampX
On olemassa myös vastaava clampX
-metodi, jota et kuitenkaan tarvitse nyt.
testi.clampX(100, 200)res7: Pos = (100.0,50.0)
Vaihe 4: varsinainen ratkaisu
Noilla pohjustuksilla varsinainen ratkaisu on muutaman merkin mittainen.
Tarkoitus oli toisaalta estää y-koordinaattia kasvamasta liian isoksi, toisaalta liian
pieneksi. Molemmat kärpäset liiskautuvat yhdellä kertaa, kun puristat ötökän sijainnin
halutulle välille clampY
-metodilla.
Tee tuo pieni lisäys move
-metodiin.
Entä flap
ja fall
? Jos toteutit ne vaiheen 2 ehdottamalla tavalla, niin myös ne
huomioivat nyt koordinaatin rajat, koska niiden toteutus perustuu move
-metodiin,
joka hallinnoi ötökän liikkeitä keskitetysti.
Palauttaminen
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
FlappyBug-tehtävä (osa 7/16: vauhdin hurmaa)
Eihän tuollainen paikallaan hyppeleminen sovi ötökälle.
Voisimme pistää ötökän liikkumaan myös x-suunnassa. Mutta kokeillaan nyt toista tapaa
tuoda liikkeen tuntua peliimme: laitetaan tausta liikkumaan oikealta vasemmalle. Tässä
kätevänä apuna toimii eräs Pic
-luokan metodi:
Pohjustus: shiftLeft
val ympyra = circle(200, Red)ympyra: Pic = circle-shape val siirretty = ympyra.shiftLeft(25)siirretty: Pic = circle-shape (transformed) show(siirretty)
Oheisessa kuvassa näkyy shiftLeft
in tuottama uusi kuva, jossa alkuperäistä on
siirretty parametrin ilmoittama pikselimäärä vasemmalle. Samalla vasemmasta reunasta
"kadonnut" osio on kiinnitetty oikeaan reunaan.
Aavistat jo varmaan idean: otetaan tuttu maisemakuva mutta siirretään sitä kellon tikittäessä vähitellen vasemmalle, jolloin saadaan aina uusi versio maisemasta taustakuvana käytettäväksi.
Tausta liikkeelle
Tee kolme muutosta FlappyBugApp
-ohjelmaan.
Luo
View
-oliolle uusi ilmentymämuuttuja, nimeltään esimerkiksibackground
. Alusta sescenery
n arvolla:var background = scenery
Huomaa, että kyseessä on
var
. Käytämme tätä muuttujaa pitämään kirjaa siitä, mitä kuvaa parhaillaan käytetään taustakuvana. Aluksi taustana on se tuttu maisemakuva, jossa on puu keskellä.Muistutus: jotta
background
ista tulisiView
-olion ilmentymämuuttuja, määrittele seView
-olion määrittelyn sisällä. Älä kuitenkaan kirjaa sitämakePic
in tai muunkaan metodin sisään — silloin siitä tulisi metodin paikallinen muuttuja eikä se olisi olemassa kuin tuon metodin suorituksen aikana.Lisää
onTick
-metodiin tämä käsky, joka päivittää taustaa:this.background = this.background.shiftLeft(2)
Taustaa siis siirretään pari pikseliä vasemmalle aina kellon lyödessä.
Jos haluat, voit myös määritellä vakion (luku 2.6) ja käyttää sitä maagisen arvon 2 sijaan.
Äskeiset käskyt kyllä pyörittelevät taustakuvaa muuttujassa, mutta jos kokeilet ohjelmaasi nyt, niin mitään havaittavaa muutosta ei näy. Metodi
makePic
nimittäin edelleen muodostaa näkymän käyttäen alkuperäistäscenery
-kuvaa. Vaihda sieltäscenery
background
iksi ja voilà!
Palauttaminen
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
FlappyBug-tehtävä (osa 8/16: ötökkä kiihtyy)
Ötökkämme putoaa hitaasti, tasavauhtia. Laitetaan se liikkumaan siiveniskujen välillä kiihtyvää vauhtia kohti maata. Perusideana olkoon tämä:
- Ötökällä on nopeus, joka ilmoittaa, montako pikseliä se liikkuu pystysuunnassa kullakin kellonlyömällä. Positiivinen nopeus tarkoittaa liikettä alaspäin ja negatiivinen ylöspäin.
- Ötökän siiveniskut antavat sille vauhdin ylöspäin. Ötökkä ei siis välittömästi siirry mihinkään vaan sen nopeus muuttuu (ks. tarkemmin alta).
- Joka kellonlyömällä ötökän nopeuteen lisätään kaksi eli se tulee kiskotuksi aina vain kovempaa alaspäin.
Tarkemmat ohjeet
Muuta Bug
-luokkaa seuraavasti:
- Lisää ilmentymämuuttuja, joka pitää kirjaa ötökän tämänhetkisestä
nopeudesta pystysuunnassa: montako pikseliä ötökkä nousee tai
putoaa kullakin viisariniskulla.
- Laita nopeusmuuttujan alkuarvoksi 0.0.
- Laita nimeksi
yVelocity
.
- Muuta
flap
-metodia siten, että se ei välittömästi siirräkään ötökkää ylöspäin tai muuallekaan. Sen sijaan siipien lyöminen antaa ötökälle vauhtia ylöspäin. Metodi ottaa edelleen lyönnin voimakkuuden parametriksi, mutta tuo parametri kertookin nyt uuden ylöspäin suuntautuvan nopeuden. Esimerkiksi kutsuttaessa metodia parametriarvolla 15, ötökän nopeudeksi tulee -15. Sijoita tuo arvo nopeusmuuttujaanyVelocity
.Game
-luokanactivateBug
-metodista pitää (edelleen) kutsuaBug
-luokanflap
-metodia juuri tuolla parametriarvolla 15. Mutta se, mitäflap
-metodi tuolla parametriluvullaan tekee, muuttuu.
- Muuta
fall
-metodia siten, että se:- Ensin kasvattaa
yVelocity
-muuttujan arvoa: uudeksi arvoksi tulee vanha arvo plus kaksi. - Sitten siirtää ötökkää
yVelocity
-muuttujan uuden arvon verran (ylös tai alas; riippuu nykyisestä arvosta). Jos toteutit luokkaanmove
-apumetodin aiemmassa tehtävässä, niin tämä hoituu sitä kutsumalla.
- Ensin kasvattaa
Palauttaminen
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Jatkamme FlappyBugin kanssa luvussa 3.1.
Ohjelmani huomaa hiiren (ym.)
Seuraavissa vapaaehtoisissa tehtävissä muun muassa reagoidaan hiiren liikkeisiin, mikä ei ole kurssin pisteytettyjen tehtävien kannalta välttämätöntä mutta mitä varmaan haluat silti kokeilla.
Lisätehtävä: jotain kiinni hiiressä
Tässä leluesimerkissä mallinnamme takiaista:
class Takiainen {
var sijainti = new Pos(0, 0) // tuoreimman säilyttäjä
}
Takiaisolion ainoa ominaisuus on siis sen sijainti, jota voi muuttaa:
import o1.takiainen.Takiainenimport o1.takiainen.Takiainen val testi = new Takiainentesti: o1.takiainen.Takiainen = o1.takiainen.Takiainen@34ece05e testi.sijaintires8: Pos = (0.0,0.0) testi.sijainti = new Pos(10, 50)testi.sijaintires9: Pos = (10.0,50.0)
Pikkusovelluksia-projektista löytyy paitsi tuo takiaisluokka myös tiedosto
Takiaisohjelma1.scala
. Täydennä tiedostossa olevaa koodinraakiletta siten,
että syntyy seuraavanlainen sovellus:
Mallina on yksi takiaisolio.
Sovelluksen käyttöliittymänä on näkymä, joka piirtää annettua valkoista taustakuvaa (
tausta
) vasten takiaisen koordinaatteihin vihertävän ympyrän (takiaisenKuva
).makePic
-metodin on palautettava sellainen kuva, jossa takiaisen kuva on oikeassa kohdassa taustakuvan päällä.- Anna tässä ohjelmassa
View
-olioon viittaavalle muuttujalle nimeksinakyma
. (Tämä ei ole muuten tärkeää mutta helpottaa automaattista arviointia.)
Näkymään on liitetty tapahtumankäsittelijämetodi seuraavasti:
- Metodin nimi on
onMouseMove
ja parametrina yksi hiiren sijainnin kertovaPos
-olio (aivan kuinonClick
-metodilla ylempänä). - Metodi sijoittaa takiaiselle uudeksi sijainniksi hiiren kursorin koordinaatit. Takiainen siis seuraa hiirtä.
- Voit lisäksi lisätä metodin runkoon käskyn, joka tulostaa parametriarvon, niin näet ohjelmaa kokeillessasi miten usein tuo metodisi tulee kutsuttua, kun siirtelet hiirtä ikkunan päällä.
Huomaa, että
onMouseMove
-tapahtumankäsittelijämetodi ei tee kuvilla mitään. OnmakePic
-metodin tehtävä muodostaa kuva käyttäen apuna sitä viimeisintä koordinaattiparia, jonkaonMouseMove
on takiaiselle sijoittanut.- Metodin nimi on
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Jatko edelliselle: takiaisesta tähtäimeksi
Piirretään ympyrän sijaan kaksi viivaa, jotka leikkaavat liikkuvan kursorin kohdalla.
Viivoja on helppo luoda o1
-pakkauksen line
-funktiolla (vrt. circle
, rectangle
jne.). Tässä pieni esimerkki:
val viiva = line(new Pos(0, 0), new Pos(150, 100), Red)viiva: Pic = line-shape val tausta = circle(200, LightBlue)tausta: Pic = circle-shape val kuva = tausta.place(viiva, new Pos(20, 20))kuva: Pic = combined pic
Ota esiin Takiaisohjelma2.scala
. Kopioi sinne pohjaksi edelliseen tehtävään
tekemäsi ratkaisu. Muuta makePic
-metodia siten, että se ei piirräkään "takiaisen
kuvaa" vaan asemoi taustaa vasten kaksi mustaa (Black
) viivaa:
- Yksi viivoista alkaa yläreunasta suoraan kursorin sijainnin yläpuolelta ja piirtyy pystysuoraan aina alareunaan saakka.
- Toinen viiva alkaa vasemmasta reunasta suoraan kursorin sijainnin vasemmalta puolelta ja piirtyy oikeaan reunaan saakka.
Ohjeita ja vinkkejä:
- Kun asemoit viivan taustaa vasten esimerkiksi
place
-metodilla, niin viivan alkupää — ei keskikohta — tulee antamiisi koordinaatteihin. onMouseMove
-metodia ei ole syytä muuttaa.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Jatkotehtävä: Pos
-laskentaa
Laadi tiedostoon Takiaisohjelma3.scala
edellisiä muistuttava sovellus, jossa
näkyy takiaispalluran ja ristiviivojen sijaan yksi ainoa musta viiva, joka piirtyy
aina näkymän keskikohdasta hiiren kursoria kohti mutta vain puolet matkasta. Viiva
siis ikään kuin "osoittaa" keskeltä näkymää kursoria kohti kuitenkaan yltämättä
kursoriin.
Tehtävä vaatii hieman koordinaateilla laskemista ja ratkeaa kätevämmin, jos käytät
apuna Pos
-luokan metodeita add
, multiply
ja/tai divide
:
- Kokeile kutsua
add
-metodia siten, että annat parametriksi viittauksen toiseenPos
-olioon:sijainti1.add(sijainti2)
. - Voit myös kertoa tai jakaa molemmat koordinaatit luvulla:
sijainti1.multiply(luku)
taisijainti1.divide(luku)
.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Jatkotehtävä: tikittävä takiainen
Laadi tiedostoon Takiaisohjelma4.scala
sovellus, jossa takiaista kuvaava
pallura seuraa kursoria kuten ensimmäisessäkin näistä ohjelmista. Kuitenkin tällä
kertaa pallura ei välittömästi ilmesty sinne, missä kursori on vaan lipuu kursoria
kohti vähitellen.
Kopioi pohjaksi ensimmäiseen takiaisohjelmaan kirjoittamasi koodi ja muuta sitä seuraavasti:
- Lisää
View
-oliolle ilmentymämuuttuja, jossa pidät kirjaa tuoreimmasta havaitusta hiirikursorin sijainnista. Aluksi se voi osoittaa esimerkiksi koordinaatteihin (0,0). Muuttujan nimi voi olla esimerkiksiviimeisinKursori
. - Muokkaa
onMouseMove
-metodia siten, että se ei tee muuta kuin sijoittaa vastaanottamansa parametriarvon (eli kursorin senhetkisen sijainnin)viimeisinKursori
-muuttujan arvoksi. Tässä ohjelmassa hiiren liikuttaminen ei siis suoraan liikuta takiaista vaan vain pistää talteen sen, mihin hiiri viimeksi liikkui. - Lisää
onTick
-metodi, joka siirtää takiaista. Uusi sijainti saadaan laskemalla piste, joka on 10 % matkasta takiaisen aiemmasta sijainnista kohti viimeisintä kursorin sijaintia.- Esimerkiksi jos takiainen on ennestään
sijainnissa (10,20) ja
viimeisinKursori
-muuttujassa on sijainti (100,100), niin takiaisen uudeksi sijainniksi tulee (19,28). - Tämän laskutoimituksen tekemisessä voit hyödyntää askeisessä
tehtävässä mainittuja
Pos
-luokan metodeita.
- Esimerkiksi jos takiainen on ennestään
sijainnissa (10,20) ja
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Lisätehtävä: piirto-ohjelma
Tutustu alla olevaan pseudokoodiin luokasta, joka kuvaa "taideprojekteja" eli kuvia, joihin vähä vähältä lisätään väripisteitä. On tässä pseudokoodissa valmistakin Scalaa jo pitkälti mukana:
class Taideprojekti(tausta: Pic) { var kuva = tausta // kokooja var pensseli = circle(10, Black) // tuoreimman säilyttäjä def piirra(mihin: Pos) = { Sijoita kuva-muuttujalle uusi arvo, joka saadaan asemoimalla sen aiemman arvon päälle pensselin kuva mihin-parametrin osoittamaan kohtaan. } }
kuva
-muuttuja on rooliltaan kokooja (luku 2.6): aluksi
sen arvona on vain taustakuva mutta vähitellen taustan päälle
lisätään pieniä kuvia, "pensselinjälkiä". Muuttuja siis
viittaa kuvaan, joka on taustakuvan ja kaikkien lisättyjen
pensselijälkien yhdistelmä. (Näin on ainakin tarkoitus,
mutta pensselinjälkiä lisäävä metodi on vielä
toteuttamatta.)piirra
-metodia kutsutaan. Oletusarvoisesti
pensselinä on pieni musta ympyrän kuva.o1.taide
tiedostoon Taideprojekti.scala
.Samasta pakkauksesta löytyy myös käynnistysolio Piirustusohjelma
. Tutustu siihen;
huomaat, että se luo näkymän mallina toimivaan Taideprojekti
-olioon. Annettu koodi
on hyvä alku mutta tapahtumankäsittelijä puuttuu. Joten:
- Toteuta
Taideprojekti
-luokanpiirra
-metodille runko. - Lisää
Piirustusohjelma
lle tapahtumankäsittelijäonMouseMove
, joka yksinkertaisesti kutsuu näkymässä näkyvälle teoksellepiirra
-metodia lisätäkseen (place
) pensselinjäljen hiiren kursorin kohdalle. - Kokeile.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Hieman haastavampi jatko: värejä piirto-ohjelmaan
Muokkaa äskeistä piirto-ohjelmaa siten, että hiiren klikkaus vaihtaa piirtoväriä.
Muokkaa ensin Taideprojekti
-luokkaa siten, että se pitää kirjaa valitusta
piirtoväristä ja vaihtaa pyydettäessä seuraavaan. Tee siis seuraavat muutokset:
Lisää alkuun kaksi ilmentymämuuttujaa:
var varinumero = 0 val paletti = Buffer(Black, Red, Green, Blue)
Muuttuja
paletti
osoittaa kokoelmaan värejä, joiden välillä käyttäjä voi vaihdella.Yksi paletin väreistä on kerrallaan aktiivisena; muuttuja
varinumero
kertoo, monesko. Tässä tehtävässä tuota muuttujaa on tarkoitus käyttää askeltajana, joka käy läpi paletin indeksejä järjestyksessä palaten aina lopusta alkuun: 0, 1, 2, 3, 0, 1, 2, 3, 0, 1, jne.Toteuta luokkaan parametriton ja vaikutukseton metodi
piirtovari
, joka palauttaa parhaillaan valittuna olevan piirtovärin. Siis sen paletin väreistä, johonvarinumero
osoittaa (aluksi musta).Toteuta vaikutuksellinen metodi
vaihdaVaria
, joka:- Vaihtaa
varinumero
-muuttujan arvoksi seuraavan luvun. (Käytä nollaan palaamiseen modulo-operaattoria.) - Vaihtaa
pensseli
-muuttujan arvoksi uuden samankokoisen mutta erivärisen ympyrän. Käytäpiirtovari
-metodia apuna. - On parametriton (mutta määritellään tyhjillä kaarisulkeilla, koska on vaikutuksellinen; luku 2.6).
- Vaihtaa
Lisää sitten käyttöliittymään tapahtumankäsittelijä onClick
, joka (parametrinsa
arvosta välittämättä) kutsuu teoksen vaihdaVaria
-metodia. Voit ottaa mallia tämän
luvun alun klikkausohjelmasta.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Jatkoa edelliselle: lisää käyttöliittymätapahtumista
Muokkaa äskeisessä tehtävässä laatimaasi ohjelmaa siten, että väriä ei vaihdetakaan aina seuraavaan, kun käyttäjä klikkaa. Sen sijaan käyttäjän peräkkäisten klikkausten lukumäärä määrää uuden piirtovärin: yksi klikkaus valitsee ensimmäisen värin, tuplaklikkaus toisen, kolmoisklikkaus kolmannen ja niin edelleen.
Muokkaa ensin Taideprojekti
-luokkaa. Lisää vaihdaVaria
-metodille Int
-parametri.
Muuta metodia niin, että se ei askellakaan seuraavaan väriin vaan asettaa
varinumero
n saamansa parametriarvon perusteella.
Korvaa sitten luokan Piirustusohjelma
tapahtumankäsittelijä onClick
tällä uudella
versiolla:
override def onClick(klikkaustapahtuma: MouseClicked) = {
teos.vaihdaVaria(klikkaustapahtuma.clicks)
}
Aiemmassa versiossa parametriksi saatiin vain klikkauksen sijainti (Pos
). Sellainen
riittää moneen tarkoitukseen. Jos kuitenkin haluamme tarkempia tietoja tapahtumasta,
kuten vaikkapa peräkkäisten klikkausten lukumäärän, voimme...
MouseClicked
.MouseClicked
-oliolta voi kysellä monenlaisia tietoja.
Tässä meille riittää clicks
, joka on napsautusten
lukumäärän kertova kokonaisluku.Testaa ohjelmaasi. Kokeile myös mitä tapahtuu, jos klikkaat lukuisia kertoja peräkkäin.
Huomioitko tämän mahdollisuuden vaihdaVaria
-metodissa? Jos et, huomioi se jotenkin;
jos kyllä, voit silti kokeilla mitä tapahtuu, jos tuon jättää huomioimatta.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Halutessasi voit jatkaa tapahtumankuuntelijoiden ja käyttöliittymätapahtumien
tutkimista. Esimerkiksi View
-luokan dokumentaatiosta
löydät luettelon käytettävissä olevista on
-alkuisista tapahtumankäsittelijämetodeista.
Yhteenvetoa
- Kun käyttäjä vuorovaikuttaa graafisen käyttöliittymän kanssa, syntyy ns. GUI-tapahtumia. Tapahtuma voi olla esimerkiksi näppäimen painallus tai hiiren liikahdus. Tapahtumiksi voidaan lukea myös sovelluksen sisäisen kellon “lyönnit”.
- Tapahtumankäsittelijäksi sanotaan aliohjelmaa, jolle tieto
tapahtumasta välitetään ja joka määrittää, mitä ohjelma
tällöin tekee.
- Tapahtumankäsittelijä voi saada parametrina lisätietoa tapahtumasta, esim. sijannin.
- Tällä kurssilla käyttämällemme luokalle
View
voi kirjoittaa tapahtumankäsittelijöitä.
- Graafisten ohjelmien tekeminen on aika kivaa.
- Lukuun liittyviä termejä sanastosivulla: malli, käyttöliittymä; graafinen käyttöliittymä eli GUI; käyttöliittymätapahtuma, tapahtumankäsittelijä, tapahtumankuuntelija; refaktoroida; askeltaja.
Saa luoda!
Tämän luvun välineillä voi tehdä paljon muutakin kuin mitä yllä ehdotettiin. Sen kun kokeilet! Muokkaa jotakin esitellyistä ohjelmista tai keksi jotain ihan omaa.
Palaute
Huomaathan, että tämä on henkilökohtainen osio! Vaikka olisit tehnyt lukuun liittyvät tehtävät parin kanssa, täytä palautelomake itse.
Tekijät
Tämän oppimateriaalin kehitystyössä on käytetty apuna tuhansilta opiskelijoilta kerättyä palautetta. Kiitos!
Kierrokset 1–13 ja niihin liittyvät tehtävät ja viikkokoosteet on laatinut Juha Sorva.
Kierrokset 14–20 on laatinut Otto Seppälä. Ne eivät ole julki syksyllä, mutta julkaistaan ennen kuin määräajat lähestyvät.
Liitesivut (sanasto, Scala-kooste, usein kysytyt kysymykset jne.) on kirjoittanut Juha Sorva sikäli kuin sivulla ei ole toisin mainittu.
Tehtävien automaattisen arvioinnin ovat toteuttaneet Riku Autio, Jaakko Kantojärvi, Teemu Lehtinen, Timi Seppälä, Teemu Sirkiä ja Aleksi Vartiainen.
Lukujen alkuja koristavat kuvat ja muut vastaavat kuvituskuvat on piirtänyt Christina Lassheikki.
Yksityiskohtaiset animaatiot Scala-ohjelmien suorituksen vaiheista ovat suunnitelleet Juha Sorva ja Teemu Sirkiä. Niiden teknisen toteutuksen ovat tehneet Teemu Sirkiä ja Riku Autio käyttäen Teemun toteuttamia Jsvee- ja Kelmu-työkaluja.
Muut diagrammit ja materiaaliin upotetut vuorovaikutteiset esitykset on laatinut Juha Sorva.
O1Library-ohjelmakirjaston ovat kehittäneet Aleksi Lukkarinen ja Juha Sorva. Useat sen keskeisistä osista tukeutuvat Aleksin SMCL-kirjastoon.
Opetustapa, jossa käytämme O1Libraryn työkaluja (kuten Pic
) yksinkertaiseen graafiseen
ohjelmointiin on saanut vaikutteita tekijöiden Flatt, Felleisen, Findler ja Krishnamurthi
oppikirjasta How to Design Programs sekä Stephen Blochin oppikirjasta Picturing Programs.
Oppimisalusta A+ on luotu Aallon LeTech-tutkimusryhmässä pitkälti opiskelijavoimin. Pääkehittäjänä toimii tällä hetkellä Jaakko Kantojärvi, jonka lisäksi järjestelmää kehittävät useat tietotekniikan ja informaatioverkostojen opiskelijat.
Kurssin tämänhetkinen henkilökunta on kerrottu luvussa 1.1.