1°/ Description de l'application

Dans ce TP vous allez utiliser les composants graphiques vues au 4° cours pour faire une application avec une interface graphique qui affiche les images contenues dans un répertoire donné. La fenêtre comprendra deux onglets créés par une TabPane.
Le premier onglet est divisé en 2 parties avec une SplitPane. La première partie comprend une ListView contenant les noms des images contenues dans le répertoire et la 2ème partie affiche l'image sélectionnée dans la ListView. 
Le deuxième onglet contient un conteneur pour afficher une image et les boutons : "first", "previous", "play", "next" et "last" pour afficher d'autres images contenues dans le répertoire. Enfin, le deuxième onglet contient une ProgressBar pour afficher la progression du diaporama. 


2°/Implémentation 

L'application doit être conforme au paradigme MVC. Donc, elle se composera de :
  1. une classe Model qui contiendra les données : tableau de chaines de caractères contenant les noms des images, indice de l'image affichée pour chaque onglet, nombre des images, état de l'application, ... et les getters pour tous les attributs
  2. une classe View qui sera la vue contenant les 2 onglets pour afficher les images comme décrit ci-dessus
  3. une classe ControlList pour détecter les évènements de sélection des éléments de la ListView et afficher l'image correspondante à la sélection
  4. une classe ControlButton pour gérer les boutons "first" (afficher la première image), "previous" (afficher l'image précédente), "play" (afficher toutes les 2 secondes une image selon leur ordre dans le répertoire), "next" (afficher l'image suivante) et "last" (afficher la dernière image). La ProgressBar doit être mise à jour avec tout changement d'images
 

3°/Remarques

* Pour lire les images dans un répertoire :

File folder=new File(imagesPath);

int cpt=0;

for (final File fileEntry : folder.listFiles()) {
imageNames[cpt]=fileEntry.getName();
cpt++;
}


* Pour ajouter une image à l'interface et modifier ses dimensions :

ImageView imageView1=new ImageView("file:path_to_image/image_name");
imageView1.setFitWidth(300);
imageView1.setFitHeight(300);

* Pour le bouton "play" vous devez utiliser un timer qui affiche toutes les 2 secondes une nouvelle image et met à jour la ProgressBar. 

                                                                          

Capture décran 2022 04 30 à 12.19.56
Figure 1 : Premier onglet divisé en deux parties avec la SplitPane. La première partie contient une ListView affichant les noms des images et la deuxième partie affiche l'image sélectionnée dans la ListView.

Capture décran 2022 04 30 à 12.24.19                             

Figure 2 : Deuxième onglet affichant une image du répertoire et contenant des boutons pour changer l'image affichée.



4°/Supplément

Ajouter un menu qui permet de modifier le répertoire contenant les images à afficher dans les 2 onglets. Utiliser un DirectoryChooser qui va afficher une petite fenêtre à l'utilisateur pour lui permettre de sélectionner un répertoire dans son système de fichiers. Le modèle et la liste doivent être modifiés en fonction du nouveau répertoire sélectionné.

Capture décran 2022 04 30 à 14.52.52                        
Figure 3 : Sélectionner à l'aide du menu un nouveau répertoire contenant des images

Après avoir présenté quelques composants simples (Button, Label, TextField, ...) de JavaFx dans le 1er cours, ce cours présente d'autres composants plus complexes tel que les ListView, SplitPane, ScrollPane, ... Ce cours explique aussi comment utiliser le concepteur d'interface graphique intégré dans l'IDE  IDEA.


1°/ ListView

Une liste permet d'afficher plusieurs éléments dans une ou plusieurs colonnes. Plusieurs éléments de la liste peuvent être sélectionnés en même temps. Une ListView peut être associée à un ScrollPane si elle contient beaucoup d'éléments.

Capture décran 2022 04 28 à 11.53.06

Figure 1 : ListView contenant 4 éléments et le deuxième élément est sélectionné.


Les RadioButtons, CheckBoxs et ComboBoxs permettent de sélectionner aussi des éléments. Cependant, ils ne sont pas utilisables de la même façon. Un groupe de RadioButtons ne permet que la sélection d'un seul élément dans le groupe. Les éléments sont en général liés. Un ComboBox peut remplacer le groupe de RadioButtons si le groupe contient beaucoup d'éléments dans l'objectif de réduire l'espace fenêtre occupé par les RadioButtons. Les CheckBoxs sont en général indépendants et plusieurs éléments peuvent être sélectionnés en même temps. Enfin La  ListView peut d'une part remplacer les CheckBoxes si leur nombre est conséquent et elle est configurée au mode SelectionMode.MULTIPLE. D'autre part, elle peut être similaire à la ComboBox si elle est configurée au mode SelectionMode.SINGLE mais elle affiche plus d'éléments et prend plus de place dans la fenêtre.

