Interface graphique (I)
Etape 10

1 Introduction

Le but de cette étape est de commencer la réalisation de l'interface graphique en écrivant deux classes contenant respectivement :

  1. les paramètres (utilisateur) du panorama,
  2. le panorama, son image et ses étiquettes.

Les instances de ces deux classes ont la propriété d'être observables, au sens du patron Observer.

L'observabilité est la raison d'être de ces classes : si elles ne devaient pas être observables, on pourrait s'en passer et utiliser des classes existantes à leur place, p.ex. PanoramaUserParameters pour les paramètres utilisateur. L'observabilité est importante pour l'interface graphique, dont les composants doivent se mettre à jour lorsque les attributs des classes susmentionnées changent.

1.1 Interface graphique

L'interface graphique du programme Alpano est composée d'une seule fenêtre, découpée en deux parties principales :

  1. le haut contenant l'image du panorama et les étiquettes,
  2. le bas contenant les paramètres du panorama (à gauche), modifiables par l'utilisateur, et des informations concernant le point situé sous le pointeur de la souris (à droite).

Cette organisation est visible dans la copie d'écran ci-dessous, qui n'inclut pas d'étiquettes en raison de sa petite taille.

gui.png

Figure 1 : Interface graphique d'Alpano (cliquez pour agrandir)

Une caractéristique importante de cette interface est que le panorama n'est pas recalculé automatiquement chaque fois qu'un paramètre est modifié. Au lieu de cela, dès que les paramètres du bas de la fenêtre ne correspondent plus au panorama dessiné en haut, un message recouvre le panorama, demandant à l'utilisateur de cliquer dans la fenêtre une fois les changements de paramètres terminés, afin de mettre à jour l'image. Ce mode de fonctionnement a été choisi car le recalcul du panorama est une opération coûteuse sur laquelle il est bon d'avoir le contrôle.

La mise en œuvre de ce comportement nécessite l'existence de deux versions des paramètres utilisateur : une première correspondant aux paramètres affichés au bas de la fenêtre, une seconde à ceux utilisés lors du calcul de l'image affichée en haut. Dès que ces deux versions diffèrent, il y a incohérence entre les paramètres et l'image, et le message mentionné ci-dessus s'affiche.

Les paramètres du panorama affichés dans le bas de l'image sont stockés dans un objet que nous appellerons le parameters bean. (En Java, le mot bean est souvent utilisé pour désigner des objets dotés d'attributs observables respectant certaines conventions spécifiées par la norme JavaBeans1.)

Les paramètres utilisés pour le calcul du panorama, de même que ce panorama lui-même, son image et ses étiquettes, sont quant à eux stockés dans un second objet que nous appellerons le computer bean, car il se charge entre autres du calcul du panorama.

Les attributs de ces deux « beans » sont observables et liés — au moyen de mécanismes propres à JavaFX — aux différents composants de l'interface graphique. Par exemple, l'attribut donnant la longitude de l'observateur, stocké dans le parameters bean, est lié au champ textuel de l'interface graphique qui correspond à ce paramètre.

De nombreux autres liens similaires existent entre les attributs des deux beans et les composants de l'interface graphique. La figure ci-dessous en montre plusieurs — représentés par des lignes rouges — sur une version très simplifiée de l'interface graphique.

Sorry, your browser does not support SVG.

Figure 2 : Liens entre beans et composants de l'interface graphique

2 Mise en œuvre Java

Comme celui de l'étape précédente, tout le code de cette étape fait partie du paquetage ch.epfl.alpano.gui.

2.1 Beans JavaFX et propriétés

Comme cela a été brièvement expliqué plus haut, un bean JavaFX est un objet Java doté d'attributs observables, que l'on nomme généralement propriétés (properties). Le paquetage javafx.beans.property contient un grand nombre de classes et interfaces facilitant la définition de propriétés observables contenant différents types de valeurs (entiers, objets, etc.), accessibles en lecture seule (read only) ou en lecture et écriture (read-write).

Pour simplifier les choses, nous n'utiliserons qu'un seul type de propriété dans ce projet, celles dont la valeur est un objet de type donné.

2.1.1 Propriétés objet

Les propriétés dont la valeur est un objet sont représentées par plusieurs classes et interfaces, celles qui nous intéressent ici étant :

  • ReadOnlyObjectProperty, une classe abstraite et générique représentant une propriété accessible en lecture seule et contenant un objet ; son paramètre de type donne le type de l'objet qu'elle contient,
  • ObjectProperty, similaire à ReadOnlyProperty mais accessible en lecture et écriture,
  • SimpleObjectProperty, une classe concrète et générique représentant une propriété contenant un objet.

Ces propriétés étant observables, elles offrent la méthode addListener qui permet de leur ajouter un « auditeur », c-à-d un objet qui est informé des changements de la propriété. Cet auditeur est spécifié par l'interface fonctionnelle ChangeListener dont la méthode abstraite, changed, prend trois arguments : la propriété dont la valeur a changé, son ancienne valeur et sa nouvelle valeur.

L'utilisation de ces classes n'est pas très complexe et peut s'illustrer au moyen de l'exemple suivant :

