Kurssin viimeisimmän version löydät täältä: O1: 2024
- CS-A1110
- Kierros 12
- Luku 12.3: Käyttöliittymiä Swing-kirjastolla
Luku 12.3: Käyttöliittymiä Swing-kirjastolla
Tästä sivusta:
Pääkysymyksiä: Miten niitä ikkunoita ja nappuloita saadaan ruudulle? Miten laadin graafisen käyttöliittymän ilman kurssin apukirjastoa?
Mitä käsitellään? Scalan Swing-käyttöliittymäkirjasto: käyttöliittymäelementtejä ja niiden asemointia; olion kirjaaminen tapahtumankuuntelijaksi ja tapahtumien käsitteleminen Swingillä.
Mitä tehdään? Luetaan ja kokeillaan itse mukana.
Suuntaa antava työläysarvio:? Puolitoista tuntia. (Tai voit halutessasi ohittaa kokonaan.)
Pistearvo: Vapaaehtoinen luku; ei pisteytettyjä tehtäviä.
Oheisprojektit: SwingExamples (uusi).
Johdanto
Ensin: laamaohjelma
Nouki käyttöön oheisprojekti SwingExamples ja aja sen sisältämä sovellus o1.llama.LlamaApp
.
Käynnistysolio on o1.llama.LlamaApp
. Ohjelmassa on graafinen käyttöliittymä (GUI), jossa on
yksi kolmiosainen ikkuna: ylhäällä tekstiä, keskellä kuva ja alhaalla nappula. Ohjelma reagoi
hiiren näykkäisyihin ja rullaan. Kokeile.
Luvun juoni
Luvuissa 2.7 ja 2.8 opetettiin, että voit mallintaan käyttöliittymän osia olioilla ja laatia metodeita, jotka toimivat tapahtumankäsittelijöinä. Ja siinä jo pitkälti täyttyivätkin kurssin viralliset oppimistavoitteet GUI-ohjelmoinnin osalta. Kuitenkin on hauskaa ja hyödyllistä opetella hieman lisää peruskäsitteitä ja taitoja tästä konkreettisesta aiheesta; siksi tämä vapaaehtoinen luku.
Jo kurssin alussa totesimme, että graafiset käyttöliittymät rakennetaan jollakin tarkoitukseen
sopivalla kirjastolla. Tähän saakka kurssilla olet käyttänyt O1:n omaa kirjastoa, erityisesti
sen luokkia View
ja Pic
. Tässä luvussa opit vähän erilaisten graafisten käyttöliittymien
laatimisesta toisella kirjastolla.
Otetaan avoitteeksi, että ymmärrät, miten esimerkiksi äskeisen laamaohjelman käyttöliittymä on toteutettu. Kuitenkin on järkevää aloittaa yksinkertaisemmista esimerkeistä, joiden kautta opit käsittelemään yksittäisiä graafisen käyttöliittymän rakennusosasia eli käyttöliittymäelementtejä (GUI elements tai GUI components). Edetään seuraavassa järjestyksessä:
- muutama sana GUI:n laatimistyökaluista yleisesti;
- yksittäisiä käyttöliittymien osia REPLissä: nappulat, teksti, yms.; elementtien asemointi ja niiden ominaisuuksien asettaminen;
- tuollaisen käyttöliittymän tallentaminen sovellukseksi;
- tapahtumiin reagoiminen: nappulanpainallukset, hiiren rulla ja liikkeet yms.;
- laamaohjelman käyttöliittymän toteutus;
- toinen esimerkki: käyttöliittymä satunnaislukugeneraattorille.
GUI-kirjastoista
Scala-kielen kirjastoihin lukeutuu graafisten käyttöliittymien laatimiseen tarkoitettu kirjasto nimeltä Swing. Scalalle on olemassa ja kehitteillä Swingin lisäksi myös muita käyttöliittymäkirjastoja, mutta käytämme nyt tässä johdannossa nimenomaan Swingiä.
Swing O1Library:n GUI-työkaluissa
O1Library-projektin sisältämä GUI-työkalusto on itse asiassa rakennettu Swing-kirjastoa käyttäen. O1:n työkaluja ja Swingiä voi myös yhdistellä; niin on tehty esimerkiksi luvun 7.4 Segregation-projektin käyttöliittymässä.
O1:n View
ja Pic
ovat erityisen käteviä, kun haluttu käyttöliittymä on suhteellisen
yksinkertainen ja muodostuu kuvista tai geometrisista kuvioista, joita asemoidaan
suhteessa toisiinsa. Se tarjoaa myös helppokäyttöisen mutta rajallisen tuen tasaisesti
tikittävälle ajalle. Swing on kankeampi käyttää mutta sopii useammanlaisiin tarkoituksiin.
Swing-kirjasto sisältää luokkia, jotka kuvaavat erilaisia GUI-elementtejä: nappuloita, ikkunoita, tekstikenttiä, valikoita jne. Näitä yleishyödyllisiä luokkia voi käyttää ja yhdistellä omissa ohjelmissa. Niistä voi myös periyttämällä (luku 7.3) johtaa omia uudenlaisia elementtityyppejä.
Alla on uusintana lukua 7.3 koristanut kaavio, jossa näkyy eräitä Swing-kirjaston luokkia. Luokat eivät ole vielä tuttuja, mutta useimpiin niistä tutustutaan tässä luvussa.
Mitä muita grafiikkakirjastoja on kuin Swing?
Esimerkiksi Eclipse on tehty SWT-grafiikkakirjastolla. Java-puolella Swingin kilpailijoihin lukeutuu myös JavaFX, jonka Scala-versio on ScalaFX.
Omanlaisiaan työkaluja käyttöliittymien laatimiseen tarjoavat mm. Qt Jambi, Tcl/Tk ja niin edelleen. Funktionaalisessa GUI-ohjelmoinnissa voidaan hyödyntää reaktiivista ohjelmointitapaa.
Kun kyseessä on palvelinohjelmalla toimiva Web-sovellus, niin käyttöliittymän roolissa voi olla web-sivu. Tällaisiin web-sivuihin voi nykyään liittää Scalallakin kirjoitettua dynaamista käyttöliittymäkoodia, koska Scala-koodin voi kääntää selaimessa toimivaksi JavaScriptiksi Scala.js-työkalulla.
Graafisen käyttöliittymän osia
Ensimmäinen ikkunaolio
Kokeillaan elementtien luomista REPLissä. Kokeile itse mukana! Valitse REPLiä käynnistäessäsi projekti SwingExamples.
Aloitetaan luomalla ikkuna eli Frame
. Näistä ikkunaolioista voivat tulla mieleen
o1
-pakkauksen View
-oliot.
import scala.swing._import scala.swing._ val ikkuna = new Frameikkuna: scala.swing.Frame = scala.swing.Frame$$anon$1[frame0,0,0,0x0,invalid,hidden, ... ]
Vaikka ikkunaa ei ruudulle vielä ilmestynytkään, nyt on luotu uusi ikkunaa kuvaava
Frame
-olio. Sen toString
-metodi palauttaa pitkän luettelon erilaisia ikkunan
ominaisuuksia, jotka tulostuvat REPLiin.
Tämän ikkunaolion kykyihin kuuluu, että se osaa huolehtia itseään vastaavien pikselien
piirtämisestä tietokoneen ruudulle. Se ei kuitenkaan vielä tee tätä, koska luotu
Frame
-olio on oletusarvoisesti piilotetussa tilassa. Ikkuna pitää erikseen käskeä
näkyviin. Näkyvyyttä säätelee ikkunaolion visible
-muuttuja:
ikkuna.visible = trueikkuna.visible: Boolean = true
Heti, kun annat tämän käskyn, ikkuna tulee näkyviin. Se voi tosin olla ensin vaikea huomata, koska se on pieni ja voi olla näytön yläkulmassa. (Saatat joutua etsimään sen Windowsin tehtäväpalkista tai vastaavasta paikasta riippuen työskentely-ympäristöstäsi.)
Voit liikuttaa ikkunaa ja muuttaa sen kokoa samaan tapaan kuin käyttämässäsi ympäristössä ikkunoille muutenkin voi tehdä. Kokeile.
Jos tulet näpäyttäneeksi ikkunansulkemisraksia, ja ikkuna poistuu näkyvistä, niin saat sen takaisin näkyviin antamalla viimeksi mainitun käskyn uudestaan.
GUI-elementin ominaisuuden asettaminen
Luodulla ikkunalla ei vielä ole otsikkoa. Kunkin Frame
-olion otsikon määrää
title
-niminen ominaisuus. Sille voi sijoittaa uuden arvon:
ikkuna.title = "Kokeilu"ikkuna.title: String = Kokeilu
Kokeile, niin huomaat, että otsikko piirtyy ruudulle välittömästi.
Nappula ikkunan sisälle
Luodaan ikkunaan nappula. Nappuloita kuvaa luokka Button
, josta on helppo luoda
ilmentymä:
val nappula = new Button("Klikkaa tästä")nappula: scala.swing.Button = scala.swing wrapper scala.swing.Button$$anon$1[,0,0,0x0, ... ]
Jälleen olemme luoneet GUI-elementin, mutta mitään uutta ei ilmesty ruudulle. Tämä johtuu siitä, että vaikka meillä on nyt nappulaolio ja näkyvä ikkuna, niin missään ei ole määritelty, että kyseisen nappulan pitäisi olla juuri kyseisessä ikkunassa.
Jos emme halua ikkunaan mitään muuta kuin yhden nappulan, niin asia hoituu näin:
ikkuna.contents = nappulaikkuna.contents: Seq[scala.swing.Component] = List(scala.swing wrapper scala.swing.Button$$anon$1 ... )
Nyt ikkuna näyttää suunnilleen tältä:
Voit painaa nappulaa. Pinnallisin puolin se toimii, joskaan painamisesta ei varsinaisesti seuraa mitään, koska emme ole määritelleet nappulanpainallustapahtumia käsittelevää metodia. (Siihen palaamme jäljempänä.)
Swing-Frame
lla ei siis ole makePic
-metodia kuten o1.View
-oliolla (luku 2.7). Emme
muodosta Swing-ikkunan "kuvaa" vaan koostamme sen sisällön GUI-elementeistä. Vastaavasti
myös työkalut Swing-elementtien asemointiin ovat toisenlaiset kuin o1.Pic
-olioista tutut
place
, leftOf
, jne.
Elementtien asemointia paneeleilla
Ikkunan contents
-muuttuja määrää, mikä elementti ikkunan varsinaisena sisältönä näkyy
(poislukien esim. yläreunan otsikkorivi, jos sellainen on). Yllä siis määrittelimme, että
ikkunan sisällön muodostaa luomamme nappulaolio.
contents
in arvoksi voi sijoittaa yksittäisen elementin. Kuitenkin yleensä haluaisimme,
että ikkunassa on useita elementtejä. Voisimme haluta luoda vaikkapa tällaisen ikkunan:
Ongelman ratkaisee se, että GUI-elementit voivat olla sisäkkäin. Yhteen liittyvät elementit voidaan ryhmittää yhdeksi paneeliksi (panel), jonka voi asettaa ikkunan sisällöksi. Paneeli on elementti, joka voi sisältää useita toisia elementtejä. Katsotaan äskeistä kuvaa tarkemmin:
scala.swing.Label
.scala.swing.Button
, kuten jo nähtiinkin.Label
iä vaan nämä kaikki sisältävä paneelielementti. Kyseisen
paneelin ominaisuuksiin kuuluu muun muassa se, että sen sisältö
asemoidaan allekkain.Tällaisen ikkunan luominen ei ole vaikeaa. Aloitetaan vaikkapa luomalla halutut nappulat ja tekstilappu:
val ekaNappi = new Button("Paina minua")ekaNappi: scala.swing.Button = scala.swing wrapper scala.swing.Button$$anon$1[,0,0,0x0, ... ] val tokaNappi = new Button("Ei kun MINUA!")tokaNappi: scala.swing.Button = scala.swing wrapper scala.swing.Button$$anon$1[,0,0,0x0, ... ] val kehote = new Label("Paina jompaakumpaa napeista.")kehote: scala.swing.Label = scala.swing wrapper scala.swing.Label$$anon$1[,0,0,0x0, ... ]
Luodaan sitten paneeli, joka kokoaa nämä yhteen:
val kaikkiJutut = new BoxPanel(Orientation.Vertical)kaikkiJutut: scala.swing.BoxPanel = scala.swing wrapper scala.swing.BoxPanel$$anon$1[,0,0,0x0,invalid, layout=javax.swing.BoxLayout, alignmentX=0.0,alignmentY=0.0,border=,flags=9,maximumSize=,minimumSize=,preferredSize=]
BoxPanel
on eräs paneelityyppi. BoxPanel
-olio edustaa paneelia,
johon paneelin sisältö on sijoitettu joko vierekkäin tai allekkain.Vertical
on vakio. Sitä käytetään tässä merkkinä
siitä, että luotavan paneelin sisältö on tarkoitus sijoittaa
"pystysuoraan" eli allekkain. (Vertical
on yksittäisolion
Orientation
muuttuja; samaan yhteyteen on määritelty myös
arvo Horizontal
.)Paneelilla on contents
-ilmentymämuuttuja, jonka tyyppi on Buffer[Component]
eli
puskurillinen GUI-komponentteja. Voimme lisätä puskuriin komponentteja tuttuun
kokoelmankäsittelytyyliin:
kaikkiJutut.contents += kehote kaikkiJutut.contents += ekaNappi kaikkiJutut.contents += tokaNappires0: Buffer[Component] = Buffer(scala.swing wrapper scala.swing.Label$$anon$1[,0,0, ... ]
Näin saimme aikaan paneelin, joka sisältää allekkain kehotteen ja nappulat. Luodaan vielä ikkuna ja laitetaan sen sisällöksi tämä paneeli:
val nappulaikkuna = new Framenappulaikkuna: scala.swing.Frame = scala.swing.Frame$$anon$1[frame1,0,0,0x0, ... ] nappulaikkuna.contents = kaikkiJututnappulaikkuna.contents: Seq[scala.swing.Component] = List(scala.swing wrapper scala.swing.BoxPanel ... ) nappulaikkuna.visible = truenappulaikkuna.visible: Boolean = true
Nyt näkyviin ilmestyy kaksinappulainen ikkuna.
Huomasithan, että järjestys, jossa puskurin sisällöksi laitetut oliot luotiin, ei millään tavoin määrää sitä, miten komponentit asemoituvat ikkunaan? Asemoinnin määrää se järjestys, jossa oliot lisättiin paneeliin, tässä tapauksessa kehote ensin ja nappulat sitten.
Kuvia
Yllä käytimme Label
-luokkaa tekstin esittämiseen käyttöliittymässä. Tuosta luokasta on
moneksi: Label
-"lappusessa" voi olla tekstin sijaan myös kuva. (Voi myös olla molemmat,
jolloin teksti toimii kuvatekstinä.)
Olemassa olevien kuvien lataamiseen sopii Swingin luokka ImageIcon
; ImageIcon
-oliota
luodessa voi ilmoittaa, mistä kuvan sisältö noudetaan. Kuvia voi ladata esimerkiksi
projektin mukaan liitetyistä tiedostoista, muista oman koneen massamuistiin tallennetuista
tiedostoista, netti-URL:eista, jne. (Tältä osin ImageIcon
on kuin o1.Pic
.)
Tehdään kokeeksi vaikkapa tällainen ikkuna:
Alla olevassa REPL-esimerkissä tämä saadaan aikaan luomalla Label
, johon laitetaan
näkyviin internetistä ladattu kuva.
import
ataan aluksi kuvia edustava luokka ImageIcon
ja nettiosoitteiden käsittelyyn
käyttämämme luokka URL
:
import javax.swing.ImageIconimport javax.swing.ImageIcon import java.net.URLimport java.net.URL
Seuraavaksi luodaan Label
, jossa ei ole tekstiä lainkaan:
val kuvakomponentti = new Labelkuvakomponentti: scala.swing.Label = scala.swing wrapper scala.swing.Label$$anon$1[,0,0,0x0, ... ]
Tekstin sijaan sijoitamme sen "ikoniksi" eli kuvaksi ImageIcon
-olion, jota vastaava
kuva ladataan eräästä URL
ista:
kuvakomponentti.icon = new ImageIcon(new URL("http://www.anttisorva.fi/other/Stories9.jpg"))kuvakomponentti.icon: javax.swing.Icon = http://www.anttisorva.fi/other/Stories9.jpg
Luodaan vielä uusi ikkuna, jonka sisältönä on kuvan sisältävä Label
:
val kuvaikkuna = new Framekuvaikkuna: scala.swing.Frame = scala.swing.Frame$$anon$1[frame5,0,0,0x0, ... ] kuvaikkuna.contents = kuvakomponenttikuvaikkuna.contents: Seq[scala.swing.Component] = List(scala.swing wrapper ... ) kuvaikkuna.visible = truekuvaikkuna.visible: Boolean = true
Näkyviin ilmestyy sellainen ikkuna kuin yllä kuvattiin.
javax.swing
?
Erään yllä käytetyn pakkauksen nimi on javax.swing
, mikä voi
tuntua oudolta. Käyttämämme Scalan GUI-kirjasto on rakennettu
Java-kielen samannimisen kirjaston varaan. Scala-ohjelmoidessakin
usein käytetään joitakin Java-kirjastoja suoraan, koska se on
kätevää. Ainakaan nykyisessä Scalan Swing-kirjastossa ei ole omaa
ImageIcon
-luokkaa tai vastaavaa, mutta ei tarvitakaan, koska
voimme käyttää Java-Swingin luokkaa.
Kirjain x — eXtension — on Javan Swing-pakkauksen nimessä historiallisesta syystä.
Apuikkunoita eli dialogeja
Pieniä, väliaikaisesti ruudulla näkyviä "apuikkunoita" eli dialogeja (dialog) käytetään erilaisiin tarkoituksiin kuten viestien välittämiseen käyttäjälle, valintojen tekemiseen ja muuhun syötteen pyytämiseen käyttäjältä.
Monet dialogit ovat pieniä ikkunoita, joissa on muutama elementti aseteltuna tietyllä eri ohjelmissa samanlaisena toistuvalla tavalla. Esimerkiksi viestin välittämiseen käyttävä ikkuna voi muodostua pienestä kuvakkeesta, viestistä ja OK-napista näin:
Dialogi-ikkunoita kuvaa luokka scala.swing.Dialog
. Monissa yksinkertaisissa tilanteissa
siitä ei tarvitse nimenomaisesti luoda ilmentymää, vaan voimme käyttää Dialog
-kumppaniolion
käteviä tehdasmetodeita. Käsittelemme tässä niistä vain yhden nimeltä showMessage
, jota
voi käyttää näin:
Dialog.showMessage(kuvakomponentti, "Ave, Munde!", "Tämä on viesti")
showMessage
-metodin ensimmäinen parametri määrää, minkä
komponentin päälle apuikkuna ilmestyy. Tässä se määrätään
ilmestyväksi ylempänä luodun kuvan keskelle.Äskeinen käsky luo kuvatunlaisen apuikkunan ja laittaa sen näkyviin. Näkyvissä olevan komponentin päälle luotu apuikkuna pysyy esillä, kunnes käyttäjä sulkee sen yläkulman rastista tai OK-napilla.
Swing-käyttöliittymä osana sovellusta
Toistaiseksi puuhasimme REPLissä. Tarkoituksena olisi kuitenkin saada komponenttien luomiskäskyt tallennettua tiedostoon, jossa ne muodostavat sovelluksen käyttöliittymän.
Alla on yksi tapa muodostaa sovellus, jossa on samanlainen kaksinappulainen käyttöliittymä kuin yllä.
object Komponenttikokeilu extends SimpleSwingApplication {
// Pääkomponentit:
val ekaNappi = new Button("Paina minua")
val tokaNappi = new Button("Ei kun MINUA!")
val kehote = new Label("Paina jompaakumpaa napeista.")
// Komponenttien asemointi ikkunaan:
val kaikkiJutut = new BoxPanel(Orientation.Vertical)
kaikkiJutut.contents += kehote
kaikkiJutut.contents += ekaNappi
kaikkiJutut.contents += tokaNappi
val nappulaikkuna = new MainFrame
nappulaikkuna.contents = kaikkiJutut
nappulaikkuna.title = "Kokeiluikkuna"
// Sovelluksen pääikkunan palauttava metodi:
def top = this.nappulaikkuna
}
Komponenttikokeilu
.
Se periytyy luokasta SimpleSwingApplication
, joka on
GUI-sovelluksia kuvaava luokka. SimpleSwingApplication
-tyyppinen
olio voi toimia ohjelman käynnistysoliona (vrt. extends
App
; luku 2.7). Tuo yliluokka myös huolehtii eräistä
alustustoimenpiteistä, jotta niitä ei tarvitse jokaisessa
eri sovelluksessa määrätä suoritettaviksi.MainFrame
Frame
n sijaan. MainFrame
on Frame
n aliluokka.
Sen lisäominaisuutena on, että se päättää koko sovelluksen
suorituksen, kun kyseinen ikkuna suljetaan (vrt. tavallinen
Frame
vain piilottuu suljettaessa, mutta ohjelma jää
käyntiin). Tämä on toivottavaa erillisessä sovelluksessa,
muttei REPLissä.SimpleSwingApplication
on abstrakti luokka (luku 7.3).
Sovellusta kuvaavan oliomme on toteutettava yliluokkansa
abstraktiksi jättämä top
-niminen metodi, joka kertoo
palautusarvollaan, mikä on kyseisen sovelluksen pääikkuna.
SimpleSwingApplication
-yliluokka huolehtii siitä, että
pääikkuna laitetaan näkyviin sovelluksen käynnistyessä.nappulaikkuna.visible = true
, jollainen REPLissä tarvittiin.Tämä koodi löytyy myös SwingExamples-projektin luokasta o1.swingtest.ComponentTestApp
(englanniksi). Voit kokeilla ajaa ohjelman ja tehdä siitä variaatioita. Kokeile
esimerkiksi laittaa Horizontal
Vertical
in tilalle.
Vaihtoehtoinen merkintätapa: sisäkkäiset määrittelyt
Luvussa 2.4 opit luomaan luokasta ilmentymän ja lisäämään sille ilmentymäkohtaisia
ominaisuuksia "lennosta". Olemme sittemmin hyödyntäneet tätä mahdollisuutta runsaasti
o1.View
-luokan yhteydessä.
Vastaavaa toimii myös Swing-luokkien kanssa. Esimerkiksi seuraavat kaksi tekevät saman.
// Versio 1: sama kuin edellä
val nappulaikkuna = new MainFrame
nappulaikkuna.contents = kaikkiJutut
nappulaikkuna.title = "Kokeiluikkuna"
// Versio 2: muuttujille arvot "lennosta"
val nappulaikkuna = new MainFrame {
this.contents = kaikkiJutut
this.title = "Kokeiluikkuna"
}
Tässä vielä koko äskeinen ohjelma kirjoitettuna jälkimmäisellä tyylillä ja muutenkin hieman tiiviimmin:
object Komponenttikokeilu extends SimpleSwingApplication {
// Pääkomponentit:
val ekaNappi = new Button("Paina minua")
val tokaNappi = new Button("Ei kun MINUA!")
val kehote = new Label("Paina jompaakumpaa napeista.")
// Komponenttien asemointi ikkunaan:
val nappulaikkuna = new MainFrame {
title = "Kokeiluikkuna"
contents = new BoxPanel(Orientation.Vertical) {
contents ++= Vector(kehote, ekaNappi, tokaNappi)
}
}
// Sovelluksen pääikkunan palauttava metodi:
def top = this.nappulaikkuna
}
Toimintaa käyttöliittymään tapahtumankäsittelyllä
Pikakertaus: käyttöliittymätapahtumat, -kuuntelijat ja -käsittelijät
Luvusta 2.8:
- Käyttöliittymätapahtumia (GUI event) ovat esimerkiksi hiiren klikkaukset ja liikkeet, näppäimen painallukset ja niin edelleen.
- Tapahtumankäsittelijä (event handler) on aliohjelma, joka
suoritetaan reaktiona tapahtumaan.
o1.View
-olioissa määrittelimme tapahtumankäsittelijöiksi metodeita kutenonClick
jaonTick
.
- Tapahtumankuuntelija (event listener) on ohjelman osa, jolle
raportoidaan tietynlaisista tapahtumista ja joka reagoi niihin
jollakin tavoin.
o1.View
-olio toimii tapahtumankuuntelijana itselleen: se vastaanottaa itse tiedon siitä, kun jotakin tapahtuu, ja kutsuu silloin tapahtumankäsittelijämetodeitaan.
Käyttöliittymätapahtumat ja Swing
Swing-käyttöliittymäkomponentteja kuvaavat oliot on laadittu siten, että ne osaavat havaita käyttöliittymätapahtumia ja ilmoittaa niistä kuuntelijoilleen. Sovellusohjelmoijan erikseen päätettäviksi jäävät:
- Kenelle (eli mille oliolle) tapahtumista tulisi ilmoittaa niiden
sattuessa? Toisin sanoen: mikä tai mitkä oliot toimivat kunkin
Swing-komponentin tapahtumankuuntelijoina?
- Tätä emme
o1
-kirjastolla tehneet, vaanView
-olio toimi omana kuuntelijanaan.
- Tätä emme
- Mikä ohjelmakoodi suoritetaan, kun esimerkiksi nappulaa painetaan?
Eli: mitä tapahtumasta pitäisi seurata?
- Tämän määrittelimme
View
-olioiden tapahtumankäsittelijöihin. Swingissä teemme suurin piirtein samoin.
- Tämän määrittelimme
Tehdään Swing-käyttöliittymä, joka reagoi napinpainalluksiin näin:
Tarkastellaan ratkaisua ensin pseudokoodina ja sitten konkreettisena Scala-koodina.
Ensimmäinen pseudokooditoteutus
Seuraavan pseudokoodin pohjana on yllä määritelty Komponenttikokeilu
-sovellus.
Sen loppupäähän on lisätty hahmotelma tapahtumankäsittelykoodista:
object Tapahtumakokeilu extends SimpleSwingApplication { // Pääkomponentit: val ekaNappi = new Button("Paina minua") val tokaNappi = new Button("Ei kun MINUA!") val kehote = new Label("Paina jompaakumpaa napeista.") // Komponenttien asemointi ikkunaan: val kaikkiJutut = new BoxPanel(Orientation.Vertical) kaikkiJutut.contents += kehote kaikkiJutut.contents += ekaNappi kaikkiJutut.contents += tokaNappi val nappulaikkuna = new MainFrame nappulaikkuna.contents = kaikkiJutut nappulaikkuna.title = "Kokeiluikkuna" // Tapahtumat: Määrää, että tämä Tapahtumakokeilu-olio vastaanottaa ilmoitukset, kun nappulaa ekaNappi tai tokaNappi painetaan. Määrää, että kun tämä olio saa ilmoituksen napinpainalluksesta, se laittaa näkyviin apuikkunan, jossa on viesti "Painoit nappia, jossa lukee: [painetun napin teksti]" // Sovelluksen pääikkunan palauttava metodi: def top = this.nappulaikkuna }
Tarkennettu pseudokoodi
Tarkennetaan käyttöliittymätapahtumiin liittyvää pseudokoodia:
Määrää tämä Tapahtumakokeilu-olio ekaNappi-nappulan kuuntelijaksi. Määrää tämä Tapahtumakokeilu-olio tokaNappi-nappulan kuuntelijaksi. Lisää Tapahtumakokeilu-oliolle tapahtumankäsittelijä, joka toimii nappulaa painettaessa näin: 1. Poimi paikalliseen muuttujaan painallus viittaus tapahtumaa kuvaavaan olioon. 2. Selvitä tapahtumaoliolta kysymällä tapahtuman lähde eli nappula, jota painettiin. 3. Selvitä lähdenappulan teksti nappulaoliolta kysymällä. 4. Kutsu Dialog.showMessage, viestinä "Painoit nappia, jossa lukee: [lähdenappulan teksti]"
Scala-toteutus
Tässä kokeiluohjelmamme Scalalla:
import scala.swing._
import scala.swing.event._
object Tapahtumakokeilu extends SimpleSwingApplication {
// Muuten kuten yllä: ...
this.listenTo(ekaNappi)
this.listenTo(tokaNappi)
this.reactions += {
case painallus: ButtonClicked =>
val lahdenappula = painallus.source
val nappulanTeksti = lahdenappula.text
Dialog.showMessage(kaikkiJutut, "Painoit nappia, jossa lukee: " + nappulanTeksti, "Viesti")
}
}
scala.swing.event
sisältöä.listenTo
-metodi määrää olion ilmoittautumaan kuuntelijaksi
parametrin ilmoittamalle tapahtumien lähteelle.reactions
viittaa kokoelmaan tapahtumankäsittelijöitä.
Tässä siihen lisätään yksi tapahtumankäsittelijä...match
-käskyistäkin tuttua case
-avainsanaa.ButtonClicked
on scala.swing.event
-pakkauksen luokka, joka
kuvaa nappulanpainalluksia. painallus
puolestaan on tässä
ohjelmoijan valitsema nimi muuttujalle, johon tallentuu havaittu
tapahtuma (sovellusohjelmoijan näkökulmasta automaattisesti;
vrt. match
-käsky ja metodiparametrit). Muuttujan arvona on
viittaus ButtonClicked
-tyyppiseen olioon.ButtonClicked
, ota viittaus tapahtumaolioon talteen
muuttujaan nimeltä painallus
ja suorita seuraavat koodirivit:"source
-muuttuja kertoo tapahtuman lähteen eli
tässä painetun nappulan. Nappulaolion text
-muuttujasta saadaan
nappulan sisältämä merkkijono.Koodi löytyy myös SwingExamples-projektin tiedostosta EventTestApp.scala
. Voit itse
muokkailla ohjelmaa ja kokeilla mitä tapahtuu.
Tapahtumankäsittelijöistä Scalassa
Scala Swing-kirjastoineen tarjoaa muitakin tapoja määritellä tapahtumankäsittelijöitä. Tässä esitetty tapa on kurssin tarjoamien esitietojen valossa yksinkertaisin ja siksi valittu tähän.
Muista tapahtumatyypeistä
Tuttuja luokkia?
Tässä mainitut Swing-kirjaston tapahtumaluokat kuten ButtonClicked
ovat
juuri samoja, joita käytit luvussa 2.8, jos teit sen lopussa olleet
vapaaehtoiset tehtävät.
scala.swing.event
-pakkauksessa on luokkaa ButtonClicked
vastaavia luokkia myös muunlaisille
käyttöliittymätapahtumille. On olemassa esimerkiksi luokat MouseClicked
, MouseWheelMoved
,
MouseMoved
, KeyTyped
, jne. Kahdesta ensin mainitusta on esimerkit Llama-ohjelmassa, johon
palaamme hetimmiten.
Laama
Et toki lintsannut
Kokeilithan jo Llama-sovellusta kuten luvun alussa pyydettiin? Seuraava teksti voi olla kovin vaikeaselkoista, jos et kokeillut.
Aiemmista luvuista tiedät, että oliopohjaisen sovellusohjelman keskeisiä osia ovat käyttöliittymä ja sisäinen malli. Käyttöliittymä mahdollistaa käyttäjän komentojen tulkitsemisen sekä sisäisen mallin muuttamisen niiden perusteella.
Aloitetaan laamaohjelman tarkastelu sen sisäisestä mallista eli laaman toimintalogiikasta.
Llama
-luokka
Logiikka on toteutettu luokaksi Llama
. Sen toteutusta ei käydä tässä läpi, vaan
opettelemme ainoastaan rajapinnan:
- Laamaa kuvaa yksi
Llama
-luokan ilmentymä. - Parametrittomilla metodeilla
tickle
,poke
jaslap
laamaa voi rapsuttaa, tökätä tai inhottavan käyttäjän ollessa kyseessä läpsäyttää. - Laamalla on kärsivällisyysarvo, joka alkaa sadasta prosentista. Kun laamaa käsitellään, se kiihtyy sadasta nollaan varsin rivakasti.
- Parametriton
isOutOfPatience
-metodi kertoo, onko laaman kärsivällisyys aivan lopussa. - Laamaolion
stateOfMind
-metodi — parametriton sekin — palauttaa merkkijonon, joka kuvaa laamaolion mielipidettä asiaintilasta.
Käyttöliittymän komponentit
Llama-sovelluksen käyttöliittymän siis kuuluisi luoda Llama
-olio sekä tarjota graafiset
komponentit, joilla voi:
- näyttää laamaa esittävän kuvan ja sen yläpuolella laaman mielentilaa kuvaavan tekstin,
- Tarjota käyttäjälle mahdollisuuden rapsuttaa (hiiren rullalla), tökätä (hiiren napilla) tai läpsäistä (tuplaklikkauksella) laamaa, ja
- mahdollistaa uuden laaman luominen Again!-nappulalla.
Seuraava koodi luo ja asemoi käyttöliittymän osat, mutta ei vielä reagoi tapahtumiin:
object LlamaApp extends SimpleSwingApplication {
// A couple of images:
val alivePic = new ImageIcon("o1/llama/pics/alive.jpg")
val deadPic = new ImageIcon("o1/llama/pics/dead.jpg")
// Access to the model (internal logic of the app):
var targetLlama = new Llama
// Components:
val commentary = new Label
val pictureLabel = new Label
val startOverButton = new Button("Again!")
this.updateLlamaView()
// Layout:
val verticalPanel = new BoxPanel(Orientation.Vertical)
verticalPanel.contents += commentary
verticalPanel.contents += pictureLabel
verticalPanel.contents += startOverButton
val llamaWindow = new MainFrame
llamaWindow.title = "A Llama"
llamaWindow.resizable = false
llamaWindow.contents = verticalPanel
def top = this.llamaWindow
private def updateLlamaView() = {
this.commentary.text = targetLlama.stateOfMind
this.pictureLabel.icon = if (this.targetLlama.isOutOfPatience) this.deadPic else this.alivePic
}
}
LlamaApp
-olio lataa aluksi projektin sisältämistä tiedostoista
pari kuvaa ImageIcon
-olioiksi.Label
ia (teksti ja kuva) sekä yksi
nappula.resizable
-muuttujan asettaminen false
ksi lukitsee ikkunan
koon. Käyttäjä ei voi muuttaa kokoa esimerkiksi ikkunan reunoja
siirtämällä.updateLlamaView
selvittää laaman nykytilan
perusteella näytettävän kommentin ja kuvan ja päivittää
Label
eita sen mukaisesti.Hiiren ja nappulan kuunteleminen
Lisäämällä seuraavat rivit LlamaApp
-sovellukseen saamme ohjelman toimimaan halutusti.
// Events:
this.listenTo(startOverButton)
this.listenTo(pictureLabel.mouse.clicks)
this.listenTo(pictureLabel.mouse.wheel)
this.reactions += {
case moveEvent: MouseWheelMoved =>
targetLlama.scratch()
updateLlamaView()
case clickEvent: MouseClicked =>
if (clickEvent.clicks > 1) { // double-click (or triple, etc.)
targetLlama.slap()
} else {
targetLlama.poke()
}
updateLlamaView()
case clickEvent: ButtonClicked =>
targetLlama = new Llama
updateLlamaView()
}
LlamaApp
-olio kuuntelee paitsi nappulaa myös hiiren
käyttöä. Se järjestyy näin.Anteeksi
Pahoittelut kaikille vahingoittuneille virtuaalilaamoille ja laamanmielisille. Laamat ovat kivoja eivätkä aiheuta syöpää.
Toinen esimerkki: RandomText
Siirrytään toisen sovelluksen pariin. Käytetään pakkausta o1.randomtext
ja sen
sisältämää luokkaa RandomTextGenerator
.
RandomTextGenerator
-oliot ovat kevyeen viihdekäyttöön sopivia "puppusanageneraattoreita".
Tällaisella oliolla on metodi randomize
, jolle annetaan parametriksi tiedoston nimi tai
verkko-URL. Metodi palauttaa satunnaisesti tuotettua tekstiä, joka muistuttaa
alkuperäistekstiä. (Näet esimerkin, kunhan ohjelma saadaan toimimaan.)
Tähdätään siihen, että ohjelmassa olisi tämännäköinen graafinen käyttöliittymä:
Verrattuna aiempiin esimerkkeihin on tässä kaksi uutta ongelmaa:
- Miten saamme näkyviin tekstikentän (text field) syötettä varten ja monirivisen tekstialueen (text area), johon ohjelman syytämä puppu ilmestyy käyttäjän painaessa Randomize!-nappulaa?
- Miten saamme komponentit asemoitua niin, että kehote, tekstikenttä ja nappula ovat ylärivillä ja tekstialue täyttää loput ikkunasta?
Aloitetaan jälkimmäisestä. Ratkaisu on sama kuin aiemminkin: paneelit. Ikkunassa on käytetty kahta paneelia:
BoxPanel
. Siinä ovat
allekkain "ylärivi" sekä iso tekstialue (TextArea
).Label
), tekstinsyöttökentän (TextField
) sekä
nappulan (Button
). Tämä paneeli on tarkemmin sanoen tyyppiä
FlowPanel
. Olisi myös voitu tehdä vaakasuora BoxPanel
, mutta...BoxPanel
in ominaisuuksiin
kuuluu sen sisällön venyttäminen reunoihin asti, mikä näyttää tässä
vähemmän kivalta.Seuraava Scala-koodi asettelee komponentit kuvattuun tapaan.
object RandomTextApp extends SimpleSwingApplication {
// Components:
val prompt = new Label("Source file or URL:")
val sourceField = new TextField("alice.txt", 50)
val randomizeButton = new Button("Randomize!")
val outputArea = new TextArea("Random stuff will appear here.", 30, 85)
outputArea.editable = false
outputArea.lineWrap = true
// Layout:
val topRow = new FlowPanel
topRow.contents += prompt
topRow.contents += sourceField
topRow.contents += randomizeButton
val wholeLayout = new BoxPanel(Orientation.Vertical)
wholeLayout.contents += topRow
wholeLayout.contents += outputArea
val window = new MainFrame
window.title = "Random Text Generator"
window.resizable = false
window.contents = wholeLayout
def top = this.window
}
TextField
ja TextArea
käyttö.
Konstruktoriparametrit alustavat näiden komponenttien
sisällön ja määräävät niiden mitat (sarakkeina ja
tekstialueen tapauksessa myös riveinä).outputArea
n sisällön.
Asettamalla editable
-ominaisuuden arvon false
ksi estämme
käyttäjää muokkaamasta tekstiä. lineWrap
-ominaisuudella
puolestaan saadaan komponentti jakamaan pitkä merkkijono
usealle riville.FlowPanel
tuottaa tässä hieman kauniimman lopputuloksen
kuin BoxPanel
.BoxPanel
iakin on tässä käytetty. Sillä sijoitamme yläriviä
kuvaavan paneelin tekstialueen yläpuolelle.Tämä toteutus löytyy myös SwingExamples-projektin pakkauksesta o1.randomtext
.
Mutta siitähän tuli ruma?
Kun kokeilet ajaa oheisprojektista SwingExamples löytyvän ohjelman, niin saatat huomata, ettei se tuota ihan sen näköistä käyttöliittymää kuin yllä olevassa kuvassa. Osaset ovat kyllä oikeassa järjestyksessä, mutta nappulan ja muiden komponenttien ilmiasu on ikävällä tavalla "retron" näköinen eikä sellainen kuin se muilla ikkunoilla ja nappuloilla yleensä on siinä ympäristössä, jossa ajat ohjelmaa. Miksi?
Asian taustalla on se, että eri käyttöympäristöissä ikkunat ja muut käyttöliittymäkomponentit näyttävät pääpiirteissään samalta mutta kuitenkin selvästi erilaisilta. (Vertaa esimerkiksi, miltä graafiset käyttöliittymät näyttävät Windows- ja Mac-koneilla.) Sanotaan, että käyttöliittymillä on eri ympäristöissä eri look and feel.
Swing-käyttöliittymäkirjasto tarjoaa mahdollisuuden käyttää erilaisia look and feel -asetuksia. Ellet toisin määrittele, käytetään Swingin oletusasetuksia (ns. cross-platform look and feel). Jos haluat käyttää muita asetuksia, on annettava erillinen käsky.
Muokataan RandomTextApp
in alkua siten, että siinä lukee:
import javax.swing.UIManager
object RandomTextApp extends SimpleSwingApplication {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName)
// ...
UIManager
in metodit ovat graafiseen ajoympäristöön liittyviä palveluita.
Tässä niitä käytetään pyytämään (ohjelmaa ajettaessa) tieto siitä, mikä on
ohjelmaa senhetkisen ajoympäristön look and feel (getSystemLookAndFeelClassName
)
sekä määräämään tämä look and feel käyttöön (setLookAndFeel
).
Kun näin täydennetty ohjelma satutaan ajamaan esimerkiksi Windows 7 -käyttöjärjestelmässä, ikkuna näyttää siltä kuin yllä olevassa kuvassa. Muissa käyttöympäristöissä ikkuna näyttää erilaiselta, mutta kuitenkin koko lailla juuri sellaiselta kuin kyseisessä käyttöympäristössä kuuluukin. Muokattu ohjelma siis ei käytä Swingin oletusasetuksia vaan mukautuu kulloiseenkin ajoympäristöönsä.
Jos haluat, voit itse kaunistella RandomTextApp
ia lisäämällä sen koodiin
yllä olevan import
-käskyn ja UIManager
in metodien kutsut.
Ohjelmointitehtävä: RandomTextApp
Täydennä RandomTextApp
sellaiseksi, että se toimii koodiin kirjoitettujen
kommenttien. Tarkemmin sanoen:
- Aja annettu versio
RandomTextApp
-sovelluksesta ja kokeile sen käyttöä. Käyttöliittymän palaset ovat kohdallaan, mutta sovellus ei vielä tee mitään. - Ohjelman tulisi suoltaa "pupputekstiä" käyttäjän painaessa
nappulaa. Tekstin tuottamisesta huolehtii ohjelman sisäisenä
mallina toimiva
RandomTextGenerator
-olio. LiitäRandomTextApp
-olioon viittaus luokanRandomTextGenerator
ilmentymään.- Vrt. se, miten
LlamaApp
ista oli viittausLlama
-olioon yllä. - Anna konstruktoriparametriksi kokonaisluku 9. Pupputekstigeneraattorin konstruktoriparametrien merkitys on selitetty koodin kommenteissa. Voit kyllä kokeilla omin päin muitakin parametriarvoja.
- Vrt. se, miten
- Aseta
RandomTextApp
-olio kuuntelemaan napinpainalluksia. - Lisää
RandomTextApp
-oliolle tapahtumankäsittelijäkoodi, joka reagoiButtonClicked
-tyyppisiin tapahtumiin päivittämällä tekstialuettaoutputArea
.- Tuota pupputeksti kutsumalla generaattoriolion
randomize
-metodia. Anna parametriksi tekstikentänsourceField
sisältämä merkkijono eli tekstikentäntext
-ominaisuuden arvo. - Aseta palautusarvona saatu puppu
outputArea
n sisällöksi sijoittamalla setext
-ominaisuuden arvoksi.
- Tuota pupputeksti kutsumalla generaattoriolion
- Kokeile, miten ohjelma toimii. Jos kaikki on mennyt hyvin, Randomize!-nappulan painaminen tuottaa nyt sisältöä tekstialueelle.
- Koska tekstialueen
lineWrap
-ominaisuus asetettiin yllätrue
ksi, tuotettu pupputeksti jakautuu riveiksi. Rivit kuitenkin katkeavat myös sanojen keskeltä, mikä ei näytä edustavalta. Korjaa asia laittamalla tekstialueenwordWrap
-ominaisuus "päälle" samaan tapaan.
A+ esittää tässä kohdassa tehtävän palautuslomakkeen.
Käyttöliittymät jäävuoren huippuna
Joel Spolsky, seurattu blogisti ja toinen Stack Overflow -neuvontasivuston perustajista, kirjoitti vuonna 2002 kaupallisiin ohjelmistohankkeisiin liittyvästä "jäävuorisalaisuudesta". Tämä salatieto on nyt ulottuvillasi, kun olet oppinut ohjelmoinnista ja käyttöliittymistä.
You know how an iceberg is 90% underwater? Well, most software is like that too — there’s a pretty user interface that takes about 10% of the work, and then 90% of the programming work is under the covers.
That’s not the secret. The secret is that People Who Aren’t Programmers Do Not Understand This.
There are some very, very important corollaries to the Iceberg Secret.
Important Corollary One. If you show a nonprogrammer a screen which has a user interface that is 90% worse, they will think that the program is 90% worse.
Important Corollary Two. If you show a nonprogrammer a screen which has a user interface which is 100% beautiful, they will think the program is almost done.
—pätkä kirjoituksesta The Iceberg Secret, Revealed
Osin tämänkin vuoksi on hyvä, että yhä useampi kansalainen ymmärtää edes vähän ohjelmoinnista.
Yhteenvetoa
- Sovelluksen graafinen käyttöliittymä eli GUI rakennetaan apukirjastoa käyttäen. Yksi tällainen kirjasto on Scalan Swing.
- Moni GUI-kirjasto tarjoaa käyttöliittymäkomponentteja kuten ikkunoita, nappuloita ja tekstikenttiä. Niin Swingkin. Swing on oliopohjainen ja kuvaa nämä käsitteet luokkina.
- Kun komponentteja asemoidaan GUI-ikkunaan on usein hyödyllistä käyttää paneeleiksi kutsuttuja apukomponentteja, joihin muita komponentteja voi ryhmitellä.
- GUI voi reagoida käyttäjän toimiin eli käyttöliittymätapahtumiin
määräämällä olion toimimaan ns. tapahtumankuuntelijana.
- Esimerkiksi nappulaolio voi välittää kuuntelijoilleen tiedon siitä, kun sitä painetaan.
- Saadessaan näin tiedon tapahtumasta kuuntelija suorittaa tapahtumankäsittelykoodin, joka määrää, mitä esimerkiksi nappulanpainalluksesta kyseisessä sovelluksessa seuraa.
- Lukuun liittyviä termejä sanastosivulla: graafinen käyttöliittymä GUI; käyttöliittymätapahtuma, tapahtumankuuntelija, tapahtumankäsittelijä; Swing.
Tahtoo lisää!
Viralliset GUI-oppimistavoitteet ovat tällä kurssilla vaatimattomat. Aihetta ei ole painotettu kurssilla, koska GUI-ohjelmointiin tarvitaan melko paljon sellaistakin osaamista, joka ei parhaalla mahdollisella tavalla tue ohjelmoinnin yleisten peruskäsitteiden oppimista ja joka edellyttää tietyn GUI-kirjaston yksityiskohtiin menemistä.
Voit opetella lisää GUI-ohjelmoinnista ja Swing-kirjastosta omin päin sekä kurssilla Ohjelmointistudio 2.
Löydät lisäesimerkkejä myös monesta kurssin oheisprojektista, jossa on valmiina annettu graafinen käyttöliittymä. Näissä projekteissa käytetään runsaasti sellaisia tekniikoita, joita ei tässä luvussa käsitelty (esim. omien elementtityyppien periyttämistä Swing-kirjaston valmiista elementtityypeistä).
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.
Joidenkin lukujen lopuissa on lukukohtaisia lisäyksiä tähän tekijäluetteloon.
Frame
-olio.