ListView<String> list = new ListView<String>();
ObservableList<String> items =FXCollections.observableArrayList (
"IE", "Firefox", "Chrome", "Edge");
list.setItems(items);
list.setPrefWidth(100);
list.setPrefHeight(75);
list.getSelectionModel().select(1);
list.getSelectionModel().selectedItemProperty().addListener(
new ChangeListener<String>() {
public void changed(ObservableValue<? extends String> ov, String old_val, String new_val) {
System.out.println("old value : "+old_val+", new value : "+new_val);
}
});

 

Pour contrôler la barre défilante d'une liste, il faut mettre la listView dans un ScrollPane et puis configurer la barre défilante du ScrollPane comme dans le code suivant. D'autres composants peuvent être ajoutés à un ScrollPane tels qu'un TextArea, Rectangle, etc.

 

ScrollPane scrollPane= new ScrollPane();
scrollPane.setContent(list);
scrollPane.hbarPolicyProperty().setValue(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.vbarPolicyProperty().setValue(ScrollPane.ScrollBarPolicy.AS_NEEDED);


2°/SplitPane 

Une SplitPane permet de diviser le contenu de la fenêtre horizontalement ou verticalement en deux parties. 


Capture décran 2022 04 28 à 17.42.03
Figure 2 : Une SplitPane qui divise la fenêtre en deux parties. La partie gauche contient une ListView et la partie droite contient une TextArea.
 
//créer une ListView et une TextArea et les ajouter au SplitPane
ListView<String> list = new ListView<String>();
ObservableList<String> items =FXCollections.observableArrayList (
"IE", "Firefox", "Chrome", "Edge");
list.setItems(items);

TextArea textArea=new TextArea(); textArea.setFont(Font.font("serif", FontWeight.BOLD, FontPosture.ITALIC,13));
SplitPane splitPane = new SplitPane(); splitPane.setDividerPosition(0, 0.3); splitPane.getItems().addAll(list, textArea);
 

3°/TabPane 

La TabPane crée un conteneur avec plusieurs onglets. Chaque onglet peut contenir différents composants graphiques.

Capture décran 2022 04 29 à 11.02.49
         Figure 3 : une fenêtre contenant une TabPane avec quatre onglets.

 

TabPane tabPane = new TabPane();
Tab tabStark = new Tab();
ImageView imageViewStark= new ImageView("images/stark.png");
imageViewStark.setFitHeight(25);
imageViewStark.setFitWidth(25);
tabStark.setGraphic(imageViewStark);
tabStark.setText("Stark");
tabStark.setTooltip(new Tooltip("Tab for the house Stark"));
tabStark.setContent(new StackPane(new Label("Stark")));

Tab tabBara = new Tab();
ImageView imageViewBara= new ImageView("images/baratheon.gif");
imageViewBara.setFitHeight(25);
imageViewBara.setFitWidth(25);
tabBara.setGraphic(imageViewBara);
tabBara.setText("Baratheon");
tabBara.setTooltip(new Tooltip("Tab for the house Baratheon"));
tabBara.setContent(new StackPane(new Label("Baratheon")));

Tab tabLannister = new Tab();
ImageView imageViewLannister= new ImageView("images/lannister.gif");
imageViewLannister.setFitHeight(25);
imageViewLannister.setFitWidth(25);
tabLannister.setGraphic(imageViewLannister);
tabLannister.setText("Lannister");
tabLannister.setTooltip(new Tooltip("Tab for the house Lannister"));
tabLannister.setContent(new StackPane(new Label("Lannister")));

Tab tabMartell = new Tab();
ImageView imageViewMartell= new ImageView("images/martell.png");
imageViewMartell.setFitHeight(25);
imageViewMartell.setFitWidth(25);
tabMartell.setGraphic(imageViewMartell);
tabMartell.setText("Martell");
tabMartell.setTooltip(new Tooltip("Tab for the house Martell"));
tabMartell.setContent(new StackPane(new Label("Martell")));


tabPane.getTabs().addAll(tabStark,tabBara,tabLannister,tabMartell);
Scene scene=new Scene(tabPane, 400, 300);


4°/Timer et ProgressBar

Un Timer permet d'exécuter une seule fois un code après N millisecondes ou plusieurs fois le même code tous les N millisecondes. On peut l'utiliser pour modifier l'interface après un délai donné ou bien pour mettre à jour régulièrement l'interface. 
Une ProgressBar permet d'afficher la progression d'une tâche en temps réel.                               

Capture décran 2022 04 29 à 15.57.53
Figure 4 : Une ProgressBar et un ProgressIndicator qui sont mis à jour chaque seconde par un Timer

ProgressBar progressBar= new ProgressBar(0);
ProgressIndicator pi = new ProgressIndicator(0);

FlowPane fp =new FlowPane();
fp.setAlignment(Pos.CENTER);
fp.getChildren().addAll(new Label("Progress : "), progressBar, pi);

Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
progressBar.setProgress(progressBar.getProgress()+0.1);
pi.setProgress(pi.getProgress()+0.1);
if(progressBar.getProgress()==1.0) {
timer.cancel();
}
}
}, 100,100);

 

