La programació d'interfícies d'usuari es fa mitjançant esdeveniments. Aquesta és la seqüència:
L'usuari interactua amb el GUI
Es produeix un esdeveniment
En resposta, una peça de codi s'executa
S'actualitza l'aparença del GUI
Aquestes operacions es produeixen dins del bucle d'esdeveniments (event loop). Els esdeveniments s'afegeixen a una mena de cua, i es van satisfent o gestionant amb el codi que el programador ha decidit. Aquest bucle és un sol fil, i per tant no es poden realitzar operacions massa llargues, ja que es bloquejaria el GUI i deixaria de respondre.
El codi equivalent seria:
do {
e = getNextEvent();
processEvent(e);
} while (e != quit);
El flux d'un programa amb GUI no està predeterminat: depèn dels esdeveniments que es produeixin. En contrast, les aplicacions que es recolzen en algorismes esperen unes dades d'entrada en un ordre i temps predeterminat.
El patró principal que s'utilitza a les interfícies gràfiques és el de l'observador. En aquest patró intervenen una parella subjecte/observador. També es poden dir observable/observador, o origen/gestor d'esdeveniments (event source/handler). L'observador també es pot dir Listener. Bàsicament tenim un subjecte que genera esdeveniments i un o més observadors que els escolten. Això ens permet fer push dels esdeveniments, en lloc de fer polling.
Aquest apartat utilitzarà JavaFX per a la creació d'interfícies gràfiques. Aquesta plataforma substitueix Swing com a llibreria GUI de Java, i permet desenvolupar aplicacions d'escriptori.
A la pàgina Eclipse es pot veure la configuració necessària per treballar amb JavaFX a l'IDE Eclipse.
Aquests són els aspectes principals dels components gràfics de JavaFX:
Un programa JavaFX consisteix a una classe que estén la classe abstracta javafx.application.Application.
El contenidor de màxim nivell màxim és javafx.stage.Stage. Es correspon amb una finestra.
Els components visuals (nodes) estan continguts dins d'un scene: javafx.scene.Scene.
Una aplicació pot contenir diverses scenes, però només una es pot mostrar al stage.
Un scene conté un graf jeràrquic de nodes: javafx.scene.Node.
Per tant, per construir la UI cal:
Preparar un graf de scene.
Construir un scene amb el node arrel del graf.
Configurar el stage amb aquest scene.
Els nodes poden ser de tres tipus:
Arrel: el primer del graf. Pot ser del tipus 2.
Parent (Branch)
Group: és un node col.lectiu que renderitza tots els seus fills en ordre.
Region: base per als controls UI, com Chart, Pane i Control.
WebView: gestiona l'engine web.
Leaf: no conté nodes fill.
Els nodes (components visuals) inclouen:
Figures geomètriques: Circle, Rectangle, Polygon, etc.
Controls (javafx.scene.layout): Button, Checkbox, Choice Box, Text Area, etc.
Contenidors (layout panes javafx.scene.layout): Border Pane, Grid Pane, Flow Pane, etc.
Elements media (Audio, Video, Image)
Cada tipus de node té mètodes que permeten modificar el seu aspecte o el seu contingut, habitualment són getters i setters.
Per exemple: un Label és un node amb un text, i té dos mètodes per accedir i canviar el contingut: setText i getText.
També hi ha la possibilitat de treballar directament amb un canvas, dibuixant en ell.
És la classe Canvas. Aquí hi ha una explicació de com funciona.
Ara veure'm un exemple mínim d'aplicació. Tenim els següents components gràfics:
Stage: la finestra principal
Scene: el contenidor del graf d'elements gràfics
Graf scene: la jerarquia d'elements gràfics, en aquest exemple: Label.
public class JavaWorldApp extends Application {
@Override
public void start(Stage primaryStage) throws Exception {
// creació del stage, scene i scene graph
primaryStage.setTitle("Hello world App");
Label label = new Label("Hello World!");
Scene scene = new Scene(label, 400, 200);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String args[]){
launch(args);
}
}
Tenim bàsicament dos tipus de grafs de nodes: Group i Region.
Group root = new Group();
ObservableList list = root.getChildren();
list.add(nodeObject1);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
StackPane pane = new StackPane();
ObservableList list = pane.getChildren();
list.add(nodeObject1);
Scene scene = new Scene(root);
primaryStage.setScene(scene);
A l'hora de crear elements gràfics tenim dues opcions: crear-los programàticament o bé amb un arxiu de tipus XML anomenat FXML. El format FXML facilita el dibuix mitjançant eines de disseny com el Scene Builder. A més, permet associar el codi XML amb el codi Java:
Cal definir un controlador, un objecte Java que serà el nexe de comunicació del món XML i el món Java. Aquest ha d'implementar la interfície javafx.fxml.Initializable.
Defineix associacions entre objectes al FXML (propietat fx:id dels elements) i objectes Java del controlador.
Defineix associacions entre accions al FXML (propietat onAction dels elements) i mètodes Java del controlador.
Per a carregar un arxiu FXML cal fer les següents operacions:
FXMLLoader loader = new FXMLLoader();
loader.setController(controlador);
loader.setLocation(getClass().getResource("/cami/arxiu.fxml"));
Parent parent = loader.load();
Scene scene = new Scene(parent);
El camí pot ser absolut (utilitzant la jerarquia de paquets) o bé relatiu al paquet actual, sense utilitzar camí.
Les associacions al FXML es poden fer així:
<Label fx:id="inputLabel" ... </Label>
...
<Button ... onAction="#onButtonClick" ... />
Aquest codi es correspondrà amb el següent al controlador:
@FXML
private Label inputLabel;
...
@FXML
private void onButtonClick(ActionEvent event) {
...
}
Amb aquest codi podem accedir a l'etiqueta definida al XML mitjançant l'objecte inputLabel, i cada cop que es cliqui al botó es cridarà al mètode onButtonClick.
Una stage equival a una finestra.
Podem canviar el contingut d'una finestra modificant el graf de scenes. Això es pot fer amb el mètode:
scene.setRoot(Parent node)
Podem crear finestres modals de tres tipus:
Alert
TextInputDialog
ChoiceDialog
El mètode start(Stage primaryStage) d'una aplicació permet establir la finestra principal, però es podrien crear noves, modals o no. Per fer-ho, crear una stage, i utilitzar els mètodes:
stage.initOwner(Window w)
stage.initModality(Modality m)
Modality pot tenir tres valors:
Modality.NONE: un stage que no bloqueja cap altra finestra.
Modality.WINDOW_MODAL: un stage que impedeix que els esdeveniments d’entrada es lliurin a totes les finestres des del seu pare fins a l’arrel. La seva arrel és la finestra més avantpassada sense owner.
Modality.APPLICATION_MODAL: un stage que impedeix que els esdeveniments d'entrada es lliurin a totes les finestres des de la mateixa aplicació, excepte els de la seva jerarquia de fills.
Els esdeveniments notifiquen a l'aplicació de les accions de l'usuari.
Un esdeveniment es compon de tipus, origen i destí. L'origen canvia mentre es fa l'encaminament. El destí pot ser qualsevol objecte que implementi EventTarget . Això passa amb Window, Scene i Node.
Els esdeveniments tenen gestors (handlers), que permeten a les aplicacions prendre accions en funció del seu tipus, origen i destí.
Els filtres permeten gestionar el processament de l'esdeveniment, com s'explica a continuació.
El processament de l'esdeveniment és el següent:
Selecció del destí:
Si és un esdeveniment de tecles (keys), l'element que tingui el focus.
Si és un esdeveniment de mouse, l'element a sota. Si hi ha més d'un, el que estigui a sobre.
Construcció de l'encaminament: en funció de la jerarquia dels nodes.
Captura (camí des del stage fins al destí). Aquí no es criden els gestors, però sí els filtres, que poden consumir l'esdeveniment i finalitzar la captura.
Retorn (bombolla): pel camí de tornada cap al stage. Aquí es criden els gestors. Si el gestor d'un node no consumeix l'esdeveniment, un gestor del node pare pot fer-ho, permetent gestors comuns per diversos nodes fill.
Exemple de gestió d'un esdeveniment d'un botó (control de tipus Button):
button.setOnAction(new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
System.out.println("Botó clicat!");
}
});
Aquest codi utilitza classes anònimes.
També podem utilitzar expressions Lambda, ja que els gestors d'esdeveniments són interfícies funcionals (un sol mètode abstracte):
buttn.setOnAction(
event -> System.out.println("Botó clicat!")
);
Tenim mètodes associats als nodes (controls o no) que ens permeten afegir aquests gestors. Són de dos tipus:
el de tipus genèric, addEventHandler, amb un primer paràmetre amb el tipus d'esdeveniment (veure diagrama anterior):
<T extends Event> void addEventHandler(EventType<T> eventType, EventHandler<? super T> eventHandler)
els de tipus particulars, més fàcils de recordar i amb només un paràmetre, com:
setOnAction (el botó de l'exemple)
setOnMouseClicked, setOnMouseEntered, setOnMouseExited, setOnMousePressed
setOnKeyTyped, setOnKeyPressed, setOnKeyReleased
En general, setOnAction funciona per tots els controls. Hi ha casos especials, com per exemple si volem atendre el canvi de qualsevol contingut d'un TextField. Es pot utilitzar:
TextField.textProperty().addListener(ChangeListener listener)
I per escoltar un índex numèric sobre un ChoiceBox:
ChoiceBox.getSelectionModel().selectedIndexProperty().addListener(ChangeListener listener)
Pots veure la llista de controls i com utilitzar-los.
Un patró de disseny associat típicament al desenvolupament d'interfícies d'usuari (UI) és el model-vista-controlador (MVC). Una particularització d'aquest patró és el model-vista-presentador (MVP), on el presentador és un controlador que fa d'intermediari entre la vista (passiva) i el model.
A l'hora de dissenyar una aplicació JavaFX, és important tenir en compte:
Vista: genera la part visual de l'aplicació. Envia esdeveniments cap al presentador, i rep peticions del presentador per actualitzar-se.
Presentador: s'encarrega de mitjançar entre la vista i el model. No conté cap codi associat a la UI.
Model: part del patró que s'encarrega d'accedir a funcionalitats o dades a una llibreria independent, que no té cap relació amb la presentació visual.
En aquest esquema, és important encapsular correctament. Si es fa bé, tant la vista com el model serien substituïbles. Això només es pot aconseguir si les tres parts es relacionen mitjançant abstraccions d'un contracte, les quals poden ser implementades mitjançant una interfície Java.
Així es defineixen i relacionen les parts:
La vista té una instància del presentador. Li serveix per poder enviar-li esdeveniments. Només la vista hauria de contenir classes de la llibreria visual (JavaFX).
El presentador té una instància de la vista (per enviar-li actualitzacions de la part visual) i una instància del model (per fer consultes i actualitzacions).
El model té una instància del presentador. Li serveix per enviar canvis d'estat al presentador. Per implementar aquesta relació s'utilitza el patró de l'observador. D'aquesta forma, el model és agnòstic respecte de com l'utilitzen, i no coneix cap aspecte visual.
Les aplicacions GUI (interfície gràfica d'usuari) de Java (inclosa JavaFX) són inherentment multifil. Diversos fils realitzen tasques diferents per mantenir la interfície d'usuari en sincronització amb les accions de l'usuari. JavaFX utilitza un únic fil, anomenat JavaFX Application Thread, per processar tots els esdeveniments de la interfície d'usuari. Els nodes que representen la interfície d'usuari d'una gràfica d'escena no són segurs. El disseny de nodes que no són segurs per a fils presenta avantatges i inconvenients. Són més ràpids, ja que no hi ha cap sincronització. L’inconvenient és que s’han d’accedir des d’un mateix fil per evitar estar en un estat il·legal. JavaFX posa una restricció a la qual s’ha d’accedir a un gràfic d’escena en directe des d’un únic fil, el fil d’aplicacions JavaFX. Aquesta restricció imposa indirectament una altra restricció que un esdeveniment d’UI no ha de processar una tasca de llarga durada, ja que farà que l’aplicació no respongui.
Una propietat és un atribut accesible públicament i que afecta el seu estat i/o comportament. Les propietats són observables: poden notificar a observadors de canvis. Poden ser només lectura, només escriptura o lectura i escriptura.
El binding de les dades, en aquest context, es refereix a la relació entre variables d'un programa per tal de mantenir-se sincronitzades. A les GUI ens permet mantenir sincronitzats elements de la capa de model amb els elements GUI corresponents. Això s'aconsegueix gràcies a la implementació del patró observador.
Tipus de binding:
Eager (ansiós) o lazy (mandrós): si el valor de variable es recalcula immediatament quan ho fa la dependència, o només quan es llegeix. Les propietats de JavaFX utilitzen avaluació mandrosa.
Unidireccional o bidireccional: si només es fa en una direcció la sincronització, o en les dues.
Exemple de binding unidireccional:
IntegerProperty p1 = new SimpleIntegerProperty(1);
IntegerProperty p2 = new SimpleIntegerProperty(2);
p1.bind(p2); // p1 pren el valor de p2
p2.set(3);
int valor1 = p1.get(); // retorna 3
Exemple de binding bidireccional:
IntegerProperty p1 = new SimpleIntegerProperty(1);
IntegerProperty p2 = new SimpleIntegerProperty(2);
p1.bindBidirectional(p2);
p2.set(3);
int valor1 = p1.get(); // retorna 3
p1.set(4);
int valor2 = p2.get(); // retorna 4
Els binding es poden fer a JavaFX utilitzant les propietats associades als elements gràfics. Per exemple:
TextField tf1 = new TextField();
TextField tf2 = new TextField();
tf1.textProperty().bind(tf2.textProperty());
Per associar una etiqueta a una propietat del model de diferents tipus podem utilitzar el mètode asString():
Label l = new Label();
IntegerProperty p = new SimpleIntegerProperty(3);
l.textProperty().bind(p.asString());