Document ID: /fineweb-2-swissfilter-quality_10-filterrobots/filtered/07114.jsonl.gz/769

Interface graphique (II)
Etape 11
1 Introduction
Le but de cette étape, la dernière de la partie obligatoire du projet, est de terminer l'interface graphique et, du même coup, le programme Alpano.
1.1 Interface graphique
L'interface graphique a déjà été présentée à l'étape précédente, mais ses trois composantes principales sont rappelées dans la figure 1 ci-dessous :
- la partie supérieure montrant l'image du panorama et les étiquettes,
- la partie inférieure gauche montrant les paramètres et permettant leur modification,
- la partie inférieure droite montrant des informations liées au point du panorama actuellement situé sous le pointeur de la souris.
Le comportement général de ces trois parties a déjà été décrit, raison pour laquelle seule leur mise en œuvre Java est détaillée dans ce qui suit.
Figure 1 : Les trois parties principales de l'interface graphique d'Alpano
2 Mise en œuvre Java
La mise en œuvre de l'interface graphique se fait dans une unique classe contenant le programme principal, placée comme d'habitude dans le paquetage
ch.epfl.alpano.gui.
2.1 Classe
Alpano
La classe
Alpano contient le programme principal du projet. Elle se charge de construire l'interface graphique, puis laisse le contrôle à JavaFX, qui gère les événements et informe les différents auditeurs attachés à l'interface jusqu'à ce que l'utilisateur quitte l'application.
Comme toute classe représentant le programme principal d'une application JavaFX, la classe
Alpano doit hériter de la classe
Application et son code principal doit se trouver dans la redéfinition de la méthode
start, ainsi :
import javafx.application.Application; public final class Alpano extends Application { public static void main(String[] args) { Application.launch(args); } @Override public void start(Stage primaryStage) { // … création de l'interface graphique BorderPane root = …; Scene scene = new Scene(root); primaryStage.setTitle("Alpano"); primaryStage.setScene(scene); primaryStage.show(); } }
Au démarrage du programme, la liste des sommets est lue depuis le fichier nommé
alps.txt, de même que les huit fichiers HGT couvrant la zone allant de 6° à 10° de longitude et de 45° à 47° de latitude, qui sont combinés en un unique MNT discret. Ces huit fichiers HGT suffisent au dessin de tous les panoramas prédéfinis, et ne charger qu'eux permet de ne pas inutilement ralentir l'exécution du programme.
Attention : aussi bien le fichier contenant les sommets que les fichiers HGT doivent être lus depuis le répertoire courant. C'est-à-dire que les instances de
File leur correspondant ne doivent contenir que le nom du fichier, sans aucun chemin.
Par exemple, les sommets doivent être lus du fichier créé par l'appel
new File("alps.txt"), le premier fichier HGT par l'appel
new File("N45E006.hgt"), et ainsi de suite.
Si vous ne respectez pas ces consignes, votre programme ne fonctionnera pas durant nos tests et vous perdrez bêtement tous les points liés aux tests fonctionnels.
Une fois les fichiers chargés, l'interface graphique est construite de la manière décrite plus bas. Les paramètres qui se trouvent initialement dans l'interface sont ceux du panorama prédéfini nommé Alpes du Jura, présenté à l'étape 8. Notez que ces paramètres ne sont chargés que dans le bean des paramètres, pas dans celui chargé du calcul.
2.1.1 Construction de l'interface graphique
Le code de construction de l'interface graphique est relativement long et mérite donc d'être judicieusement découpé en plusieurs méthodes, chacune chargée d'une partie importante de cette interface.
Le graphe de scène correspondant à l'interface complète est présenté dans la figure ci-dessous. A la plupart des flèches liant un nœud parent à l'un de ses fils est associé un nom qui donne une idée du contenu de ce dernier. Les parties entourées par une ligne traitillée correspondent aux parties principales de l'interface graphique présentées dans la figure 1.
Figure 2 : Graphe de scène d'Alpano
Le nœud nommé « … » et situé en bas à gauche signifie que le panneau
labelsPane — qui contient les étiquettes et lignes du panorama — possède un nombre variable de fils, un pour chaque ligne (de type
Line) et un pour chaque étiquette (de type
Text).
Le second nœud nommé « … » et situé vers le haut à droite signifie que le panneau
paramsGrid — qui contient les composants graphiques présentant les paramètres du panorama — possède un grand nombre de fils, la plupart de type
Label et
TextField. Notez que parmi ces fils figure aussi la zone textuelle contenant les informations au sujet du point du panorama situé sous le pointeur de la souris.
Comme cette figure le montre, à la racine du graphe de scène se trouve un panneau de type
BorderPane. La partie centrale de ce panneau (
Center) est occupée par l'image du panorama (sous-arbre entouré de gauche, portant le numéro 1) tandis que sa partie inférieure (
Bottom) est occupée par le panneau des paramètres et des informations (sous-arbre entouré de droite, portant les numéros 2 et 3).
2.1.2 Image du panorama
La taille de l'image du panorama affichée à l'écran est celle donnée par les paramètres du panorama, sans prendre en compte le suréchantillonnage. Par contre, la taille de l'image du panorama calculée tient compte du suréchantillonnage. Dès lors, si le degré de suréchantillonnage n'est pas nul, l'image calculée doit être redimensionnée avant d'être affichée. La responsabilité de ce redimensionnement est laissée au composant
ImageView, d'une part car cela est plus simple et d'autre part car cela produit une image de meilleure qualité sur les écrans à haute résolution (appelés « Retina » par Apple, « HiDPI » par d'autres fabricants).
Pour garantir ce redimensionnement, la propriété
fitWidth du composant
ImageView doit toujours être égale à la largeur du panorama. Il est très simple de s'en assurer, en liant simplement la propriété
fitWidth du composant
ImageView à la propriété
width du bean des paramètres. De la même manière, il est très simple de s'assurer que l'image affichée par le composant est toujours celle du panorama en liant sa propriété
image à celle du même nom du bean calculateur (computer bean).
Etant donné que le redimensionnement ne doit pas déformer l'image du panorama, la propriété
preserveRatio du composant
ImageView doit être mise à vrai. Sa propriété
smooth doit également l'être, pour garantir un redimensionnement de bonne qualité.
Lors du survol du panorama par la souris, des informations sur le point se trouvant sous le pointeur sont affichées dans le panneau d'information. Pour ce faire, un auditeur des mouvements de la souris est attaché au composant
ImageView, via la méthode
setOnMouseMoved. Cet auditeur convertit la position du pointeur de la souris en le point du panorama correspondant, extrait les données qui y sont associées (position, distance, etc.) et fait en sorte qu'elles soient affichées selon le format suivant :
Position : 45.9380°N 7.2986°E Distance : 124.8 km Altitude : 4267 m Azimut : 162.3° (S) Elévation : 0.8°
Cet exemple a été obtenu en plaçant le pointeur de la souris proche du sommet du Grand Combin, dans le panorama des Alpes vues du Jura. Notez qu'après la valeur de l'azimut en degré figure l'octant correspondant, sous forme textuelle, entre parenthèses.
Lors du clic sur un point du panorama, une carte OpenStreetMap centrée sur ce point et dotée d'un marqueur est ouverte dans le navigateur de l'utilisateur. Pour ce faire, un auditeur des clics de la souris est attaché au composant
ImageView, via la méthode
setOnMouseClicked.
L'URL désignant une carte OpenStreetMap centrée (et marquée) en un point donné a la forme suivante :
http://www.openstreetmap.org/?mlat=ϕ&mlon=λ#map=15/ϕ/λ
où ϕ est la latitude du point en degrés, λ sa longitude en degrés et 15 est le niveau de zoom.
Notez que le séparateur décimal à utiliser pour la longitude et la latitude est le point, pas la virgule. Dès lors, si vous utilisez la méthode
format de
String pour construire les composantes de l'URL, prenez garde au fait que par défaut elle tient compte des conventions locales de la langue du système pour choisir le séparateur décimal, comme l'illustre l'extrait de programme suivant :
// Dangereux : "3.14" ou "3,14" en fonction de la langue String s1 = String.format("%.2f", Math.PI); // Sûr : toujours "3.14" String s2 = String.format((Locale)null, "%.2f", Math.PI);
L'URL elle-même est représentée par une instance de la classe
URI, qui peut être ouverte dans le navigateur de l'utilisateur au moyen de la méthode
browse, comme l'extrait de programme suivant l'illustre :
String qy = …; // "query" : partie après le ? String fg = …; // "fragment" : partie après le # URI osmURI = new URI("http", "www.openstreetmap.org", "/", qy, fg); java.awt.Desktop.getDesktop().browse(osmURI);
2.1.3 Etiquettes du panorama
Les étiquettes du panorama, produites par la classe
Labelizer décrite à l'étape 9, sont les fils d'un panneau de type
Pane et nommé
labelsPane dans la figure 2.
Tout comme la propriété
fitWidth du composant
ImageView doit toujours être égale à la largeur du panorama, la propriété
prefWidth du panneau des étiquettes doit toujours être égale à la largeur du panorama. De même, sa propriété
prefHeight doit toujours être égale à la hauteur du panorama. Bien entendu, cette égalité est garantie au moyen de liens entre ces propriétés.
Finalement, les enfants du panneau des étiquettes sont liés à la liste des étiquettes du bean calculateur. Cela peut se faire facilement au moyen de la méthode
bindContent de la classe
Bindings, car la liste des étiquettes du bean calculateur est une liste observable.
2.1.4 Notice de mise à jour
La notice de mise à jour est un panneau placé au-dessus de celui montrant l'image étiquetée du panorama, et qui n'est visible que lorsque les paramètres figurant en bas de l'interface ne correspondent pas à ceux de l'image affichée en haut. Ce panneau, partiellement transparent, affiche en gros caractères le texte « Les paramètres du panorama ont changé. Cliquez ici pour mettre le dessin à jour. »
L'animation ci-dessous illustre l'apparition et la disparition de ce panneau suite à un changement de paramètre, ici le degré de suréchantillonnage. L'image du panorama a été dessinée avec un degré de 1 (2×), et dès que l'utilisateur change le degré pour le faire passer à 2 (4×), la notice de mise à jour recouvre le panorama. Bien entendu, si le degré est remis à sa valeur originale, le panneau disparaît, sans recalcul du panorama.
Figure 3 : Apparition et disparition de la notice de mise à jour
Comme le message figurant sur le panneau l'indique, il est également possible de cliquer sur le panneau lui-même afin de provoquer le recalcul du panorama en utilisant les nouveaux paramètres. Une fois ce recalcul terminé, le panneau disparaît automatiquement, puisque l'égalité entre les paramètres du panorama et ceux affichés dans le bas de la fenêtre a été rétablie.
Le panneau de mise à jour, nommé
updateNotice dans la figure 2, a le type
StackPane, ce qui est nécessaire pour que le texte qu'il contient soit centré. Son arrière plan, qui lui est attribué au moyen de la méthode
setBackground, est simplement un blanc pur d'une opacité de 90%. Le texte lui-même est une instance de
Text dont la police est celle par défaut mais avec une taille de 40 points. Cette police s'obtient au moyen d'un des constructeurs de la classe
Font et est attribuée au texte au moyen de la méthode
setFont. Finalement, le texte est centré via un appel à la méthode
setTextAlignment.
La visibilité conditionnelle du panneau de mise à jour s'obtient très facilement en liant sa propriété
visible à l'inégalité des propriétés
parameters du bean calculateur et du bean des paramètres. Cette dernière s'obtient quant à elle avec la méthode
isNotEqualTo, dont l'exemple ci-dessous illustre l'utilisation :
ObjectProperty<String> p1 = new SimpleObjectProperty<>("a"); ObjectProperty<String> p2 = new SimpleObjectProperty<>("b"); BooleanExpression p1NotEqP2 = p1.isNotEqualTo(p2); System.out.println(p1NotEqP2.get()); // affiche "true" p2.set("a"); System.out.println(p1NotEqP2.get()); // affiche "false" p1.set("b"); System.out.println(p1NotEqP2.get()); // affiche "true"
Finalement, un clic sur le panneau de mise à jour doit provoquer la copie des paramètres stockés dans le bean des paramètres dans le bean calculateur. Cette copie provoque à son tour le recalcul du panorama et donc, lorsque celui-ci est terminé, la disparition du panneau de mise à jour.
2.1.5 Combinaison de l'image, des étiquettes et de la notice
Attention : pour que le composant contenant l'image du panorama reçoive les événements de la souris, il faut absolument rendre le panneau contenant les étiquettes transparent à la souris, au moyen de
setMouseTransparent.
L'empilement de l'image du panorama et de ses étiquettes est ensuite placé dans un composant
ScrollPane, afin qu'il soit possible de visualiser un panorama plus grand que l'écran par défilement (scrolling).
Finalement, le panneau contenant la notice de mise à jour est placé au-dessus de celui contenant l'image étiquetée du panorama.
2.1.6 Paramètres et panneau d'information
Le panneau contenant les paramètres,
paramsGrid dans la figure 2, est de type
GridPane. Ce type de panneau arrange ses fils sur une grille, ce qui est très utile pour aligner plusieurs éléments d'une interface graphique. La figure ci-dessous montre l'aspect final du panneau des paramètres — sans le panneau d'information — et l'arrangement des éléments en grille y est bien visible.
Figure 4 : Panneau des paramètres
A chacun des paramètres du panneau correspond deux composants graphiques :
- une étiquette, de type
Label, nommant le paramètre, p.ex. Latitude (°),
- un composant montrant la valeur actuelle du paramètre et permettant son édition.
A l'exception du composant correspondant au suréchantillonnage, tous les composants contenant les paramètres sont des champs textuels de type
TextField.
Etant donné que la valeur de chacun des paramètres est un entier, mais que les champs textuels ne peuvent contenir que des chaînes, il faut spécifier comment la transformation de l'un à l'autre peut se faire. Pour les champs textuels, cela est fait en attachant au champ un formatteur textuel basé sur un convertisseur de chaîne. L'extrait de code ci-dessous illustre cela :
TextField field = new TextField(); StringConverter<Integer> stringConverter = …; TextFormatter<Integer> formatter = new TextFormatter<>(stringConverter); field.setTextFormatter(formatter);
Le formatteur attaché à un champ textuel (
formatter ci-dessus) possède une propriété nommée
value et contenant la valeur du champ avant sa conversion en chaîne. Ici, cette propriété contient donc un entier et peut être directement liée, de manière bidirectionnelle, au paramètre correspondant du bean des paramètres. Cela signifie que les changements dans le formatteur sont transmis au bean, et vice versa.
Les champs textuels sont configurés de manière à aligner leur contenu à droite (via
setAlignment) et à avoir un nombre de colonnes préféré (via
setPrefColumnCount) qui dépend du champ et vaut : 7 pour la latitude et longitude de l'observateur, 4 pour l'altitude, la largeur et la hauteur, et 3 pour l'azimut, l'angle de vue et la visibilité.
Le composant permettant de régler le degré de suréchantillonnage est le seul à ne pas être un champ textuel. Il s'agit d'un menu déroulant de type
ChoiceBox proposant les choix 0, 1 et 2. Pour que ceux-ci soit plus faciles à interpréter pour l'utilisateur, un convertisseur de chaîne est également attaché, via la méthode
setConverter, au menu. Il s'agit dans ce cas d'un convertisseur de type
LabeledListStringConverter, écrit à l'étape 9 et convertissant l'entier 0 en la chaîne
non, l'entier 1 en la chaîne
2× et l'entier 2 en la chaîne
4×.
En plus des paramètres eux-même, le panneau des paramètres contient aussi la zone textuelle dans laquelle les informations liées au point sous le pointeur de la souris apparaissent. Cette zone textuelle, de type
TextArea, n'est pas éditable (propriété
editable) et son nombre de lignes préféré (propriété
prefRowCount) vaut 2.
La totalité des nœuds décrits plus haut, et qui constituent les fils du panneau
paramsGrid, sont organisés en grille sur trois lignes de sept colonnes chacune. Chaque label ou composant occupe exactement une case de cette grille, sauf le panneau des informations qui occupe toutes les cases de la dernière colonne. Pour ajouter ces fils au panneau, il est conseillé d'utiliser les méthodes
addRow et
add de la classe
GridPane, qui facilitent la tâche.
2.2 Tests
Pour cette dernière étape, nous vous fournissons à nouveau un fichier de vérification de signature, qui vous permet simplement de vous assurer que la classe principale du programme porte le bon nom. Ce fichier vous est comme d'habitude fourni dans une archive Zip à importer dans votre projet.
De plus, nous vous fournissons une copie d'écran de l'interface finale, panorama inclu. Les paramètres de ce panorama sont ceux indiqués dans le bas de l'image, et sont une variation mineure de ceux du panorama Alpes du Jura.
3 Résumé
Pour cette étape, vous devez :
- écrire la classe
Alpanoen fonction des indications données plus haut,
- tester votre code,
- documenter la totalité des entités publiques que vous avez définies,
- rendre votre projet dans le cadre du rendu final anticipé, si vous désirez tenter d'obtenir un bonus, ou dans le cadre du rendu final normal sinon ; les instructions concernant ces rendus seront publiées ultérieurement.