5°/JavaFX FXML : 

FXML est un langage basé sur XML qui permet de décrire la structure d'une interface graphique en JavaFX. Il est similaire à HTML qui permet de décrire la structure d'une page web. Le fichier contenant la description FXML a une extension .fxml et remplace en partie la classe Vue du modèle MVC. Quand vous créez un nouveau fichier FXML avec IDEA il contiendra le code suivant :

<?xml version="1.0" encoding="UTF-8"?>

<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>

<AnchorPane xmlns="http://javafx.com/javafx"
            xmlns:fx="http://javafx.com/fxml"
            fx:controller="Controller"
            prefHeight="400.0" prefWidth="600.0">

</AnchorPane>

La première ligne est l'entête d'un fichier XML. Ensuite il y a cinq lignes d'import qui fonctionnent d'une manière similaire aux imports dans un fichier java. Après les imports, il faut mettre le contenu de l'interface. Par défaut, IDEA ajoute seulement un conteneur (AnchorPane) vide. Vous pouvez constater que comme en HTML les balises peuvent avoir des attributs. Par exemple, l'AnchorPane a les attributs prefHeight et prefWidth qui permettent de préciser les dimensions souhaitées pour ce conteneur. L'attribut fx:controller permet de spécifier la classe java qui décrit le contrôleur de cette interface. 

 

Ci-dessous un exemple simple où un Label, un TextField et un bouton ont été ajoutés. L'AnchorPane a été remplacé par un VBox et les valeurs de quelques attributs ont été modifiées pour mettre en forme l'interface.

<VBox xmlns="http://javafx.com/javafx"
            xmlns:fx="http://javafx.com/fxml"
            fx:controller="Controller"
            prefHeight="100.0" prefWidth="200.0" alignment="CENTER" spacing="10">
    <Label text="Saisissez votre login : "/>
    <TextField/>
    <Button text="Envoyer"/>
</VBox>

 

Pour générer l'interface décrite dans le code ci-dessous, il faut le charger avec le code suivant :

public class GraphicEditor extends Application {
    public static void main(String[] args) {
        launch(args);
    }
    @Override
    public void start(Stage stage) throws IOException {
        stage.setTitle("Login");
        VBox layout = FXMLLoader.load(getClass().getResource("text.fxml"));
        stage.setScene(new Scene(layout));
        stage.show();
    }
}

 

Donc dans la méthode start() de la sous-classe d'Application, le fichier FXML est chargé avec la méthode FXMLLoader.load(). Cette dernière retourne le conteneur qui contient les autres composants de l'interface. Ce conteneur est associé à la scène qui est affichée dans la fenêtre de l'application. On obtient l'interface suivante :

 

Capture décran 2022 04 30 à 00.09.24

 

Pour pouvoir accéder aux composants graphiques de l'interface dans le contrôleur, il faut leur donner des identifiants avec l'attribut fx:id. On peut aussi préciser quelle méthode du contrôleur gère les évènements d'un type donné et générés par un composant graphique. Par exemple pour les évènements ActionEvent, il faut utiliser l'attribut onAction et on lui affecte le nom de la méthode précédée par un #.  Ci-dessous le code FXML modifié :

<VBox xmlns="http://javafx.com/javafx"
            xmlns:fx="http://javafx.com/fxml"
            fx:controller="Controller"
            prefHeight="100.0" prefWidth="200.0" alignment="CENTER" spacing="10">
    <Label text="Saisissez votre login : "/>
    <TextField fx:id="tf"/>
    <Button text="Envoyer" onAction="#afficher"/>
</VBox>

 

Dans la classe Controller, il faut déclarer la variable de type TextField et ayant le nom tf pour pouvoir accéder au TextField qui a l'id égal à tf. Il faut aussi définir la méthode afficher() qui sera appelée quand le bouton génère l'évènement ActionEvent. Le code suivant présente la Classe Controller :

public class Controller {
    @FXML
    TextField tf;