ObjectProperty<String> p =
  new SimpleObjectProperty<>("a");
p.addListener((prop, oldV, newV) ->
  System.out.printf("valeur : %s -> %s (prop : %s)%n",
                    oldV, newV, prop));
p.set("b");
p.set("c");
System.out.println(p.get());

Lorsqu'on l'exécute, les trois lignes suivantes s'affichent à l'écran. Les deux premières sont dues à l'auditeur attaché à la propriété, la dernière à l'énoncé println de la dernière ligne.

valeur : a -> b (prop : ObjectProperty [value: b])
valeur : b -> c (prop : ObjectProperty [value: c])
c

2.2 Classe PanoramaParametersBean

La classe nommée p.ex. PanoramaParametersBean, publique, est un bean JavaFX contenant les paramètres (utilisateur) du panorama.

Cette classe contient un total de 10 propriétés observables : une pour les paramètres composites, accessible en lecture seule, et neuf pour chacun des paramètres individuels, accessibles en lecture et écriture.

A chacune des propriétés correspond une méthode d'accès. Ainsi, à la propriété contenant les paramètres et nommée p.ex. parameters correspond la méthode d'accès suivante :

  • ReadOnlyObjectProperty<PanoramaUserParameters> parametersProperty()

tandis qu'aux neuf paramètres individuels correspondent des méthodes d'accès qui pourraient être :

  • ObjectProperty<Integer> observerLongitudeProperty(),
  • ObjectProperty<Integer> observerLatitudeProperty(),
  • ObjectProperty<Integer> observerElevationProperty(),
  • ObjectProperty<Integer> centerAzimuthProperty(),
  • ObjectProperty<Integer> horizontalFieldOfViewProperty(),
  • ObjectProperty<Integer> maxDistanceProperty(),
  • ObjectProperty<Integer> widthProperty(),
  • ObjectProperty<Integer> heightProperty(),
  • ObjectProperty<Integer> superSamplingExponentProperty().

On peut se demander pourquoi ces neuf propriétés ont le type ObjectProperty<Integer> et pas IntegerProperty, classe dont le but est justement de représenter des propriétés dont la valeur est un entier. La raison en est que cela simplifie le code de création de l'interface graphique finale, comme nous le verrons à l'étape suivante.

2.2.1 Synchronisation et validation des paramètres

Il est important de comprendre que la classe PanoramaParametersBean contient deux copies de chacun des paramètres du panorama : l'une dans la propriété individuelle lui correspondant (p.ex. widthProperty pour la largeur) et l'autre dans l'instance de PanoramaUserParameters stockée séparément. Cette duplication pose deux problèmes :

  1. rien n'empêche un utilisateur externe de PanoramaParametersBean de donner une valeur invalide à l'une des propriétés, car leur valeur n'est pas validée,
  2. suite à la modification externe d'une propriété contenant un paramètre individuel, sa valeur sera différente de celle stockée dans l'instance de PanoramaUserParameters — on dit alors que ces valeurs sont désynchronisées.

Pour résoudre ces deux problèmes, une solution est d'attacher aux propriétés contenant les paramètres individuels un auditeur se chargeant de synchroniser leur valeur avec celle stockée dans l'instance de PanoramaUserParameters. Ce faisant, les paramètres sont automatiquement rendus valides, puisque le constructeur de PanoramaUserParameters valide ses arguments.

La synchronisation des paramètres consiste à :

  1. créer une nouvelle instance de PanoramaUserParameters à laquelle la valeur actuelle de tous les paramètres individuels, stockés dans les propriétés, est passée,
  2. utiliser cette nouvelle instance de PanoramaUserParameters comme nouvelle valeur de la propriété parameters,
  3. copier la valeur de chacun des attributs — désormais valides — de cette nouvelle instance de PanoramaUserParameters dans les propriétés correspondantes.

Cette synchronisation peut se faire dans une méthode privée du bean, appelée depuis tous les auditeurs attachés aux propriétés. Attention toutefois : pour une raison difficile à expliquer à ce stade, l'appel à cette méthode de synchronisation doit être retardé au moyen de la méthode runLater de la classe Platform.

Par exemple, en admettant que la méthode de synchronisation se nomme synchronizeParameters et que property soit une propriété contenant l'un des paramètres individuels, on peut écrire :

ObjectProperty<Integer> property = …;
property.addListener((b, o, n) ->
  runLater(this::synchronizeParameters));

2.3 Classe PanoramaComputerBean

La classe nomée p.ex. PanoramaComputerBean, publique, est un bean JavaFX doté de quatre propriétés : le panorama, ses paramètres (utilisateur), son image et ses étiquettes.

La classe PanoramaComputerBean offre trois méthodes donnant accès aux paramètres du panorama :

  • ObjectProperty<PanoramaUserParameters> parametersProperty(),
  • PanoramaUserParameters getParameters(), et
  • void setParameters(PanoramaUserParameters newParameters).

qui permettent respectivement d'obtenir la propriété JavaFX correspondant aux paramètres et les paramètres eux-même, et de modifier les paramètres. Notez que les deux dernières méthodes sont triviales et ne font rien d'autre que d'appeler respectivement les méthodes get et set de la propriété contenant les paramètres.

