Helsingin yliopisto / tietojenkäsittelytieteen laitos / Ohjelmointitekniikka (Scala) / © Arto Wikla 2016

Graafisesta käyttöliittymästä hyvin lyhyesti

Muutettu viimeksi 26.4.2016 / Sivu luotu 26.4.2010 / [oppikirjan esimerkit] / [Scala]

Sivun sisältöä:

Tässä luvussa luodaan pikakatsaus graafisen käyttöliittymän toteuttamisen vaivattomuuteen Scalalla – vaivatonta se on ainakin Javaan kauheuteen verrattuna. Monet Javassa kömpelöt ilmaukset on saatu luonnollistettua Scalan funktioparametrien ja operaattorimerkeiltä näyttävien metodikutsujen ansiosta. Eikä lopulta ole lainkaan yhdentekevää, voiko ohjelmoija ajatella selkein ja luontevin käsittein!

Swing-sovellus

Pakkauksessa scala.swing on monenlaisiin graafisiin sovelluksiin riittävä luokka SimpleSwingApplication [Swing-API (versio 2.10.1)]. Luokka on abstrakti. Sen ainoa abstrakti metodi on abstract def top: Frame. Sen saa toteutettua esimerkiksi MainFrame-oliona:
  import scala.swing._

  object FirstSwingApp extends SimpleSwingApplication {
    def top = new MainFrame {
      title = "First Swing App"
      contents = new Button {
        text = "Click me"
      }
    }
  }
Kuten näkyy, MainFrame-luokassa on mukavia muuttujia eli kenttiä, joille voi antaa arvoja! Sama tekniikka löytyy myös luokasta Button. Hyvä idea!

MainFrame myös sisältää pääohjelman, joka mm. kutsuu top-metodia. MainFrame on sellainen Frame, joka osaa päättyessään lopettaa koko graafisen sovelluksen.

Layout

Edellä contents-muuttujaan asetettiin vain yksittäinen nappula. Kun halutaan useampia käyttöliittymäkomponentteja, tarvitaan jokin säiliö, "paneeli", jonne komponentteja sijoitellaan. Luokan scala.swing.Panel aliluokista tällaisia löytyy: BorderPanel, BoxPanel, FlowPanel, GridBagPanel, GridPanel. Javan GUI-ohjelmointia tunteva saattaa ehkä aavistaa, millaisia asemointeja (lay-out) nämä paneelit tarjioavat...;-)
import scala.swing._

object SecondSwingApp extends SimpleSwingApplication {
  def top = new MainFrame {
    title = "Second Swing App"
    val button = new Button {
      text = "Click me"
    }
    val label = new Label {
      text = "No button clicks registered"
    }	
    contents = new BoxPanel(Orientation.Vertical) {
      contents += button
      contents += label
      border = Swing.EmptyBorder(30, 30, 10, 30)
    }
  }
}  
Taas tätä Scala tyylikkyyttä: contents-muuttujaan lisätään nappula ja teksti lisäysoperaatiolla "+=".

Huom: Tuossa BoxPanelin luonnissa on taas yksi Scalan uutuus: new-ilmauksen yhteydessä muutetaan luotavan olioin kenttien arvoja. Pienempi esimerkki: Hmm.sca.

Reagointia

Scalan tapahtumiin reagointi muistuttaa logiikaltaan Javan tapaa, mutta kuten niin monesti on havaittu, Scalan vahvat rakenteet ovat tehneet mahdolliseksi suoristaa monia mutkia ja saada ohjelma paremmin näyttämään siltä, mitä tarkoitetaan.

Tapahtumia kuunnellaan (listenTo(source)) ja niihin reagoidaan (reactions += ...):

import scala.swing._
import scala.swing.event._

object ReactiveSwingApp extends SimpleSwingApplication {
  def top = new MainFrame {
    title = "Reactive Swing App"
    val button = new Button {
      text = "Click me"
    }
    val label = new Label {
      text = "No button clicks registered"
    }	
    contents = new BoxPanel(Orientation.Vertical) {
      contents += button
      contents += label
      border = Swing.EmptyBorder(30, 30, 10, 30)
    }
    listenTo(button)
    var nClicks = 0
    reactions += {
      case ButtonClicked(b) =>
        nClicks += 1
        label.text = "Number of button clicks: "+ nClicks
        b.text = nClicks +". time. Again?"

    }
  }
}  
Kuuntelu voidaan myös lopettaa (deafTo(source)). Tapahtumista saadaan case-class-oliota (ei käsitelty kurssilla!), joihin reagointi eli tapahtuman käsittely lisätään top-kehyksen reactions-ominaisuuteen. Tuo b tuolla ButtonClicked(b)ssä antaa käyttöön AbstractButton-tyyppisen komponentin, jolle tapahtui jotakin ja jota voidaan toki myös itse käyttää.

Yhdessä lisättävässä reaktiossa saa olla useampiakin case-osia. Sekä itse lisätyistä että perityistä case-osista suoritetaan kaikki sopivat järjestyksessä: viimeisimmäksi lisätty, toiseksi viimeisimmäksi lisätty, jne. Regoinnit siis viedään pinoon.

Reagointeja voi myös poistaa reactions-kokoelmasta. Ja sehän tietenkin tehdään vähennysoperaatiolla -=.

Celsius-Farenheit-muunnin

import swing._
import event._

object TempConverter extends SimpleSwingApplication {
  def top = new MainFrame {
    title = "Celsius/Fahrenheit Converter"

    object celsius extends TextField { columns = 5 }
    object fahrenheit extends TextField { columns = 5 }

    contents = new FlowPanel {
      contents += celsius
      contents += new Label(" Celsius  =  ")
      contents += fahrenheit
      contents += new Label(" Fahrenheit")
      border = Swing.EmptyBorder(15, 10, 10, 10)
    }

    listenTo(celsius, fahrenheit)

    reactions += {
      case EditDone(`fahrenheit`) =>   // !!
        val f = fahrenheit.text.toInt
        val c = (f - 32) * 5 / 9
        celsius.text = c.toString
      case EditDone(`celsius`) =>      // !!
        val c = celsius.text.toInt
        val f = c * 9 / 5 + 32
        fahrenheit.text = f.toString
    }
  }
}

Huom: EditDone tulkitsee parametrinsa uudelleen määritellyksi, ellei sitä kirjoiteta noihin takaperoisiin aksenttimerkkeihin. Tekstikenttäoliot annetaan siis "literal identifier" -muodossa, jotta kääntäjä ymmärtäisi noiden tunnusten tarkoittavan sovelluksen omia kenttiä...

Toinen keino hoitaa asia on nimetä nuo tekstikenttäoliot alkamaan isolla (!!) kirjaimella:

import swing._
import event._

object TempConverter extends SimpleSwingApplication {
  def top = new MainFrame {
    title = "Celsius/Fahrenheit Converter"

    object Celsius extends TextField { columns = 5 }     // !!
    object Fahrenheit extends TextField { columns = 5 }  // !!

    contents = new FlowPanel {
      contents += Celsius
      contents += new Label(" Celsius  =  ")
      contents += Fahrenheit
      contents += new Label(" Fahrenheit")
      border = Swing.EmptyBorder(15, 10, 10, 10)
    }

    listenTo(Celsius, Fahrenheit)

    reactions += {
      case EditDone(Fahrenheit) =>
        val f = Fahrenheit.text.toInt
        val c = (f - 32) * 5 / 9
        Celsius.text = c.toString
      case EditDone(Celsius) =>
        val c = Celsius.text.toInt
        val f = c * 9 / 5 + 32
        Fahrenheit.text = f.toString
    }
  }
}