La programació d'interfícies d'usuari es fa mitjançant esdeveniments. Aquesta és la seqüència:
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:
javafx.application.Application
.javafx.stage.Stage
. Es correspon amb una finestra.javafx.scene.Scene
.javafx.scene.Node
.Per tant, per construir la UI cal:
Els nodes poden ser de tres tipus:
Els nodes (components visuals) inclouen:
javafx.scene.layout
): Button, Checkbox, Choice Box, Text Area, etc.javafx.scene.layout
): Border Pane, Grid Pane, Flow Pane, etc.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:
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:
fx:id
dels elements) i objectes Java del controlador.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:
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:
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:
<T extends Event> void addEventHandler(EventType<T> eventType, EventHandler<? super T> eventHandler)
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:
I per escoltar un índex numèric sobre un ChoiceBox:
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:
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:
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:
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());