Link Cerca Menu Expand Document

Interfícies gràfiques

Programació orientada a esdeveniments

Bucle d’esdeveniments

La programació d’interfícies d’usuari es fa mitjançant esdeveniments. Aquesta és la seqüència:

  1. L’usuari interactua amb el GUI
  2. Es produeix un esdeveniment
  3. En resposta, una peça de codi s’executa
  4. 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.

Patró observador

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. El funcionament bàsic és que 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. O sigui, comunicar-los quan passen, en lloc d’haver de comprovar si han ocorregut cada cert temps.

Un patró germà és el publish-subscribe, on parlem de missatges en lloc d’esdeveniments. Tenim publicadors que generen missatges, i els subscriptors interessats es registren i els reben. Aquest patró també es relaciona amb les cues de missatges, habitualment utilitzades conjuntament.

Implementació de les notificacions

Quan l’esdeveniment succeeix, el subjecte acaba notificant a tots els observadors amb una crida al un mètode anomenat update(…). Aquesta notificació o update pot implementar-se de diverses maneres:

  • El mètode update(...) dels observadors pot tenir diversos paràmetres per a indicar a l’observador quin esdeveniment s’ha produït. En el nostre exemple, un paràmetre amb l’esdeveniment anomenat Event.
  • El mètode pot dir-se de moltes maneres. Per exemple, onEvent() o actionPerformed() són altres nomenclatures habituals.
  • L’objecte Event pot contenir el subjecte, el tipus d’esdeveniment (si hi ha més d’un) i altres paràmetres addicionals d’ajuda per a l’observador.
  • Quan el subjecte genera diversos tipus d’esdeveniments, podem implementar-ho de diferents maneres:
    • Tenir una sola classe Event i indicar el seu tipus en un camp amb, per exemple, un enum.
    • Implementar Event com a una classe abstracta amb subclasses per a cada tipus d’esdeveniment, on cadascuna emmagatzema informació diversa. Això requerirà l’ús de instanceof per a distinguir-los.
    • Tenir diverses signatures del mètode de notificació, una per tipus esdeveniment. Per exemple, onEventX(), onEventY(), etc. L’avantatge és que cada mètode pot tenir paràmetres diferents per cada tipus d’esdeveniment.
  • Tots aquests mètodes no han de retornar res (tipus void). Només es vol notificar als observadors, però el subjecte no necessita res d’ells.

Callbacks

El patró observador utilitza múltiples noms per a la parella subjecte/observador. El subjecte també pot anomenar-se observable o event source. L’observador també pot anomenar-se handler, listener o callback. Tot depèn del context.

Els callbacks són el mateix concepte explicat diferent. Un callback és un codi que passem com a paràmetre a un component, i que aquest executarà més endavant, possiblement de forma asíncrona. Les callbacks s’utilitzen quan només hi ha un observador per al subjecte.

Per exemple, a JavaFX, un botó té aquest mètode:

void setOnAction(EventHandler<ActionEvent> value)

El botó és el subjecte i EventHandler és el callback que s’executarà quan es cliqui. Per la seva banda, EventHandler és una interfície funcional, és a dir, amb un sol mètode:

void handle(ActionEvent event) 

El client (codi que utilitza la llibreria) haurà d’implementar el mètode handle, que rep ActionEvent, un objecte amb el source, target i type de l’esdeveniment.

JavaFX

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.

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:

  1. Preparar un graf de scene.
  2. Construir un scene amb el node arrel del graf.
  3. Configurar el stage amb aquest scene.

Tipus de nodes

Els nodes poden ser de tres tipus:

  1. Arrel: el primer del graf. Pot ser del tipus 2.
  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.
  3. Leaf: no conté nodes fill.

Els nodes (components visuals) inclouen:

  • Figures geomètriques (javafx.scene.shape): Circle, Rectangle, Polygon, etc.
  • Controls (javafx.scene.control): Button, Checkbox, Choice Box, Text Area, etc.
  • Contenidors (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.

Aplicació

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);      
    } 
}

Scene Graph

Tenim bàsicament dos tipus de grafs de nodes: Group i Region.

Group

Group root = new Group();
ObservableList list = root.getChildren(); 
list.add(nodeObject1);

Scene scene = new Scene(root);
primaryStage.setScene(scene); 

Region

StackPane pane = new StackPane();       
ObservableList list = pane.getChildren(); 
list.add(nodeObject1);

Scene scene = new Scene(root);
primaryStage.setScene(scene); 

Layouts

FXML

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 utilitzant un ID i amb una action:

<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.

Múltiples finestres

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.

Gestió d’esdeveniments