    public void afficher(ActionEvent actionEvent) {
        System.out.println(tf.getText());
    }
}

 

Dans la classe Controller, il est possible de définir une méthode initialize() qui sera exécutée juste après le chargement du fichier FXML. Il faut que la classe Controller implémente l'interface Initializable. Cette méthode permet d'initialiser l'interface graphique des données du modèle.

 

6°/JavaFx Scene Builder :

Au lieu d'écrire le fichier FXML, il est possible d'utiliser le Scene Builder, qui permet de créer l'interface en glissant et déposant les composants sur la fenêtre. Scene Builder génère le fichier FXML qui correspond à l'interface créée. Pour lancer le Scene Builder, il faut faire un click droit sur le fichier FXML et sélectionner open in Scene Builder. À noter que dans IDEA, il est aussi possible d'utiliser Scene Builder mais cette version ne comprend pas toutes les fonctionnalités. Ci-dessous une capture d'écran du Scene Builder.

Capture décran 2022 04 30 à 00.48.33

À gauche, Il y a les différents conteneurs et composants graphiques à glisser et déposer dans la fenêtre. Sous la palette des composants, Il y a l'ensemble des composants déjà ajoutés à la fenêtre. Dans cet exemple, un VBox contenant un Label, un TextField et un Button. Le TextField est sélectionné et ces propriétés sont affichées à droite de la fenêtre.

7°/Internationaliser votre application :


Pour internationaliser votre application, c'est à dire l'implémenter en plusieurs langues sans devoir re-développer l'interface pour chaque langue, il faudrait effectuer les étapes suivantes :

  1. Dans le Scene Builder, remplacer le texte de chaque composant graphique (Label, Button, etc.) par une clé unique. Par exemple, pour internationaliser le texte du Label "Saisissez votre login", il faut le sélectionner dans le Scene Builder et modifier sa propriété "text" en sélectionnant "Replace with internationalized String". Le texte du label sera remplacé par "%key.unspecified". Il va falloir remplacer cela par une clé unique que vous choisirez, par exemple "lHeader.value". Capture décran 2022 04 30 à 07.48.30
  2. Créer un fichier "nom"_"langue"_"pays".properties pour chaque langue que vous voulez supporter. Pour la langue par défaut, le fichier sera nommé "nom".properties, pour le français en France "nom"_fr_"FR".properties, pour l'anglais au Royaume Uni "nom"_en_UK.properties... Chaque fichier contiendra des pairs "clé"="valeur". Pour chaque chaine de caractère dans l'interface graphique, il y aura sa traduction dans la langue du fichier. 
                                          
Fichier Test.properties contiendra les valeurs par défaut en Français.
lHeader.value = Saisir votre login
bSend.value = Envoyer

 

Fichier Test_en_UK.properties

lHeader.value = Insert your login
bSend.value = Send

 

Pour visualiser l'interface dans Scene Builder avec une langue donnée, sélectionner dans le menu du Scene Builder "Preview -> Internationalization -> set Resource..." et choisir le fichier properties qui correspond à la langue voulue. Enfin il faut visualiser l'interface en sélectionnant le menu "Preview -> Show Preview in Window".

Pour exécuter l'application avec une langue spécifique, il faut passer la langue et le pays au chargeur du fichier FXML :

Locale locale = new Locale("en", "UK");
ResourceBundle bundle = ResourceBundle.getBundle("Text", locale);
VBox layout = FXMLLoader.load(getClass().getClassLoader().getResource("text.fxml"), bundle);

 

La langue et le pays peuvent être passés par paramètre à la VM avec :

-Duser.language="en" -Duser.country="UK"

Puis le chargeur prend la Locale courante :

Locale currentLocale = Locale.getDefault();
ResourceBundle bundle = ResourceBundle.getBundle("Text",currentLocale);
VBox layout = FXMLLoader.load(getClass().getClassLoader().getResource("text.fxml"), bundle);


Enfin, le ResourceBundle est accessible depuis la méthode initialize du contrôleur. Dans cette méthode, il est possible d'initialiser les composants complexes avec le texte dans la bonne langue.

@FXML
ComboBox cb;
    
@Override
public void initialize(URL url, ResourceBundle resourceBundle) {
        cb.setItems(FXCollections.observableArrayList(resourceBundle.getString("option1"), resourceBundle.getString("option2"), resourceBundle.getString("option3")));
}

 

 

 

 

1-Principes

