A Scala programozási nyelv

Swing a Scala-ban

Swing a Scala-ban

A fejezetet Kovács Vincent írta a Scala Swing Design dokumentum alapján.

Bevezetés

A scala.swing csomag a JAVA Swing csomagon alapszik. A csomag segít abban, hogy a Swing használata átláthatóbb és Scala „barát” legyen. A következő programkód tekinthető a scala.swing "Hello Word!"-jének:

import swing._ object HelloWorld extends SimpleSwingApplication { def top = new MainFrame { title = "Hello, World!" contents = new Button { text = "Click Me!" } } }

Az „egyszerű” Swing alkalmazás tartalmaz egy main objektumot, mint a fenti HelloWorld mintaalkalmazás is, ami a SimpleSwingApplication osztályból származik. Az alkalmazásnak implementálnia kell a top metódust, amelyben egy paraméternélküli Frame-t kell visszaadni. Ebben az esetben ez a MainFrame, ami a Frame osztály egyik leszármazottja.

A Frame egy standard operációs rendszeri ablak, ami tartalmazza a minimalizáló, maximalizáló és bezáró gombokat is. A MainFrame bezáráskor automatikusan terminál maga az alkalmazás is.

Indításkor a SimpleSwingApplication gondoskodik a Swing keretrendszer megfelelő inicializálásáról és megnyitja a top metódus által visszaadott Frame-t. A fenti implementáció létrehoz egy fő keretet egy anonim osztálydefiníció segítségével:

new MainFrame { title = "Hello, World!" contents = new Button { text = "Click Me!" } }

Az anonim osztály definíció megengedi, hogy a MainFrame összes változóját elérjük a „kapcsos” zárójeleken belül. Elsősorban beállításra kerül a keret címe (title paraméter), ami egy "Hello, World!" szöveg lesz, majd beállításra kerül a keretnek a tartalma (content paraméter) is.

Sok függvény és osztály a scala.swing-ben hasonlóképpen épül fel, mint a Java-ban. Például egy új gomb létrehozásához elég csak annyit írni, hogy:

contents = new Button("Click Me!")

Ha nagyon sok paramétert szeretnénk megadni, akkor ajánlott az anonim osztálydefiníciót alkalmazni, ami a kódot sokkalta olvashatóbbá teszi.

A SwingApllication és a SimpleSwingApplication

A SimpleSwingApplication osztály a SwingApllication osztály leszármazottja, ami implementál egy alapértelmezett main metódust. Ezen felül további metódusokat is implementál:

def startup(args: Array[String]) def quit() { ... } def shutdown() { ... }

A startup metódus az alapértelmezett main metódus elején kerül meghívásra és a kliens, vagyis a származtatott osztály, által kell implementálva lennie.

A quit metódus bezárja magát az alkalmazást.

A shutdown metódus a kliens által felül lehet definiálni, hogy az erőforrások a megfelelőképpen kerüljenek felszabadításra az alkalmazás bezárásakor.

A SimpleSwingApplication egy alapértelmezett implementációt tartalmaz a startup metódusra, ami megjeleníti a kliens által implementált top metódus által visszaadott Frame-t.

Komponensek felépítése és megjelenítése

Az előzőekben láthattuk, hogy egy gombot adtunk hozzá a fő ablakkerethez. Egy bonyolultabb megjelenítéssel rendelkező alkalmazásnál ez már nem elég. Ebben az esetben szükségünk van az egyes grafikus elemeket konténerekbe rendezni. A scala.swing-ben hasonlóan, mint a Java Swing-ben két fajta konténer komponens van: a panes és a panel.

Erősen típusos konténer interfészek

A Java Swing esetében a konténerek kettéválik az elrendezéstől. Ahhoz, hogy létrehozásra kerüljön egy Java Swing konténer, létre kell hozni egy JPanel-t és egy hozzá tartozó LayoutManager-t:

val panel = new JPanel() panel.setLayout(new BorderLayout())

A konténerhez tartozó komponensek a különböző add metódusokkal lehet hozzáadni, amit a java.awt.Component alaposztály tartalmazza:

def add(comp: Component, constraints: Object): Component def add(comp: Component, index: Int): Component def add(comp: Component,constraints: Object, index: Int): Component

A fenti példakódhoz egy új gombot a következő kóddal lehet hozzáadni:

panel.add(new JButton("click me"), BorderLayout.CENTER)

A scala.swing-ben a Java Swing-gel ellentétben a konténerek és az elrendezések egybeolvadnak.

A LayoutContainer

A LayoutContainer egy trait, amelyből származtatni lehet a különböző konténereket. Ehhez implementálni kell a következő absztrakt metódusokat:

constraintsFor(c: Component): Constraints areValid(c: Constraints): (Boolean, String) add(comp: Component, c: Constraints)

Események kezelése

A scala.swing az eseményeket egy publisher-ek (esemény kiváltók) és reactor-ok (esemény megfigyelők) felépítés alapján kezeli.