Els esdeveniments notifiquen a l’aplicació de les accions de l’usuari. Els esdeveniments són subclasses d’Event. Per exemple, MouseEvent, KeyEvent, DragEvent o WindowEvent.

Partim d’un exemple: un clic del ratolí a un botó. Llavors, un esdeveniment es compon de:

  • Destí: el node on succeïx l’esdeveniment. Pot ser una finestra, una escena o un node. En l’exemple, el botó.
  • Origen: el lloc on es genera l’esdeveniment. En l’exemple, el ratolí.
  • Tipus: el tipus. En l’exemple, clicar el ratolí.

Processament d’esdeveniments

El processament de l’esdeveniment és el següent:

  1. 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.
  2. Construcció de l’encaminament: en funció de la jerarquia dels nodes. És el camí des del stage fins arribar al node destí.
  3. Captura (camí des del stage fins al destí). Aquí no es criden els gestors, però sí els filtres, que poden consumir l’esdeveniment amb event.consume() i finalitzar la captura.
  4. 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.

Classes anònimes i expressions Lambda

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!")
);

Mètodes per afegir gestors i filtres

Els filtres permeten gestionar el processament de l’esdeveniment i consumir-lo, si cal.

<T extends Event> void addEventFilter(
    EventType<T> eventType, EventHandler<? super T> eventFilter)
<T extends Event> void removeEventFilter(
    EventType<T> eventType, EventHandler<? super T> eventFilter)

Els gestors (handlers) permeten a les aplicacions prendre accions en funció del seu tipus, origen i destí.

<T extends Event> void addEventHandler(
    EventType<T> eventType, EventHandler<? super T> eventHandler)
<T extends Event> void removeEventHandler(
    EventType<T> eventType, EventHandler<? super T> eventHandler)

Per als gestors tenim els mètodes generals que hem vist i els mètodes de conveniència .setXXX() que faciliten escriure el codi sense haver d’indicar el tipus d’esdeveniment. Tots els setters treballen amb un sol handler, mentre que l’add/remove permet afegir diversos handlers al mateix esdeveniment.

button.addEventHandler(MouseEvent.MOUSE_CLICKED, mouseHandler);
button.setEventHandler(MouseEvent.MOUSE_CLICKED, mouseHandler);
button.setOnMouseClicked(mouseHandler);
button.setOnAction(actionHandler);

Aquests són alguns dels mètodes de conveniència disponibles:

  • General: setOnAction
  • Ratolí: setOnMouseClicked, setOnMouseEntered, setOnMouseExited, setOnMousePressed
  • Teclat: 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.

Patró de disseny UI

Un patró de disseny associat típicament al desenvolupament d’interfícies d’usuari (UI) és el model-vista-controlador (MVC). S’utilitza habitualment a l’entorn web, i la seva implementació pot variar. El següent diagrama és una opció.

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. Està més associat amb aplicacions natives com, per exemple, JavaFX.

Aquestes són les responsabilitats de cada component del patró:

  • 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.

Encapsulació al patró MVP

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.

Si fem bé l’encapsulació podem testar tant el presentador com el model. L’aproximació és utilitzar una vista passiva: la vista mai es comunica amb el model, i és el presentador qui gestiona els seus esdeveniments i l’actualitza. Per fer el testing general podem utilitzar un doble de proves de la vista.

Així es defineixen i relacionen les parts:

  • La vista envia esdeveniments al seu únic observador, el presentador. Té mètodes que permeten al presentador actualitzar-la, i és agnòstic d’aquest. Només la vista conté 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 enviar comandes o consultes).
  • El model envia esdeveniments al presentador, el seu observador. El model és agnòstic respecte del funcionament del presentador i tampoc coneix cap aspecte visual.

A continuació es mostra una plantilla d’interfícies per a aquest patró.


interface View {
    void setListener(ViewListener l); // permet registrar el presenter
    // cal afegir comandes des del presentador
}
interface ViewListener {
    // cal afegir mètodes que escolten esdeveniments de la view
}
interface Presenter extends ViewListener, ModelListener {
    void start(); // mètode d'inici de l'aplicació
    void stop(); // mètode de fi de l'aplicació
}
interface Model {    
    void setListener(ModelListener l); // permet registrar el presenter
    // cal afegir els comandes i queries del presenter
}
interface ModelListener {
    // cal afegir un mètode per cada esdeveniment generat pel model
}

Concurrència

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.

Si un altre fil vol modificar la GUI, cal que utilitzi la següent construcció per afegir la tasca a la cua d’esdeveniments:


Platform.runLater(new Runnable() {
    @Override
    public void run() {
        // acció que es vol realitzar
    }
});

Binding de propietats

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());

Referències