Dans le cadre du développement d'une application graphique, le principe du paradigme Modèle/Vue/Contrôleur (=MVC) est de séparer les données, leur manipulation et leur visualisation dans des classes différentes. Pour caricaturer :
  • le modèle représente les données que l'application utilise. Il ne connaît ni la vue, ni le contrôleur.
  • la vue est la représentation graphique de ces données à l'écran. Elle "écoute" les modifications du modèle pour se modifier elle-même. Elle ne connaît donc que le modèle.
  • le contrôleur est la partie qui va lire/modifier le modèle en fonction des interactions de l'utilisateur avec la vue. Il n'a donc besoin de connaître que le modèle.
Ainsi, le modèle n'a pas à connaître la façon dont il est représenté à l'écran. On peut même le représenter facilement avec différente vue. Par exemple, un tableau à deux dimensions peut être visualisé sous forme de tables, camemberts, histogrammes, ... sans pour autant que sa structure soit adaptée à chaque cas.

L'objectif du paradigme MVC est de pouvoir développer les trois parties en parallèle. Pour cela, avant tout codage, il  est nécessaire de :
  • définir l'ensemble des méthodes permettant de lire/modifier les données du modèle,
  • définir l'ensemble des composants graphiques permettant d'afficher le modèle à l'écran et d'interagir avec l'utilisateur,
  • définir l'ensemble des méthodes qui permettent de modifier l'état de la vue.
A noter que le troisième ensemble peut être vide mais généralement, il contient des méthodes qui permettent d'invalider tel ou tel composant en fonction du traitement que l'on fait, de (de)sélectionner des cases, de masquer/afficher une fenêtre, ... Cet ensemble dépend donc fortement des besoins de l'application et de ce fait, est parfois difficile à définir.

2-En Java


Malheureusement, il est très difficile d'utiliser de manière stricte ce paradigme en Java. Par exemple, une vue peut utiliser plusieurs composants graphiques générant le même type d'évènements. En mode strict,  cela impose de créer autant de contrôleur que de composants, donc énormément de classes. Autre problème, si une partie de la vue doit être mise à jour en fonction de modifications dans le modèle (par exemple, une case à cocher qui devient invalide), c'est à la vue de le "détecter" puisque ni le modèle, ni le contrôleur n'ont accès à la vue. Cette "détection" est complexe à mettre en place puisqu'il faut que le modèle signale qu'une donnée a été modifiée et que la vue soit à l'écoute de ce signal.

En conclusion, il convient d'utiliser un paradigme moins strict en permettant au contrôleur d'accéder à la vue. Il pourra ainsi :
  • identifier la source d'évènements et donc gérer plusieurs composants émettant ce type d'évènement,
  • appeler directement des méthodes de la vue, afin de modifier celle-ci lorsque les traitements l'imposent.

Dans ces conditions, le nombre de classes reste raisonnable. Généralement, cela donne :
  • une ou plusieurs classes représentant la partie Modèle,
  • une classe de Vue par classe de Modèle plus une classe de Vue représentant la fenêtre principale,
  • une classe Contrôleur par Vue et par type d'évènement à capturer.
Remarques :
  • Dans le cas d'applications simples, on a tendance à n'utiliser qu'une seule classe Modèle pour toute l'application et donc une seule classe de Vue qui représente la fenêtre principale.
  • Une vue peut générer plusieurs type d'évènements. Dans ce cas, pour éviter d'obtenir un "mega-contrôleur" illisible, il est souhaitable de créer une classe Contrôleur par type d'évènement, voire plusieurs quand on veut séparer les traitements de différents types de composants. Par exemple, en Java, les boutons ainsi que les items de menu émettent des évènements ActionEvent. On peut donc parfois séparer le traitement de ces signaux dans deux classes Contrôleur.
  • Chaque interaction de l'utilisateur avec la vue ne nécessite pas forcément un traitement dans un contrôleur. Par exemple, il n'est pas forcément important de définir une méthode pour traiter le clic sur une case à cocher puisque le contrôleur a accès à la vue, il est sans doute plus intéressant de simplement lire son état lorsqu'on en a besoin.
  • Certaines interactions ont des conséquences uniquement sur le modèle, d'autres uniquement sur la vue et d'autres enfin sur les deux.

2.1°/ Implémentation

Pour créer une classe de Modèle, il faut :
  • définir comme attributs les données du modèle, généralement en protected au cas où l'on veuille étendre le Modèle par héritage,
  • définir des méthodes pour lire et modifier ces attributs,
  • éventuellement définir de nouvelles classes d'exceptions qui seront émises lorsque les modifications du modèle sont invalides.
  • définir les méthodes nécessaires au coeur de l'application
