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