Minden scala.swing komponens egyben egy esemény megfigyelő, amely figyeli az eseményeket és egy esemény kiváltó, amely kivált különböző eseményeket. A következő egyszerű kódrészlet bemutatja, hogyan lehet hőmérsékleteket átváltani Celsiusról Fahrenheitre eseményeknek a segítéségével:

object Converter extends SimpleSwingApplication { def newField = new TextField { text = "0" columns = 5 } val celsius = newField val fahrenheit = newField def top = new MainFrame { title = "Convert Celsius / Fahrenheit" contents = new FlowPanel(celsius, new Label(" Celsius = "), fahrenheit, new Label(" Fahrenheit")) } }

A fenti mintakódban látható, hogy a kerethez hozzáadódik két szövegmező, amiben a felhasználó megadhatja a kívánt hőmérsékleteket (celsius, fahrenheit változók). A TextField objektumok az EditDone eseményeket hirdetik meg, ha a szövegmezőn a szerkesztés befejezésre kerül. Ahhoz, hogy ezt az eseményt el tudjuk kapni a reactions változóban található megfigyelőhöz kell hozzáadni az szövegmezőket. A következő kódrészlettel kell kiegészíteni a programunkat:

listenTo(fahrenheit, celsius) reactions += { case EditDone(‘fahrenheit‘) => val f = Integer.parseInt(fahrenheit.text) val c = (f - 32) * 5 / 9 celsius.text = c.toString case EditDone(‘celsius‘) => val c = Integer.parseInt(celsius.text) val f = c * 9 / 5 + 32 fahrenheit.text = f.toString }

Jelen esetben a reactions az alkalmazás globális megfigyelő objektuma.

Először a listenTo metódussal feliratkozunk a szövegmezők eseményeire. (A listenTo a Reactor alaposztály egyik metódusa, amely a SwingApplication osztály egyik trait-ja.) A következő részben egyenként megadásra kerülnek azok az események, amelyeket fel szeretnénk dolgozni, vagyis a fahrenheit és a celsius paraméterekben tárolt szövegmezők EditDone eseményeire.

A feliratkozott események kiválasztását a scala.swing egyszerű mintaillesztéssel végzi el.

Java Swing Listener-ek és a scala.swing Reactor-ok

A scala.swing-ben mindent objektum egyedi, így a mintaillesztés könnyen működik. Ezzel szemben a Java Swing-ben azesemények két szakaszban hajtódnak végre. Először az esemény típusa, plusz a hozzá Listenermetódus.

A következő kódrészlet megmutatja, hogy a Java Swing-ben, hogyan kapunk el egy egyszerű egér kattintást:

new JComponent { addMouseListener(new MouseAdapter { @Override def mouseClicked(e: MouseEvent) { System.out.println("Mouse clicked at " + e.getPoint) } }) }

Ezzel ekvivalens kód a scala.swing-ben a következőképpen néz ki:

new Component { listenTo(mouse) reactions += { case e: MouseClicked => println("Mouse clicked at " + e.point) } }

Hatékonysági okokból az egér eseményei nem a Component-ből származnak, hanem az egyik paraméteréből, a mouse-ből, ezért először fel kell a listenTo metódussal erre iratkozni.

Publisher-ek és Reactor-ok

Az egész eseménykezelő rendszer a Publisher, Reactor és a Reactions osztályokon alapszik. Az API használó felhasználók részére, csak a Publisher osztályban a publish metódus az, ami érdekes. Ez értesíti a feliratkozott reactor-okat:

def publish(e: Event)

Ahhoz hogy egy osztály kiválthasson eseményeket a Publisher osztályból kell származnia és a publish metódus kell meghívnia az események kiváltásához.

A reactor-ok feliratkozhatnak a különböző eseményekre, illetve le is iratkozhatnak a Reactor osztály következő metódusaival:

def listenTo(ps: Publisher*) // feliratkozás def deafTo(ps: Publisher*) // leiratkozás

A Reactor osztálynak van egy különleges paramétere, amely tulajdonképpen a reaction-ok gyűjteménye. Egy reaction egy Reactions.Reaction típusú objektum. Új reaction-t a += metódussal lehet hozzáadni, törölni pedig a -= metódussal lehetséges. Mindkettő metódus visszaadja a kapott reaction objektumot:

def +=(r: Reactions.Reaction): this.type // hozzáadás def -=(r: Reactions.Reaction): this.type // eltávolítás

A Java Swing egy javax.swing.Action interfészt biztosít, hogy az eseményekre adott válaszok futtathatóak legyenek. Ezek különbeznek a Listener-ektől, mégpedig abban, hogy információkat szolgáltatnak, amelyek későbbiekben hasznosak lehetnek. Ezeknek lehetnek gyakran használt változók például az akció neve, gomb esetén maga a gomb felirata, vagy egy menü elem esetében a menünek a címe, vagy az akcióhoz tartozó billentyűrövidítés, stb.

A scala.swing-ben erre az Action trait szolgál. A következő beépített metódus nagyon kényelmessé teszi az akciók létrehozását:

def apply(title: String)(block: => Unit) = new Action(title) { def apply() { block } }

Egy új akció létrehozásához csak elég annyit írni, hogy:

Action("Run me") { println("Someone executed this action.") }

Egy komponenshez hozzárendelhető egy akció, amihez a Action.Triger trait-t kell származtatnia. A követező kódrészlet bemutatja, hogy hogyan lehet egy gomb kattintásához egy akciót hozzárendelni:

val button = new Button { action = Action("Click me") { println("Someone executed clicked button " + this) } }

Mivel gyakori, hogy egy gombhoz akciókat rendeljünk hozzá, ezért van egy beépített gyár metódus is:

val button = Button("Click me") { println("Someone executed clicked button " + this) }

Ahhoz, hogy egy komponensnek megmondjuk, hogy nincs hozzárendelve egy akció az ActionNoAction objektumot kell hozzárendelni, ami egyben az alapértelmezett érték is:

button.action = Action.NoAction

Lista és táblázatok nézetek (elrendezések)

A lista és táblázat nézek komponensek feladata, hogy megadott mennyiség elemet egy egységes elrendezésbe helyezze. A lista elrendezések az adatok függőlegesen, vagy vízszintesen felsorolva jelenítik meg. A táblázat elrendezéseket a megadott adatokat egy táblázatos formában jelenítik meg. Opcionálisan megadható az oszlop és a sor fejlécek is.

ListView osztály

A ListView osztálynak a konstruktora egy listát vár paraméterül, ami tartalmazza a felsorolni kívánt elemeket. Egy ListView a következőképpen hozható létre:

val items = List("Lausanne", "Paris", "New York", "Berlin", "Tokio") val view = new ListView(items)

A ListView definiál egy selection változót, amelynek köszönhetően a kliens információt nyerhet az aktuális kiválasztott elemről. Ennek megfelelően a fenti kódrészletet kicsit átalakítva:

case class City(name: String, country: String, population: Int, capital: Boolean) val items = List(City("Lausanne", "Switzerland", 129273, false), City("Paris", "France", 2203817, true), City("New York", "USA", 8363710 , false), City("Berlin", "Germany", 3416300, true), City("Tokio", "Japan", 12787981, true)) val view = new ListView(items)

Ezután lekérdezhető az aktuálisan kiválasztott City objektumot és azon belül is azoknak a nevei:

val cityNames = view.selection.items.map(_.name)

Megjelenítők (Renderers)

Ha fent megadott városokat megjelenítő ListView-t kirajzoljuk a Frame-re, akkor csak egy összefűzött felsorolást fogunk kapni, valami hasonló formában:

City(Lausanne,Switzerland,129273,false) City(Paris,France,2203817,true) City(New York,USA,8363710,false) City(Berlin,Germany,3416300,true) City(Tokio,Japan,12787981,true)

A megjelenítő automatikusan az összes elemre meghívja a toString metódust. Tehát, ha egy City-n meghívjuk a toString metódust, akkor az automatikusan visszaadja a konstruktor paraméterein szereplő változókat toString formában.

A ListView osztály példánya a ListView.Renderer osztályt használja az egyes elemek kirajzolásához. Egy új megjelenítő definiálásval ezt a listát szebbé, lehet tenni:

import ListView._ val view = new ListView(items) { renderer = Renderer(_.name) }

A fenti megjelenítő, csak a neveket fogja kirajzolni.

A Table osztály

A Table felépítése és a hozzá tartozó megjelenítők hasonlóak a ListView-hoz. A Table osztály nem csak listák megjelenítését támogatja, de kétdimenziós tömbök megjelenítésére is alkalmas. A táblázat egy eleme több mindent reprezentálhat, mint például, egy sort, oszlopot, vagy magát csak egy cella elemet.

Egyedi rajzolás

Az egyedi rajzolás a scala.swing-ben hasonló a Java Swing-hez. Ehhez elegendő a megfelelő Component osztályt, vagy ennek egyik alosztályát példányosítani és a paintComponent metódust felüldefiniálni.

Például egy vonal kirajzolása scala.swing-ben:

new Component { ... override def paintComponent(g: Graphics2D) = { super.paintComponent(g) g.setColor(new Color(100,100,100)) g.drawString("Press left mouse button and drag to paint.", 10, size.height-10) g.setColor(Color.black) g.draw(path) } }

A super.paintComponent-t azért szükséges meghívni, hogy a háttér és a különböző dekorációk is kirajzolásra kerüljenek. Ezután kirajzolunk egy szöveget és egy útvonalat, amikor a felhasználó húzza az egeret.

A Component osztályban a következő rajzoló metódusok vannak:

protected def paint(g: Graphics2D) protected def paintBorder(g: Graphics2D) protected def paintChildren(g: Graphics2D) protected def paintComponent(g: Graphics2D)

A paint metódust a Swing rajzoló mechanizmusa hívja meg, amikor az adott komponenst ki akarja rajzolni. Az alapértelmezett paint metódus meghívja a másik három rajzoló metódust.