Pour créer une classe de Vue, il faut :
  • hériter de Stage ou Scene, le premier cas quand on définit la vue de la fenêtre principale,
  • définir un constructeur qui prend en paramètre un (ou plusieurs) modèle à visualiser dans la vue,
  • créer les composant de la vue dans le constructeur (ou une méthode annexe), notamment en utilisant les données du modèle,
  • créer une (ou plusieurs) méthode(s) qui permet d'associer un contrôleur aux composants graphiques dont les évènements sont gérés par celui-ci,
  • créer les méthodes de mise à jour de la vue.
Pour créer une classe de Contrôleur, il faut :
  • implémenter une (ou plusieurs) interface comme EventHandler<> ou ChangeListener<>,
  • définir un constructeur qui prend en paramètre un (ou plusieurs) modèle et qui crée la (ou les) vue pour ce(s) modèle(s).
  • définir les méthodes traitant les évènements en provenance de la vue et qui vont éventuellement modifier le modèle et la vue.
2.2°/ Problèmes et solutions classiques

Comme dit précédemment, une vue génère souvent différents type d'évènements. Dès lors, il existe deux solutions pour contrôler la vue :
  • soit le contrôleur implémente toutes les interfaces permettant de gérer ces évènements,
  • soit on crée plusieurs contrôleurs.
La première solution est simple à mettre en oeuvre mais peut conduire à un code du contrôleur volumineux et peu lisible. La deuxième solution pose un problème particulier : d'après la section précédente c'est le contrôleur qui est censé créer la vue. Comme il y a plusieurs contrôleurs, comment faire ? Deux solutions sont envisageables :
  • inverser l'ordre de création, à savoir c'est la vue qui crée les contrôleurs. C'est la solution la plus simple en terme de mise en oeuvre mais elle "brise" un peu plus le paradigme MVC puisque dans ce cas, la vue "connaît" les contrôleurs.
  • regrouper les contrôleurs dans une même classe, qui va créer la vue puis chaque contrôleur, en donnant une référence de la vue à ces derniers.

Bien souvent, une modification donnée de la vue est le résultat du traitement d'évènements différents. Quand ces évènements sont traités dans des contrôleurs différents, cela implique que ces derniers aient des morceaux de code redondants. Dans ce cas, il convient de créer une hiérarchie de classes de contrôleurs afin de mutualiser ces parties de code.

3°/ Exemple

On veut créer une application qui fait la somme ou la soustraction de deux nombres. Le total s'affiche immédiatement, et s'actualise dès qu'un des nombres est modifié. Le résultat visible est le suivant :
 
Capture décran 2022 04 01 à 15.12.12

La solution donnée ci-dessous se base sur une organisation MVC où il y a plusieurs contrôleurs qui ont accès à la vue. 

Pour créer cette interface, on a besoin :
  • d'une classe Model qui contient les données, à savoir les deux nombres. Le total étant calculé, il n'est pas nécessaire de le stocker.
  • d'une classe View qui représente la fenêtre principale. Cette classe n'a accès qu'à une instance de Model.
  • d'une classe ControlText qui implémente ChangeListener<String> afin de traiter chaque modification des champs de saisie.
  • d'une classe ControlButton qui implémente EventHandler<ActionEvent> afin de traiter le changement d'opération et l'activation du bouton "clear".
  • d'une classe Appli qui crée le modèle, puis la vue et enfin les deux contrôleurs.
Pour télécharger les fichiers, cliquez sur le liens suivants :
Remarques :
  • La classe Appli affiche la vue uniquement après la création des deux contrôleurs. En effet, il ne faut pas que l'application puisse générer des évènements avant que les contrôleurs soient reliés aux composants de l'interface.
  • Ce lien est créé par les contrôleurs eux-mêmes, dans leur constructeurs, grâce aux méthodes setButtonControler() et setTextControler() de la classe View. Ainsi, cette dernière n'a pas besoin d'accéder aux contrôleurs.

1°/ Objectifs

Ce TP reprend les même objectifs que le TP précédent, excepté le fait que la structuration du code doit suivre le paradigme MVC. Pour cela, notes et coefficients ainsi que les méthodes pour les manipuler sont stockés dans un modèle, représenté par la classe Model que vous devez télécharger. A votre charge de modifier les classes View et ControlBouton afin de rendre l'application conforme au paradigme MVC.

2°/ La classe Model

Téléchargez le fichier contenant la classe ainsi que les exceptions associées : [ Model.java ]

Cette classe prend comme attributs les notes et les coefficients manipulés par l'application et propose des méthodes pour lire/modifier ces valeurs. Lors de modifications, des test sont faits et génèrent éventuellement des exceptions du type NoteOutOfBound, CoeffOutOfBound, NumOptionOutOfBound et SansOptionException. Toutes ces exceptions sont définies par des sous classes d'Exception.
Comme la classe ControlBouton va manipuler le modèle, il faudra prendre garde à traiter correctement ces exceptions dans la méthode handle().