Les propriétés restantes, qui contiennent le panorama, son image et ses étiquettes, sont accessibles en lecture seule. Dès lors, seules deux méthodes d'accès leur sont associées. Celles donnant accès au panorama sont :

  • ReadOnlyObjectProperty<Panorama> panoramaProperty(), et
  • Panorama getPanorama().

Celles donnant accès à l'image du panorama sont :

  • ReadOnlyObjectProperty<Image> imageProperty(), et
  • Image getImage().

Finalement, celles donnant accès aux étiquettes du panorama sont :

  • ReadOnlyObjectProperty<ObservableList<Node>> labelsProperty(), et
  • ObservableList<Node> getLabels()

ObservableList est le type des listes observables JavaFX, décrit ci-dessous.

Une caractéristique importante de PanoramaComputerBean est que le panorama, son image et ses étiquettes correspondent toujours aux paramètres stockés dans la propriété parameters. Dès lors, dès que celle-ci change, le panorama, son image et ses étiquettes doivent être recalculés, et les propriétés les contenant mises à jour pour refléter leur nouvelle valeur.

Tout comme pour la synchronisation des paramètres dans le bean des paramètres, cela peut se faire au moyen d'un auditeur, attaché ici à la propriété contenant les paramètres du panorama.

2.3.1 Listes observables JavaFX

Les étiquettes du panorama fournies par le computer bean sont stockées dans une liste observable de type ObservableList<Node>. Le fait que cette liste soit observable permettra, lors de la création de l'interface graphique, de s'assurer que les étiquettes sont automatiquement redessinées lorsqu'elles changent.

ObservableList fait partie du paquetage javafx.collections, qui offre un certain nombre de classes et interfaces représentant des collections observables. Ces collections sont presque identiques à celles de la bibliothèque standard Java — du paquetage java.util — mais sont observables. C'est-à-dire qu'il est possible de leur attacher des auditeurs qui sont prévenus dès que leur contenu change.

Pour créer la liste observable des étiquettes du panorama, seuls deux méthodes de la classe FXCollections (le pendant JavaFX de Collections) sont nécessaires :

La seconde de ces méthodes est utile pour garantir que la liste des étiquettes n'est modifiable que par le computer bean, et pas par un de ses utilisateurs.

Lors du recalcul des étiquettes, il est possible d'utiliser la méthode setAll de ObservableList pour remplacer en un seul appel le contenu de la liste observable des étiquettes par sa nouvelle valeur.

2.4 Tests

A ce stade, les beans ne sont pas très faciles à tester, mais cela n'est pas impossible non plus, comme l'illustre le programme ci-dessous.

Il s'agit d'une très petite application JavaFX — ce qui explique que la classe principale hérite de Application et que son code soit dans la méthode start — qui crée un bean contenant les paramètres d'un panorama, puis modifie la valeur de la latitude deux fois de suite, chaque fois avec une valeur invalide.

import static ch.epfl.alpano.gui.PredefinedPanoramas.*;

import ch.epfl.alpano.gui.PanoramaParametersBean;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.stage.Stage;

public final class BeansUse extends Application {
  public static void main(String[] args) {
    launch(args);
  }

  @Override
  public void start(Stage primaryStage) throws Exception {
    PanoramaParametersBean bean =
      new PanoramaParametersBean(NIESEN);
    ObjectProperty<Integer> prop =
      bean.observerLatitudeProperty();

    prop.addListener((o, oV, nV) ->
      System.out.printf("  %d -> %d (%s)%n", oV, nV, o));
    System.out.println("set to 1");
    prop.set(1);
    System.out.println("set to 2");
    prop.set(2);

    Platform.exit();
  }
}

En l'exécutant, les lignes suivantes s'affichent sur la console :

set to 1
  467300 -> 1 (ObjectProperty [value: 1])
set to 2
  1 -> 2 (ObjectProperty [value: 2])
  2 -> 450000 (ObjectProperty [value: 450000])

La sychronisation (et donc la validation) des arguments est visible sur la dernière ligne, qui montre que la valeur invalide 2 est automatiquement ramenée à la valeur valide la plus proche, 450000, correspondant à 45°.

Le fait que cette correction soit faite après les deux appels à set est dû à l'utilisation de runLater et correspond au comportement attendu.

3 Résumé

Pour cette étape, vous devez :

  • écrire les classes PanoramaComputerBean et PanoramaParametersBean (ou des équivalents) en fonction des indications données plus haut,
  • tester votre code,
  • documenter la totalité des entités publiques que vous avez définies.

Aucun rendu n'est à faire pour cette étape avant le rendu final. N'oubliez pas de faire régulièrement des copies de sauvegarde de votre travail en suivant nos indications à ce sujet.

Notes de bas de page

1

Le mot bean fait référence aux grains de café (coffee beans). Son choix date de l'époque à laquelle tous les projets liés à Java — nommé en référence à l'île la plus peuplée d'Indonésie, grand producteur de café — se devaient d'avoir un nom lié au café. La mode semble heureusement avoir passé.