Layout di elementi GUI

La sesta lezione del tutorial introduttivo sun sulla costruzione di applicazioni GUI con JavaFX é dedicato ai layout. Lo scopo di un layout é quello di permetterci di gestire componenti GUI senza specificare coordinate assolute. Questo ci dà, generalmente, una maggiore flessibilità.

Per dimostrare ciò costruiamo un piccolo script fx che visualizza un simulazione di un semaforo regolato da un gruppo di bottoni radio. Quando un bottone é selezionato il cerchietto che rappresenta il colore ad esso collegato appare colorato, altrimenti viene lasciato in grigio.

La finestra dell'applicazione

Creiamo uno script, a nome trafficLight.fx ad esempio, e dichiariamo lo stage con la annessa scena su cui lavoriamo

import javafx.stage.Stage;
import javafx.scene.Scene;

Stage {
title: "Traffic lights"
scene: Scene {
width: 210, height: 90
} // scene
}

Bottoni

Creiamo un gruppo di bottoni, vogliamo che si possa scegliere un solo bottone alla volta, quindi importiamo e usiamo la classe ToggleGroup. I bottoni utilizzati sono di tipo RadioButton.

import javafx.scene.control.RadioButton;
import javafx.scene.control.ToggleGroup;
...
def group = ToggleGroup { };
def choiceText = ["STOP", "READY", "GO"];

def choices = for (text in choiceText) {
RadioButton {
toggleGroup: group
text: text
} // radioButton
}

choices[0].selected = true;

La costante choices contiene un array di radio buttons generati dal for che cicla sugli elementi di choiceText, a sua volta un array di stringhe.
Ogni radio button ha quindi la corrispondente stringa definita sopra come text, e tutti quanti hanno come toggleGroup la costante group sopra definita.
Nota che il toggleGroup sarà gestito implicitamente dai singoli bottoni.
Come ultima istruzione marchiamo come attivo il primo bottone, ovvero il semaforo sarà inizializzato col rosso.

Cerchi

Definiamo ora la costante che si occuperà di visualizzare il semaforo. Avremo bisogno delle classi Circle, Color, RadiantGradient e Stop, che importiamo.

import javafx.scene.shape.Circle;
import javafx.scene.paint.Color;
import javafx.scene.paint.RadialGradient;
import javafx.scene.paint.Stop;
...
def colors = ["RED", "GOLD", "GREEN"];

def lights = for (color in colors) {
Circle {
centerX: 12, centerY: 12, radius: 12
stroke: Color.DARKGRAY
fill: bind RadialGradient {
centerX: 8, centerY: 8, radius: 12,
proportional: false
stops: [
Stop {offset: 0.0 color: Color.WHITE},
Stop {offset: 1.0 color:
if (choices[indexof color].selected)
then Color.web(color)
else Color.GRAY
} // stop
] // stops
} // radialGradient
} // circle
} // lights

Con un meccanismo simile a quello utilizzato per creare l'array di bottoni creiamo l'array lights che contiene oggetti di tipo Circle.
Da notare il binding dei cerchi ai bottoni: il colore che riempe (fill) il cerchio é determinato dallo stato dell'elemento di choices il cui indice é corrispondente all'indice del cerchio che stiamo colorando. Interessante notare anche l'uso del costrutto if then else, che agisce da operatore ternario, come il buon vecchio "? :".

Box

Inscatoliamo ora le nostre componenti in HBox (orizzontale, le componenti sono piazzate da sinistra verso destra) e VBox (verticale, le componenti vanno dall'alto in basso).
L'idea é quella di avere un HBox esterno che contiene un VBox per i bottoni e un HBox per i cerchi. L'HBox risultante sarà il content della nostra scena:

import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
...
HBox {
spacing: 20
content: [
VBox {
spacing: 10
content: [ for (i in [0..2]) choices[i] ]
}, // vbox
HBox {
spacing: 15,
content: lights,
translateY: 25
}
]
}

Nota che nell'HBox dei cerchi effettuiamo una traslazione verso il basso per far apparire il semaforo centrato, e non appiattito sull'alto della finestra.

Tile

A dire il vero risulta più naturale mantenere bottoni e luci in parallelo, e per far ciò, risulta comodo usare l'inscatolamento via Tile, una sorta di array bidimensionale di cui specifichiamo righe, colonne e contenuto di ogni cella.

import javafx.scene.layout.VBox;
...
Tile {
columns: 2, rows: 3, vgap: 5
content: for (i in [0..2]) [choices[i], lights[i]]
}

Anche come codide risulta decisamente più elegante.

Questo il codice complessivo che descrive lo script che abbiamo sviluppato. Nota l'uso del booleano isBox per permettere di testare i due differenti layout.

package uiTutorial;

import javafx.stage.Stage;
import javafx.scene.Scene;
import javafx.scene.control.RadioButton; // choices
import javafx.scene.control.ToggleGroup; // choices
import javafx.scene.shape.Circle; // lights
import javafx.scene.paint.Color; // lights
import javafx.scene.paint.RadialGradient; // lights
import javafx.scene.paint.Stop; // lights
import javafx.scene.layout.HBox; // layout
import javafx.scene.layout.VBox; // layout
import javafx.scene.layout.Tile; // layout

// choices definition
def group = ToggleGroup { };
def choiceText = ["STOP", "READY", "GO"];

def choices = for (text in choiceText) {
RadioButton {
toggleGroup: group
text: text
} // radioButton
}

choices[0].selected = true;
// choices ready for use

// lights definition
def colors = ["RED", "GOLD", "GREEN"];

def lights = for (color in colors) {
Circle {
centerX: 12, centerY: 12, radius: 12
stroke: Color.DARKGRAY
fill: bind RadialGradient {
centerX: 8, centerY: 8, radius: 12,
proportional: false
stops: [
Stop {offset: 0.0 color: Color.WHITE},
Stop {offset: 1.0 color:
if (choices[indexof color].selected) // binding applies here
then Color.web(color)
else Color.GRAY
} // stop
] // stops
} // radialGradient
} // circle
} // lights

def isBox = true;
def content = if(isBox)
then HBox {
spacing: 20
content: [
VBox {
spacing: 10
content: [ for (i in [0..2]) choices[i] ]
}, // vbox
HBox {
spacing: 15,
content: lights,
translateY: 25
}
]
} // scene content version 1
else Tile {
columns: 2, rows: 3, vgap: 5
content: for (i in [0..2]) [choices[i], lights[i]]
} // scene content version 2

Stage {
title: "Traffic lights"
scene: Scene {
width: 210, height: 90
content: content;
}
}

Nessun commento:

Posta un commento