3°/ Le main()

Le fichier Appli.java doit contenir le code suivant :
public class Appli extends Application{

public static void main(String[] args) {
launch(args);
}

@Override
public void start(Stage primaryStage) throws Exception {
Model model = new Model();
View view=new View(model);
ControlBouton controlBouton = new ControlBouton(model, view);
ControlMenu controlMenu = new ControlMenu(view);
view.display();
}
}

Remarque : la classe ControlMenu n'utilise pas le modèle mais uniquement la vue.

5°/ La classe View

Modifiez la classe View pour que :
  • son constructeur prenne une instance de Model en paramètre,
  • les champs de saisie soient initialisés avec les valeurs présentes dans le modèle,
  • deux méthodes, setControlBouton() et setControlMenu(), permettent d'associer les deux contrôleurs aux composants graphiques voulus (cf. exemple du cours pour définir ces méthodes).
6°/ Les classes contrôleur

Normalement, la classe ControlMenu ne change pas, à part l'appel à setControlMenu() à ajouter dans le constructeur.
La classe ControlBouton change beaucoup plus puisque chaque note et coefficient tiré de la vue doit être transcrit dans le modèle. De plus, comme le modèle propose une méthode pour calculer la moyenne, ce calcul n'est plus à faire directement dans ControlBouton.
1°/ Objectifs

L'objectif de ce TP est de reprendre l'application du TP n°1 et d'y ajouter le code qui gère l'appui sur le bouton "Moyenne". Dans un deuxième temps, nous allons modifier le code de la fenêtre principale, et en particulier, nous allons lui ajouter un menu permettant à l'utilisateur de basculer entre les interfaces graphiques versions 1 et 2.


2°/ Action du bouton Moyenne

Afin de gérer l'action du bouton Moyenne, trois ajouts doivent être faits :

  1. Créer une classe ControlButton, dont le rôle est d'écouter le bouton "Moyenne", afin de déclencher le calcul de la moyenne lorsque l'utilisateur clique sur ce bouton.
  2. Créer une méthode addListeners() qui sera appelée après l'initialisation des attributs et qui associera une instance de ControlButton au bouton moyenne pour capturer les évènements ActionEvent qu'il génère.
La classe ControlBouton va contenir le code associé au calcul de la moyenne. En résumé, ce calcul consiste à récupérer les notes et coefficients entrés par l'utilisateur, puis à calculer la moyenne. Par conséquent, la classe ControlBouton a besoin d'avoir accès à la classe Main pour pouvoir lire l'état de certains composants graphiques (qui sont des attributs de Main). 
 

3°/ Contenu de la classe ControlBouton

Lorsque l'on clique sur un Button, un évènement de type ActionEvent est généré. Donc, pour écouter un tel évènement, nous avons besoin d'un objet qui implémente l'interface EventHandler<ActionEvent>. De ce fait, cette classe doit contenir non seulement un constructeur (qui prend en paramètre un objet de la classe Main) mais également définir la méthode handle(ActionEvent e). Cette méthode sera automatiquement appelée lorsque un évènement de type ActionEvent est généré par le bouton écouté. C'est donc cette méthode qui contient le code associé au calcul de la moyenne.


Dans un premier temps, nous allons nous assurer que l'évènement clic est bien récupéré par l'écouteur du bouton. Pour ce faire, ajouter uniquement :

System.out.println( "clic sur le bouton'' );

dans le corps de la méthode handle(). Si cela fonctionne, effacer cette ligne et remplissez la méthodes en suivant les étapes indiquées ci-dessous :

  1. on récupère les notes (Anglais, Math, Info, Géo et éventuellement Option) sous forme de String dans les TextField,
  2. s'il manque une note non optionnelle, on affiche un message d'erreur dans une fenêtre de dialogue et on arrête le traitement,
  3. sinon on convertit les notes en double,
  4. si un de ces notes est inférieure à 0 ou supérieure à 20, on affiche un message d'erreur dans une fenêtre de dialogue et on arrête le traitement,
  5. on vérifie si la case à cocher "Prendre en compte les coefficients est cochée". Si non, mettre tous les coefficient à 1 et aller directement en 8.
  6. on récupère les coefficients de chaque matière, sous forme de double, en vérifiant quel bouton est coché,
  7. on vérifie quelle option est prise et on en déduit un coefficient : 2 pour latin et grec, 1 pour sport.
  8. on calcule la somme des coefficients en prenant garde de ne pas ajouter celui de l'option si la note d'option n'existe pas.
  9. on calcule la moyenne générale en faisant la somme de chaque note (sauf éventuellement celle d'option) multipliée par son coefficient et en divisant le total par la somme des coefficients calculée en 7.
  10. on change le label "Valeur" pour afficher la moyenne.
Attention, il y a des exceptions qui peuvent être générées lors de ces calculs, notamment lorsque vous convertissez des String en double. Il faudra donc capturer toutes ces exceptions. Chaque capture devra générer une fenêtre affichant un message d'erreur explicite.

 

Pour gérer l'affichage des messages d'erreur, écrivez une méthode creerDialogErr(String messageErr) qui prend en paramètre le message d'erreur à afficher. Cette méthode crée un Alert  (cf. exemple du cours n°1) afin de permettre le blocage de l'utilisation de la fenêtre principale tant que la fenêtre contenant le message d'erreur est ouverte. A noter que creerDialogErr() doit se situer dans la classe Main pour fonctionner correctement.



4°/ Modification de la fenêtre principale

Les premières modifications concernent l'apparence de l'interface. Vous devez faire les modifications suivantes :

  • ajouter un icone (téléchargez le fichier icone-fenetre.gif) et une position initiale,
  • rendre la fenêtre non redimensionable.

La figure ci-dessous représente la fenêtre que l'on souhaite obtenir.


menu

Vous devez ensuite créer un menu, composé de trois items. Les deux premiers items, nommés "Version 1" et "Version 2", permettent de changer dynamiquement l'interface graphique de l'application : on peut choisir entre les versions 1 et 2 implémentées dans le TP n°1.  Le troisième item est un sous-menu qui se nomme "Aide" et qui s'ouvre lorsque l'on passe la souris dessus. Ce sous-menu comporte deux sous-items nommés "Comment ça marche ?" et "A propos". Vous devez implémenter graphiquement le menu dans sa totalité, mais gérer uniquement les évènements des deux premiers items. Le rendu est représenté sur la figure ci-dessus.

Pour obtenir ce résultat :
  • ajouter les attributs version1 et version2 de type MenuItem dans la classe Main. Ajoutez aussi l'attribut menuBar de type MenuBar.
  • Créer le menu dans la méthode createMenu en instanciant les Menu, MenuItem et MenuBar. Vous associerez les icônes icone-menu-version1.gif et icone-menu-version2.gif respectivement à version1 et version2.
  • Ajouter le menuBar à la fenêtre dans les méthodes addWidgetsToSceneV1() et addWidgetsToSceneV2() (cf. cours n°1).
Pour gérer les clics sur les items version1 et version2 , nous avons besoin de gérer des évènements du type ActionEvent. Quand bien même il y a deux items, nous n'allons utiliser qu'un seul listener. Pour cela :
  • créer une classe ControlMenu qui implémente l'interface EventHandler<ActionEvent> et définit la méthode handle(ActionEvent)  qui génère juste un affichage sur le terminal. Le contrôleur doit avoir accès aux composants de la classe Main pour pouvoir les modifier.
  • associer une instance de la classe ControlMenu aux deux MenuItem dans la classe Main. On utilise donc le même  listener pour deux objets distincts.
  • tester que l'affichage sur le terminal a bien lieu lorsque l'on clique sur les items du menu.
Il nous reste maintenant à modifier la méthode handle(). Il faut savoir quel est l'item sur lequel l'utilisateur a cliqué, afin de choisir si l'on doit passer à la version 1 ou à la version 2 de l'interface graphique.  Pour ce faire, utiliser la méthode getSource() de la classe ActionEvent. Ensuite, il faut effectivement appliquer le changement d'interface. Pour cela :
  • créer une méthode changerVersion(int version) dans la classe Main qui prend en argument le numéro de la version voulue (1 ou 2).
  • cette méthode :
    • supprime tout le contenu de la scène,
    • en fonction de la valeur X de version, appeler addWidgetsToSceneVX() définie dans le TP n°1,
    • appel primaryStage.setScene() 
Entrer des notes, calculer une moyenne, puis changer de version de l'interface. Que constatez-vous ? Pourquoi obtient-on ce phénomène ?
 
5°/ Gestion des évènements avec des classes internes
 
Refaire le TP 2, en utilisant les classes internes anonymes à la place des classes externes ControlBouton et ControlMenu.
 
6°/ Utiliser les filters et les handlers
 
Pour le calcul de la moyenne, remplacez la convenience method "setOnAction()" par un filtre "addEventFilter()" qui vérifie les données saisies. Puis ajoutez un gestionnaire d'évènement au bouton moyenne "addEventHandler()" pour calculer la moyenne. Cette section démontre que les filtres capturent les évènements avant les gestionnaires d'évènement.