Tutoriel sur l'API Cell de JavaFX

Comment customiser les contrôles utilisant l'API Cell tels que ComboBox, ListView, TableView, TreeView et TreeTableView.

Fabrice Bouyé

Cet article vous montrera les bases de l'utilisation de l'API Cell de JavaFX. Cette API constitue la base des contrôles dits « virtualisés » tels que ComboBox, ListView, TableView, TreeView et TreeTableView.

Pour réagir au contenu de cet article, un espace de dialogue vous est proposé sur le forum Commentez

- Titre : Tutoriel sur l'API Cell de JavaFX

- Auteur : Fabrice Bouyé

- Parution : 8 décembre 2014

- Mise à jour : 23 novembre 2015

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2014 Fabrice Bouyé. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- II. Rappel : Swing

- III. JavaFX

- III-A. Concepts de l'API Cell

- III-B. Mise en œuvre

- III-C. Contrôles virtualisés

- III-D. ChoiceBox<V>

- III-E. DatePicker

- IV. L'API Cell

- IV-A. Classes

- IV-B. Propriétés

- IV-C. null vs. empty ?

- IV-D. À propos des exemples suivants

- V. Listes

- V-A. ListView<V>

- V-A-1. Propriétés

- V-A-2. En pratique

- V-A-2-a. Bases

- V-A-2-b. Mise en page riche

- V-A-2-c. Utilisation interactive

- V-B. ComboBox<V>

- V-B-1. Propriétés

- V-B-2. En pratique

- V-B-3. Affichage riche

- V-C. TableView<V>

- V-C-1. Propriétés

- V-C-2. cellValueFactory

- V-C-2-a. Propriétés JavaFX

- V-C-2-b. Propriétés JavaBeans observables

- V-C-2-c. Autres

- V-C-3. En pratique

- V-C-3-a. Booléens

- V-C-3-b. Édition

- V-C-3-c. Édition avancée

- V-C-3-d. Lignes

- VI. Arbres

- VI-A. Stockage des données

- VI-A-1. Propriétés

- VI-A-2. Arbre dynamique

- VI-B. TreeView<V>

- VI-B-1. Propriétés

- VI-B-2. Apparence de la flèche

- VI-B-3. En pratique

- VI-C. TreeTableView<V>

- VI-C-1. Propriétés

- VI-C-2. En pratique

- VI-C-2-a. Booléens

- VI-C-2-b. Icônes

- VI-C-2-c. Date

- VI-C-2-d. Taille

- VII. Conclusion

- VIII. Liens

- IX. Remerciements

- X. Annexe

- X-A. Code source et exemples

I - Introduction

Certains contrôles sont amenés à représenter à l'écran des ensembles d'objets, sous forme de liste, de boite déroulante, de table ou encore sous la forme d'un arbre graphique.

Si on se contente des apparences par défaut, les contrôles existants sont, en l'état, tout à fait capables d'afficher de simples lignes de texte contenues dans des instances de String. Cependant, les choses se corsent lorsqu'on désire afficher des objets plus complexes dont la méthode toString() ne retourne pas forcément ce qu'on désire afficher à l'utilisateur. Ici, nous avons créé des instances d'une classe Car que nous avons construite de toute pièce et nous les affichons dans une ComboBox. Comme nous n'avons pas redéfini la méthode toString() dans cette classe, l'affichage n'est… pas très sexy dirons-nous…

Figure 1 - Une ComboBox ayant un affichage très quelconque.

De plus, dans une application moderne, RIA (Rich Internet Application), ou encore mobile, on ne se contentera plus guère d'utiliser seulement du texte : ainsi, une liste de contacts dans une application de messagerie peut être amenée à afficher le portrait du dit contact ou encore changer la police ou la couleur du contact en fonction de son statut en ligne. De même, une table qui liste des produits dans un magasin peut afficher une image ou photo de ce produit, un hyperlien vers le site du fabricant ou encore une barre ou des étoiles montrant sa popularité auprès des clients.

C'est ici que l'API Cell (de l'anglais « cell » qui signifie « cellule ») entre en jeu : elle permet de rapidement customiser l'apparence d'une donnée dans un contrôle pour afficher la bonne valeur, le rendre plus attrayant ou encore le rendre interactif.

II - Rappel : Swing

Petit retour en arrière pour les aficionados de Swing. Swing utilise un concept assez proche de celui des cellules : un renderer ou encore tampon encreur. Un unique cellRenderer est créé et utilisé pour représenter chaque élément de la liste : à tour de rôle, il va endosser un élément et changer son apparence pour le représenter. Il s'agit de l'application du patron du poids-mouche. Cependant, l'implémentation dans Swing a un très gros défaut : elle n'est pas performante quand on l'utilise sur des conteneurs ayant de nombreux éléments. En effet le renderer est appelé sur l'intégralité des éléments !

Dans l'exemple suivant, nous plaçons 3000 éléments dans une JList (liste graphique), elle-même placée dans un JScrollPane (zone d'affichage restreint avec barre de défilement) :

1. final DefaultListModel model = new DefaultListModel();

2. for (int index = 0 ; index < 3000 ; index++) {

3. model.addElement(index);

4. }

5. final JList list = new JList();

6. list.setModel(model);

7. list.setCellRenderer(new DefaultListCellRenderer() {

8. private int called = 0;

10. @Override

11. public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {

12. called ++;

13. System.out.println(called);

14. return super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);

15. }

16. );

17. final JScrollPane listScroll = new JScrollPane(list);

18. final JFrame frame = new JFrame();

19. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

20. frame.setLayout(new BorderLayout());

21. frame.add(listScroll, BorderLayout.CENTER);

22. frame.setPreferredSize(new Dimension(500, 500));

23. frame.pack();

24. frame.setVisible(true);

Cela nous donne le résultat suivant à l'écran :

Figure 2 - Une bonne vieille JList d'antan.

Une fois la liste affichée, un petit tour par la console va nous dévoiler une information étonnante : la méthode getListCellRendererComponent() de notre renderer, celle qui permet de customiser le tampon pour un élément à un indice donné de la liste, a été appelée plus de 3000 fois ! Pourtant seuls les 26 premiers éléments de la liste sont visibles (on aperçoit un bout de la valeur 25).

En effet, la JList a besoin de calculer sa taille maximale de manière à permettre au JScrollPane d'afficher correctement ses barres de défilement. Le renderer est donc configuré successivement pour chacun des 3000 éléments de la liste pour déterminer cette dimension. Et ce n'est pas fini ! En effet, à chaque redimensionnement de la fenêtre, cette méthode peut être appelée plus de 1000 fois ! Cette méthode est également appelée de très nombreuses fois à chaque défilement du contenu de la liste…

Ce n'est pas très efficace ! Imaginez ce qu'il en serait avec un affichage beaucoup plus complexe dans la cellule ou avec un nombre beaucoup plus grand d'éléments qui nous seraient retournés, par exemple, par une requête sur une base de données ou un SIG.

III - JavaFX

Dans JavaFX, les composants qui sont destinés à afficher un grand nombre de valeurs ont été « virtualisés », c'est-à-dire qu'ils affichent une vue qui ne contient que les éléments qui sont visibles.

III-A - Concepts de l'API Cell

Dans le cas d'une liste telle que celle ci-dessous par exemple, chaque ligne visible est représentée par une cellule. En fait, les lignes contenant des valeurs qui sont complètement en dehors de la zone d'affichage n'existent pas dans la vue. Le calcul de la hauteur totale de la liste est effectué en extrapolant les valeurs retournées par les cellules qui existent réellement et non plus sur l'intégralité des éléments de la liste. Il en va de même pour les composants qui peuvent défiler horizontalement. Il s'agit là de l'application d'un principe souvent ignoré de la programmation orientée objet : la réutilisation/le recyclage des objets existants !

Figure 3 - Seules les parties visibles nous intéressent.

Si on fait défiler les données, les cellules qui deviennent invisibles seront récupérées, réutilisées et reconfigurées, comme l'était le renderer de Swing, de manière à afficher les nouveaux éléments qui apparaissent sur les lignes nouvellement visibles. Si la liste est agrandie (à l'écran) de nouvelles cellules seront créées pour afficher les lignes supplémentaires.

Figure 4 - Création et réutilisation de cellules lors du défilement.

Ici, nous voyons bien que seuls les éléments visibles sont affichés par des cellules :

- lorsque l'élément 6 devient visible, une nouvelle cellule orange est créée ; à ce stade, l'élément 1 est encore partiellement visible, la cellule rouge est donc toujours présente ;

- plus tard durant le défilement, la cellule rouge contenant l'élément 1 est retirée de l'affichage lorsque cet élément n'est plus visible. Cette cellule est conservée dans un pool de cellules ;

- la cellule rouge est ensuite réutilisée lorsque l'élément 7 devient visible à son tour ;

- la cellule vert clair contenant l'élément 2 disparait à son tour lorsque cet élément n'est plus visible.

Bien sûr, il s'agit là du fonctionnement théorique du concept : des améliorations de performances sont encore possibles dans la gestion de la vue virtuelle. Ainsi, l'API Cell disponible dans les JDK 7 et 8 effectue des appels redondants lorsqu'un élément est modifié (même en dehors du champ de la vue) : toutes les cellules visibles sont mises à jour même si ce n'est pas nécessaire. Des correctifs de performances ont été implémentés et seront probablement intégrés lors de mises à jour ultérieures du JDK8.

III-B - Mise en œuvre

Prenons le code suivant (nous reviendrons sur ce qu'il fait de manière plus détaillée ultérieurement) :

1. final ListView<Integer> list = new ListView<>();

2. for (int index = 0; index < 3000; index++) {

3. list.getItems().add(index);

4. }

5. list.setCellFactory(new Callback<ListView<Integer>, ListCell<Integer>>() {

6. private int created;

8. @Override

9. public ListCell<Integer> call(ListView<Integer> p) {

10. created++;

11. System.out.println("=================Created " + created);

12. return new ListCell<Integer>() {

13. private int called;

15. @Override

16. protected void updateItem(Integer value, boolean empty) {

17. final int cellIndex = created;

18. called++;

19. System.out.println("Called " + cellIndex + " " + called);

20. super.updateItem(value, empty);

21. final String text = (value == null || empty) ? null : String.valueOf(value);

22. setText(text);

23. }

24. };

25. }

26. });

27. final StackPane root = new StackPane();

28. root.getChildren().add(list);

29. final Scene scene = new Scene(root);

30. primaryStage.setTitle("ListView");

31. primaryStage.setWidth(500);

32. primaryStage.setHeight(500);

33. primaryStage.setScene(scene);

34. primaryStage.show();

Ici, nous remplissons également une liste graphique avec 3000 éléments. Nous spécifions une fabrique à cellules sur la ListView. Dans cette fabrique, à chaque appel à la méthode call(), nous créons une nouvelle implémentation d'une cellule capable de transformer un Integer en String pour procéder à son affichage. Cette transformation s'effectue lorsque la méthode updateItem() est appelée. Il n'y a pas besoin de spécifier un ScrollPane, les composants JavaFX peuvent afficher des barres de scroll quand nécessaire.

Cela nous donne le résultat suivant à l'écran :

Figure 5 - Une ListView de nombres.

Ici, seuls les 20 premiers éléments sont visibles (la police et la hauteur des lignes dans JavaFX sont différentes de celles de Swing). Un petit tour sur la console nous montre que la méthode call() de la fabrique à cellules placée sur notre ListView a été appelée 21 fois environ (une cellule supplémentaire est créée pour la ligne affichant le nombre 20). La méthode updateItem() des cellules créées a été, quant à elle, appelée 2-3 fois en moyenne pour chaque cellule. On reste très loin des 3000+ appels dans la version Swing, même si ici, on a créé 21 objets cellule au lieu d'un seul objet renderer.

C'est lors de l'utilisation de la liste que cette manière de faire prend tout son sens : si nous faisons défiler le contenu de la liste ou si nous redimensionnons la fenêtre horizontalement, nous nous apercevons alors que seule la méthode updateItem() de chacune des 21 cellules existantes est appelée. Aucune nouvelle cellule n'est créée.

En fait la méthode call() de la fabrique à cellule n'est appelée que si nous agrandissons notre fenêtre verticalement : en effet pour chaque nouvelle ligne qui devient visible, une nouvelle cellule doit être générée.

Si on réduit au contraire la taille de la fenêtre, on se rend alors compte que la méthode updateItem() n'est appelée que sur les cellules qui sont visibles à l'écran.

Note : si on réduit la taille de la liste jusqu'à n'afficher qu'une seule ligne, les cellules non visibles sont toujours référencées en mémoire. Elles seront à nouveau utilisées si on fait défiler les données ou si on agrandit la fenêtre.

C'est un sacré gain de performances et de mémoire qui permet d'afficher des listes contenant un très très grand nombre d'éléments avec une rapidité d'affichage maximale ou encore sur des environnements restreints tels que des plateformes mobiles.

III-C - Contrôles virtualisés

Voici la liste de tous les contrôles actuels dits « virtualisés » de JavaFX et qui utilisent l'API Cell.

- javafx.scene.control.ListView<V> : une liste graphique.

- javafx.scene.control.ComboBox<V> : une boite déroulante.

- javafx.scene.control.TableView<V> : une table affichant des données sur plusieurs colonnes.

- javafx.scene.control.TreeView<V> : un arbre graphique.

- javafx.scene.control.TreeTableView<V> : une table dont la première colonne contient un arbre. JDK8 ou supérieur.

On peut classer ces contrôles en deux grandes familles : ceux utilisant une liste d'objets et ceux utilisant une arborescence d'objets.

Les contrôles à base de liste :

- ListView

- ComboBox

- TableView

Les contrôles à base d'arbre :

- TreeView

- TreeTableView

III-D - ChoiceBox<V>

Le contrôle javafx.scene.control.ChoiceBox<V> ne fait pas partie des contrôles supportant l'API Cell. Ce contrôle est destiné à afficher une liste déroulante et ressemble à s'y méprendre à une ComboBox. Bien qu'acceptant n'importe quel type d'objet, la liste déroulante ne peut afficher que du texte simple sans aucune possibilité d'en changer la présentation ou d'ajouter des éléments graphiques ou riches. De plus, la boite déroulante n'est pas éditable.

La propriété converter, de type StringConverter<V>, permet de spécifier un convertisseur qui prendra un élément de type V en entrée et produira une String en sortie.

Dans la plupart des cas on préfèrera sans doute utiliser ComboBox à la place de cette classe, sauf quand on doit montrer une courte liste de valeurs simples à l'utilisateur.

III-E - DatePicker

Le contrôle javafx.scene.control.DatePicker, ajouté dans le JavaFX 8, permet d'utiliser une boite déroulante pour sélectionner une date dans un calendrier qui s'affiche dans un menu surgissant. Ce contrôle utilise également l'API Cell pour générer les cellules qui servent à l'affichage de chaque jour du calendrier.

Le programmeur peut utiliser sa propriété dayCellFactory pour fournir une fabrique à cellules permettant de customiser l'apparence de certains jours dans le calendrier, par exemple : afficher les anniversaires ou les fêtes ou jours fériés ou encore des rendez-vous.

IV - L'API Cell

La classe de base de l'API Cell est la classe javafx.scene.control.Cell<V> où V est le type de l'élément présent dans le conteneur. Cette classe hérite du contrôle javafx.scene.control.Labeled qui est la classe parente, entre autres, de la classe Label et des diverses classes de boutons dans l'API SceneGraph JavaFX.

IV-A - Classes

La classe Cell<V> a deux classes filles :

- javafx.scene.control.IndexedCell<V> - qui contient une propriété index. C'est cette classe qui est utilisée dans les contrôles « virtualisés » ;

- javafx.scene.control.DateCell - une classe de cellules utilisée par le contrôle DatePicker. JDK 8 ou supérieur.

La classe IndexedCell<V> est ensuite elle-même dérivée en classes destinées aux divers contrôles « virtualisés » :

- javafx.scene.controle.ListView<V> - est destinée à être utilisée dans ListView<V> et ComboBox<V> ;

- javafx.scene.controle.TableCell<V, T> - est destinée à être utilisée dans TableView<V>. Ici T est le type de l'objet affiché dans la colonne dans laquelle se trouve la cellule ;

- javafx.scene.controle.TableRow<V> - est destinée à être utilisée dans TableView<V>. Cette classe est rarement utilisée sauf si le programmeur veut remplacer une ligne entière de la table ;

- javafx.scene.controle.TreeCell<V> - est destinée à être utilisée dans TreeView<V> ;

- avafx.scene.controle.TreeTableCell<V, T> - est destinée à être utilisée dans TreeTableView<V>. Ici T est le type de l'objet affiché dans la colonne dans laquelle se trouve la cellule. JDK8 ou supérieur ;

- javafx.scene.controle.TreeTableRow<V> - est destinée à être utilisée dans TreeTableView<V>. Cette classe est rarement utilisée sauf si le programmeur veut remplacer une ligne entière de la table. JDK8 ou supérieur.

À partir de JavaFX 2.2, ces classes disposent d'une multitude de classes filles concrètes prêtes à l'emploi, spécialement destinées à afficher des champs de saisie texte, des cases à cocher, des barres de progression, ou même des boites déroulantes, etc.

Cela nous donne un graphe de hiérarchie de classes similaire à celui-ci :

Figure 6 - Hiérarchie des classes.

IV-B - Propriétés

Le fait d'hériter de la classe Labeled, signifie qu'une cellule partage plusieurs propriétés en commun avec le contrôle Label et les divers contrôles de boutons (Button, CheckBox, RadioButton, ToggleButton, etc.). Une cellule se manipule de manière similaire :

- la propriété text permet d'afficher du texte ;

- le texte dans la cellule peut être configuré au moyen de diverses propriétés liées :

- ellipsisString - le texte à utiliser en cas d'ellipse,

- font - la police de caractères du texte,

- lineSpacing - permet de spécifier l'espace entre les lignes en cas de texte multiligne. JDK8 ou supérieur,

- mnemonicParsing - indique si l'API doit rechercher le caractère _ comme indicateur de raccourci clavier,

- textAlignment - permet de spécifier l'alignement du paragraphe en cas de texte multiligne,

- textFill - permet de spécifier la couleur ou la texture du texte,

- textOverrun - spécifie le type d'ellipse à utiliser quand le texte doit être coupé lors de l'affichage.

- underline - permet de spécifier si le texte doit être souligné,

- wrapText - indique si un texte trop long peut avoir un retour à la ligne ;

- la propriété graphic permet de spécifier un « graphique », un nœud quelconque permettant d'avoir un affichage riche :

- une icône via une instance du contrôle ImageView,

- un hyperlien via une instance du contrôle HyperLink,

- un texte riche via une instance du contrôle TextFlow. JDK8 ou supérieur,

- une apparence riche et interactive beaucoup plus complexe qu'un simple label en spécifiant n'importe quel nœud ou contrôle ;

- le positionnement du graphique par rapport au texte peut être spécifié via diverses propriétés :

- contentDisplay - permet de choisir entre un affichage texte seul, graphique seul ou combinant les deux ainsi que la position relative de l'un par rapport à l'autre,

- graphicTextGap - permet de spécifier l'espacement entre le texte et le graphique ;

- d'autres propriétés s'appliquent sur le couple texte + graphique :

- alignement - l'alignement du couple à l'intérieur du label,

- labelPadding - l'espacement tout autour du couple à l'intérieur du label ;

- les accesseurs CSS contrôlant l'aspect et le positionnement du texte et du graphique sont similaires à ceux des autres classes héritant de Labeled.

De plus, la classe Cell contient des propriétés qui lui sont propres et qui permettent l'édition lorsqu'elle est utilisée dans certains contrôles :

- editable - indique s'il est possible d'éditer le contenu de la cellule ;

- editing - une propriété en lecture seule qui indique si le contenu de la cellule est en train d'être édité ;

- empty - une propriété en lecture seule qui indique si la cellule est vide et qu'il n'y a pas de données à visualiser (ex. : les lignes vides d'une table qui complètent l'affichage lorsque la table contient peu de données) ;

- item - l'objet contenu dans la cellule. La valeur null est une valeur valide sans pour autant que la cellule soit considérée comme vide ;

- selected - une propriété en lecture seule qui indique si la cellule est sélectionnée.

Enfin, les cellules héritant de la classe IndexedCell disposent également de la propriété :

- index - une propriété en lecture seule qui indique la position de la cellule dans le contrôle « virtualisé ».

IV-C - null vs. empty ?

On peut penser que, si on n'a pas de valeur sur une ligne, cette dernière est vide, c'est-à-dire que la propriété empty de la cellule de cette liste est à la valeur true. Ce n'est pas ainsi que cela fonctionne : la propriété empty ne prend la valeur true que lorsque la cellule est destinée à être affichée dans une région du contrôle (liste, arbre, table, etc.) où il n'y a pas de valeur : on est en dehors du champ des valeurs possibles et, par exemple dans le cas d'une liste, on a dû ajouter des lignes vierges pour compléter l'affichage.

Par exemple, imaginons une liste graphique qui affiche des entiers et qui contient les valeurs null, 0, 1, 2, 3 et 4. La capture ci-dessous nous montre ce à quoi pourrait ressembler l'affichage d'une telle liste ; ici nous avons beaucoup plus de lignes à l'écran que de données dans la liste :

Figure 7 - ListView contenant null en première position.

La valeur null est affichée par la cellule de la première ligne ; ici nous avons choisi de ne rien afficher pour cette ligne. Nous aurions tout autant pu décider d'afficher le texte « <vide> » ou encore « rien » au lieu de ne rien afficher du tout. Cette ligne n'est donc pas considérée comme vide, car il y a bien une valeur, même si cette dernière est égale à null. On peut sans souci sélectionner cette ligne dans la liste graphique.

D'un autre côté, toutes les lignes situées en dessous du chiffre 4 sont effectivement vides de valeur. Il n'existe pas de valeur dans la liste pour peupler ces lignes. Une dernière précision : il n'est pas possible de sélectionner ces cellules qui sont vides puisqu'elles ne contiennent pas de donnée.

Ainsi, nous pouvons très bien avoir les deux cas de figure suivants :

- item = null et empty = false : nous sommes sur une ligne de donnée et cette donnée est null ;

- item = null et empty = true : nous sommes sur une ligne complémentaire et il n'y a aucune donnée à afficher, ce qui est tout à fait normal.

Pour résumer : une valeur null ne signifie pas forcément une ligne vide ; par contre une ligne vide contient toujours la valeur null. Il faudra faire bien attention à traiter correctement ces cas de figure lorsque nous serons amenés à créer nos propres cellules !

IV-D - À propos des exemples suivants

Nous allons faire un rapide tour d'horizon des divers contrôles « virtualisés » en nous attachant à chaque fois à montrer des cas de figure particuliers. Cependant, les diverses méthodes montrées sont applicables de manière générale dans l'API Cell : par exemple les astuces montrées pour les cas tournant autour de TableView peuvent être aisément appliquées à ListView ou TreeView en utilisant les classes idoines ou du code similaire.

V - Listes

Cette première série de contrôles utilise une simple liste d'objets pour stocker ses éléments (oui, même TableView !). Ces contrôles disposent donc tous de la propriété suivante :

- items - une ObservableList<V> contenant les objets à afficher.

V-A - ListView<V>

Le contrôle javafx.scene.control.ListView<V> permet d'afficher une liste graphique, soit orientée verticalement avec une seule colonne, soit orientée horizontalement sur une seule ligne. Les listes graphiques de JavaFX sont également éditables si le besoin s'en fait sentir :

- Affichage vertical - il s'agit de l'affichage classique le plus souvent utilisé :

Figure 8 - ListView à affichage vertical.

Dans une ListView verticale, les cellules sont agencées les unes sous les autres. Chaque cellule est étendue pour remplir tout l'espace horizontal occupé par la liste graphique. Ici les cellules vides sont affichées en gris.

Figure 9 - Cellules dans un affichage vertical.

- Affichage horizontal :

Figure 10 - ListView à affichage horizontal.

Dans une ListView horizontale, les cellules sont agencées les unes à côté des autres. Chaque cellule est étendue pour remplir tout l'espace vertical occupé par la liste graphique.

Figure 11 - Cellules dans un affichage horizontal.

V-A-1 - Propriétés

Les propriétés importantes propres à ce contrôle sont les suivantes :

- cellFactory - de type Callback<ListView<V>, ListCell<V>> ; une fabrique à cellules qui est utilisée pour produire une cellule destinée à afficher chaque valeur visible de la liste ;

- editable - permet de spécifier si les lignes de la liste peuvent être éditées ;

- editingIndex - une propriété en lecture seule qui retourne l'indice de la ligne en train d'être éditée ;

- items - une ObservableList<V> contenant les objets à afficher ;

- fixedCellSize - permet de forcer une hauteur commune à toutes les cellules au lieu de calculer la hauteur cellule par cellule. Cette propriété est destinée à améliorer les performances de mise en page. JDK8 ou supérieur ;

- onEditCancel - un callback appelé lorsque l'utilisateur annule son édition ;

- onEditCommit - un callback appelé lorsque l'utilisateur valide son édition ;

- onEditStart - un callback appelé lorsque l'utilisateur démarre son édition ;

- onScrollTo - un callback appelé lorsque la méthode scrollTo(indice) ou scrollTo(valeur) sont invoquées. JDK8 ou supérieur ;

- orientation - permet de spécifier si la liste est verticale ou horizontale ;

- placeHolder - un nœud graphique à afficher lorsque la liste n'a pas de contenu. JDK8 ou supérieur.

V-A-2 - En pratique

Prenons le code suivant :

1. final ListView<Integer> listView = new ListView<>();

2. for (int index = 0; index < 3000; index++) {

3. listView.getItems().add(index);

4. }

5. final StackPane root = new StackPane();

6. root.getChildren().add(listView);

7. final Scene scene = new Scene(root);

8. primaryStage.setTitle("ListView");

9. primaryStage.setWidth(300);

10. primaryStage.setHeight(350);

11. primaryStage.setScene(scene);

12. primaryStage.show();

Nous venons de créer une simple liste qui affiche des nombres en utilisant l'affichage par défaut.

Figure 12 - Affichage simple de nombres entiers.

V-A-2-a - Bases

Imaginons maintenant que nous désirons afficher en gras et dans une couleur différente, tous les nombres premiers de cette liste. Nous avons tout d'abord besoin d'une méthode naïve permettant de déterminer si un nombre est premier :

1. public static boolean isPrime(int value) {

2. boolean result = true;

3. if (value < 2) {

4. result = false;

5. } else if (value > 2) {

6. if (value % 2 == 0) {

7. result = false;

8. } else {

9. final int max = (int) (Math.ceil(Math.sqrt(value)));

10. for (int i = 3; i <= max; i++) {

11. if (value % i == 0) {

12. result = false;

13. break;

14. }

15. }

16. }

17. }

18. return result;

19. }

Grâce à cette méthode, les nombres 2, 3, 5, 7, 11, 13, 17, 19, 23, 29 … sont testés positifs comme étant des nombres premiers.

Maintenant, nous allons fournir une fabrique à cellules à notre liste graphique : pour chaque ligne de la liste qui a besoin d'une nouvelle cellule, la fabrique sera appelée et une nouvelle instance de la cellule sera créée. Pour cela nous devons fournir dans la propriété cellFactory, un objet de type Callback<ListView<Integer>, ListCell<Integer>>, c'est-à-dire un callback qui prend une instance de ListView<Integer> en paramètre d'entrée et renvoie une nouvelle instance de ListCell<Integer> en sortie. Nous pouvons déclarer cela comme suit :

1. listView.setCellFactory(new Callback<ListView<Integer>, ListCell<Integer>>() {

3. @Override

4. public ListCell<Integer> call(ListView<Integer> p) {

5. // Ici nous devons créer une nouvelle cellule capable d'afficher un entier !

6. […]

7. }

8. });

Maintenant, il nous faut un type de cellule qui soit capable d'afficher un entier à l'écran. Le plus simple est encore d'étendre ListCell<Integer> dans une nouvelle classe anonyme et de surcharger la méthode updateItem(). Tant qu'à faire, ajoutons également le fait que lorsqu'on rencontre un nombre premier, on mette la police en gras, on souligne le nombre et on change la couleur du texte. Pour cela, nous allons utiliser les CSS, ce qui donne par exemple :

1. new ListCell<Integer>() {

3. @Override

4. protected void updateItem(Integer value, boolean empty) {

5. // Cet appel permet de s'assurer de la bonne configuration de la cellule.

6. super.updateItem(value, empty);

7. // Et maintenant, on change le texte en fonction de l'élément courant.

8. final String text = (value == null || empty) ? null : String.valueOf(value);

9. setText(text);

10. String style = null;

11. if (!empty && value != null && isPrime(value)) {

12. // On utilise les CSS pour modifier l'apparence de la cellule.

13. style = "-fx-font-weight: bold; -fx-text-fill: skyblue; -fx-underline: true;";

14. }

15. setStyle(style);

16. }

17. };

En premier lieu, il est important d'effectuer un appel à super.updateItem() pour avoir une configuration correcte de la cellule. Si cet appel manque, la cellule peut ne pas fonctionner correctement : si vous avez, par exemple, une boite déroulante dans laquelle le fait de sélectionner une valeur dans la liste ne semble avoir aucun effet, cela est probablement dû au fait que vous avez omis d'appeler cette méthode.

Ensuite, étant donné qu'une cellule peut être amenée à être réutilisée pour afficher d'autres valeurs, il est important de réinitialiser correctement les propriétés auxquelles nous accédons, ici les propriétés text et style.

Nous pouvons finir en intégrant les deux bouts de code ensemble :

1. listView.setCellFactory(new Callback<ListView<Integer>, ListCell<Integer>>() {

3. @Override

4. public ListCell<Integer> call(ListView<Integer> p) {

5. return new ListCell<Integer>() {

7. @Override

8. protected void updateItem(Integer value, boolean empty) {

9. super.updateItem(value, empty);

10. final String text = (value == null || empty) ? null : String.valueOf(value);

11. setText(text);

12. String style = null;

13. if (!empty && value != null && isPrime(value)) {

14. // On utilise les CSS pour modifier l'apparence de la cellule.

15. style = "-fx-font-weight: bold; -fx-text-fill: skyblue; -fx-underline: true;";

16. }

17. setStyle(style);

18. }

19. };

20. }

21. });

À l'exécution, nous obtenons le résultat suivant : les nombres premiers s'affichent bien en gras, soulignés et en bleu ciel !

Figure 13 - Mise en surbrillance des nombres premiers.

V-A-2-b - Mise en page riche

Nous allons désormais afficher un peu plus d'informations concernant nos nombres premiers : par exemple leur notation dans d'autres bases, un hyperlien vers l'encyclopédie en ligne Wikipedia ou encore une petite description.

Commençons par créer ce à quoi notre nouvelle cellule va ressembler. Plutôt que de nous embêter avec du long code Java pour créer ce nouveau composant, on peut directement créer un nouveau FXML avec SceneBuilder ou directement en codant du XML dans votre IDE. Ce nouveau composant servira de patron pour les futures cellules :

Figure 14 - Création de la cellule dans SceneBuilder.

Fichier test.PrimeListCell.fxml

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

3. <?import java.lang.*?>

4. <?import javafx.scene.control.*?>

5. <?import javafx.scene.layout.*?>

6. <?import javafx.scene.text.*?>

8. <AnchorPane fx:id="rootPane" prefHeight="-1.0" prefWidth="-1.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2" fx:controller="test.PrimeListCellController">

9. <children>

10. <GridPane fx:id="gridPane" prefHeight="100.0" style="-fx-hgap: 6; -fx-vgap: 6; -fx-padding: 6;" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">

11. <children>

12. <Label fx:id="numberLabel" style="-fx-font-size: 2.5em; -fx-font-weight: bold;" text="2" GridPane.columnIndex="0" GridPane.halignment="CENTER" GridPane.rowIndex="0" GridPane.rowSpan="2147483647" GridPane.valignment="CENTER" />

13. <Label fx:id="otherLabel" style="-fx-font-size: 0.8em;" text="Lorem ipsum dolor sit amet, consectetur adipiscing elit." textAlignment="JUSTIFY" wrapText="true" GridPane.columnIndex="1" GridPane.hgrow="ALWAYS" GridPane.rowIndex="0" GridPane.valignment="TOP" GridPane.vgrow="ALWAYS" />

14. <Label fx:id="descriptionLabel" style="-fx-font-size: 0.8em; -fx-font-style: italic; -fx-opacity: 0.6;" text="Lorem ipsum dolor sit amet, consectetur adipiscing elit." textAlignment="JUSTIFY" wrapText="true" GridPane.columnIndex="1" GridPane.columnSpan="2147483647" GridPane.hgrow="SOMETIMES" GridPane.rowIndex="1" GridPane.valignment="TOP" GridPane.vgrow="ALWAYS" />

15. <Hyperlink fx:id="hyperlink" onAction="#handleLink" style="-fx-font-size: 0.8em;" text="Hyperlink" GridPane.columnIndex="2" GridPane.halignment="RIGHT" GridPane.rowIndex="0" GridPane.valignment="TOP" />

16. </children>

17. <columnConstraints>

18. <ColumnConstraints hgrow="NEVER" minWidth="-1.0" prefWidth="-1.0" />

19. <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />

20. <ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />

21. </columnConstraints>

22. <rowConstraints>

23. <RowConstraints minHeight="-1.0" prefHeight="-1.0" vgrow="NEVER" />

24. <RowConstraints minHeight="-1.0" prefHeight="-1.0" vgrow="NEVER" />

25. </rowConstraints>

26. </GridPane>

27. </children>

28. </AnchorPane>

Nous allons maintenant créer un contrôleur pour gérer correctement ce contrôle que nous avons défini dans le fichier FXML. Lorsqu'il reçoit un nombre, le contrôleur modifie les valeurs des textes des labels. De plus, en cas de clic sur l'hyperlien, il ouvre le navigateur sur une page de Wikipédia donnant plus de détails sur ce nombre. À cet effet, nous avons besoin d'une référence sur l'application actuelle. Nous allons donc rajouter deux propriétés sur ce contrôleur : une propriété value qui est le nombre à afficher et une propriété application qui est la référence vers l'application parente.

Fichier test.PrimeListCellController.java, code JDK7

1. public class PrimeListCellController implements Initializable {

3. @FXML

4. private AnchorPane rootPane;

6. @FXML

7. private GridPane gridPane;

9. @FXML

10. private Label numberLabel;

12. @FXML

13. private Label descriptionLabel;

15. @FXML

16. private Label otherLabel;

18. @FXML

19. private Hyperlink hyperlink;

21. public PrimeListCellController() {

22. valueProperty().addListener(valueChangeListener);

23. }

25. /**

26. * Initialisation du contrôleur.

27. */

28. @Override

29. public void initialize(URL url, ResourceBundle rb) {

30. numberLabel.setText(null);

31. descriptionLabel.setText(null);

32. otherLabel.setText(null);

33. hyperlink.setText("En savoir plus...");

34. hyperlink.disableProperty().bind(applicationProperty().isNull());

35. hyperlink.visibleProperty().bind(valueProperty().lessThanOrEqualTo(500));

36. hyperlink.managedProperty().bind(hyperlink.visibleProperty());

37. }

39. /**

40. * Cet écouteur est appelé lorsque la propriété value change.

41. */

42. private final ChangeListener<Number> valueChangeListener = new ChangeListener<Number>() {

44. @Override

45. public void changed(ObservableValue<? extends Number> observableValue, Number oldValue, Number newValue) {

46. updateUI(newValue);

47. }

48. };

50. private void updateUI(Number newValue) {

51. final int val = newValue.intValue();

52. final String numberText = String.valueOf(newValue);

53. final String descriptionText = String.format("Le nombre %d est un nombre premier.Il ne peut être divisé que par lui-même et par le nombre 1.", val);

54. final String otherText = String.format("En binaire : %s\nEn octal : 0%s\nEn hexadécimal: 0x%s", Integer.toBinaryString(val), Integer.toOctalString(val), Integer.toHexString(val));

55. numberLabel.setText(numberText);

56. descriptionLabel.setText(descriptionText);

57. otherLabel.setText(otherText);

58. }

60. /**

61. * Cette méthode est appelée lorsque l'utilisateur clique sur l'hyperlien.

62. */

63. @FXML

64. private void handleLink(ActionEvent actionEvent) {

65. final Application application = getApplication();

66. final Integer value = getValue();

67. if (application != null && value != null) {

68. final int val = getValue();

69. final String url = String.format("http://fr.wikipedia.org/wiki/%d (nombre)", val);

70. application.getHostServices().showDocument(url);

71. }

72. }

74. /**

75. * Contient la valeur à afficher.

76. */

77. private final IntegerProperty value = new SimpleIntegerProperty(this, "value");

79. public final Integer getValue() {

80. return value.get();

81. }

83. public final void setValue(Integer value) {

84. this.value.set(value);

85. }

87. public final IntegerProperty valueProperty() {

88. return value;

89. }

91. /**

92. * Contient une référence vers l'application parente.

93. */

94. private final ObjectProperty<Application> application = new SimpleObjectProperty<>(this, "application");

96. public final Application getApplication() {

97. return application.get();

98. }

100. public final void setApplication(Application value) {

101. this.application.set(value);

102. }

104. public final ObjectProperty<Application> applicationProperty() {

105. return application;

106. }

107. }

Fichier test.PrimeListCellController.java, code JDK8

1. public class PrimeListCellController implements Initializable {

3. @FXML

4. private AnchorPane rootPane;

6. @FXML

7. private GridPane gridPane;

9. @FXML

10. private Label numberLabel;

12. @FXML

13. private Label descriptionLabel;

15. @FXML

16. private Label otherLabel;

18. @FXML

19. private Hyperlink hyperlink;

21. public PrimeListCellController() {

22. valueProperty().addListener(valueChangeListener);

23. }

25. /**

26. * Initialisation du contrôleur.

27. */

28. @Override

29. public void initialize(URL url, ResourceBundle rb) {

30. numberLabel.setText(null);

31. descriptionLabel.setText(null);

32. otherLabel.setText(null);

33. hyperlink.setText("En savoir plus...");

34. hyperlink.disableProperty().bind(applicationProperty().isNull());

35. hyperlink.visibleProperty().bind(valueProperty().lessThanOrEqualTo(500));

36. hyperlink.managedProperty().bind(hyperlink.visibleProperty());

37. }

39. /**

40. * Cet écouteur est appelé lorsque la propriété value change.

41. */

42. private final ChangeListener<Number> valueChangeListener = (ObservableValue<? extends Number> observableValue, Number oldValue, Number newValue) -> {

43. updateUI(newValue);

44. };

46. private void updateUI(Number newValue) {

47. final int val = newValue.intValue();

48. final String numberText = String.valueOf(newValue);

49. final String descriptionText = String.format("Le nombre %d est un nombre premier.Il ne peut être divisé que par lui-même et par le nombre 1.", val);

50. final String otherText = String.format("En binaire : %s\nEn octal : 0%s\nEn hexadécimal: 0x%s", Integer.toBinaryString(val), Integer.toOctalString(val), Integer.toHexString(val));

51. numberLabel.setText(numberText);

52. descriptionLabel.setText(descriptionText);

53. otherLabel.setText(otherText);

54. }

56. /**

57. * Cette méthode est appelée lorsque l'utilisateur clique sur l'hyperlien.

58. */

59. @FXML

60. private void handleLink(ActionEvent actionEvent) {

61. final Application application = getApplication();

62. final Integer value = getValue();

63. if (application != null && value != null) {

64. final int val = getValue();

65. final String url = String.format("http://fr.wikipedia.org/wiki/%d (nombre)", val);

66. application.getHostServices().showDocument(url);

67. }

68. }

70. /**

71. * Contient la valeur à afficher.

72. */

73. private final IntegerProperty value = new SimpleIntegerProperty(this, "value");

75. public final Integer getValue() {

76. return value.get();

77. }

79. public final void setValue(Integer value) {

80. this.value.set(value);

81. }

83. public final IntegerProperty valueProperty() {

84. return value;

85. }

87. /**

88. * Contient une référence vers l'application parente.

89. */

90. private final ObjectProperty<Application> application = new SimpleObjectProperty<>(this, "application");

92. public final Application getApplication() {

93. return application.get();

94. }

96. public final void setApplication(Application value) {

97. this.application.set(value);

98. }

100. public final ObjectProperty<Application> applicationProperty() {

101. return application;

102. }

103. }

Pour rendre les choses plus lisibles, nous allons copier le code de notre cellule dans une classe séparée et nous allons lui faire charger le FXML. Le constructeur de cette classe nécessite une référence à l'application parente en paramètre ; cette référence est par la suite fournie au contrôleur du FXML.

Lorsque la méthode updateItem() est appelée, si notre nombre actuel n'est pas premier, nous utilisons une simple conversion en texte et nous mettons le graphique de la cellule à null, comme précédemment. Par contre, dans le cas d'un nombre premier, nous passons ce nombre au contrôleur du FXML, nous mettons le texte de la cellule à null et nous affichons le nœud chargé depuis le FXML en tant que graphique de la cellule.

Fichier test.PrimeListCell.java

1. public class PrimeListCell extends ListCell<Integer> {

3. private Node renderer;

4. private PrimeListCellController rendererController;

6. public PrimeListCell(final Application application) {

7. super();

8. // Chargement du FXML.

9. try {

10. final URL fxmlURL = getClass().getResource("PrimeListCell.fxml");

11. final FXMLLoader fxmlLoader = new FXMLLoader(fxmlURL);

12. renderer = (Node) fxmlLoader.load();

13. rendererController = (PrimeListCellController) fxmlLoader.getController();

14. rendererController.setApplication(application);

15. } catch (IOException ex) {

16. Logger.getLogger(PrimeListCell.class.getName()).log(Level.SEVERE, ex.getMessage(), ex);

17. }

18. }

20. @Override

21. protected void updateItem(Integer value, boolean empty) {

22. super.updateItem(value, empty);

23. String text = null;

24. Node graphic = null;

25. if (!empty && value != null) {

26. // Si c'est un nombre premier, on utilise le nœud provenant du FXML.

27. if (isPrime(value) && renderer != null) {

28. graphic = renderer;

29. rendererController.setValue(value);

30. } else {

31. text = String.valueOf(value);

32. }

33. }

34. setText(text);

35. setGraphic(graphic);

36. }

37. }

Et enfin, nous modifions le code de l'application qui génère et affiche la liste pour instancier notre nouvelle classe dans notre fabrique à cellules.

Code JDK7

1. final ListView<Integer> listView = new ListView<>();

2. for (int index = 0; index < 3000; index++) {

3. listView.getItems().add(index);

4. }

5. listView.setCellFactory(new Callback<ListView<Integer>, ListCell<Integer>>() {

7. @Override

8. public ListCell<Integer> call(ListView<Integer> p) {

9. return new PrimeListCell(<Nom de votre application>.this);

10. }

11. });

12. final StackPane root = new StackPane();

13. root.getChildren().add(listView);

14. final Scene scene = new Scene(root);

15. primaryStage.setScene(scene);

Code JDK8

1. final ListView<Integer> listView = new ListView<>();

2. for (int index = 0; index < 3000; index++) {

3. listView.getItems().add(index);

4. }

5. listView.setCellFactory((ListView<Integer> lv) -> new PrimeListCell(MainFX3.this));

6. final StackPane root = new StackPane();

7. root.getChildren().add(listView);

8. final Scene scene = new Scene(root);

9. primaryStage.setScene(scene);

Compilons et lançons le programme. Voici le résultat !

Figure 15 - Utilisation de la cellule pour les nombres premiers.

Désormais toutes les lignes qui affichent des nombres premiers arborent un affichage riche, tandis que les nombres non premiers utilisent toujours l'affichage normal. Si vous cliquez sur l'hyperlien « En savoir plus... », le navigateur par défaut de votre système devrait s'ouvrir sur l'article de Wikipédia concernant ce nombre.

V-A-2-c - Utilisation interactive

Comme indiqué tantôt, il existe plusieurs types de cellules pour listes dans l'API qui sont déjà prêtes à l'emploi. Dans cet exemple, nous allons nous intéresser à l'utilisation de la classe CheckBoxListCell<V>.

Cette cellule affiche une case à cocher sur laquelle les utilisateurs peuvent cliquer. Ici, nous n'avons pas besoin de surcharger la méthode updateItem() mais à la place, nous devons fournir une ObservableValue<Boolean> pour chaque instance de la classe V contenue dans la liste. Cette propriété sera liée bidirectionnellement avec la propriété selected de la case à cocher et donc changera de valeur en conséquence.

De plus, nous pouvons fournir un StringConverter<V> qui permet de convertir une instance de la classe V en une chaine de texte pour avoir un label humainement lisible affiché à côté de la case à cocher.

Ici nous allons créer une petite interface listant des propriétés de notre application et qui sont configurables par les utilisateurs. Dans cet exemple, V est de type String, il s'agit des clés utilisées pour stocker les valeurs dans les préférences utilisateur et également pour récupérer les labels de l'UI dans le fichier contenant les traductions.

Fichier test.string.properties

1. file.overwrite.confirm=Confirmer l'écrasement des fichiers ?

2. file.save.auto=Activer la sauvegarde automatique ?

3. load.last.file.on.open=Toujours ouvrir le dernier fichier au lancement ?

Code JDK7

1. // Permet d'accéder aux préférences utilisateur.

2. final Preferences prefs = Preferences.userNodeForPackage(<nom de votre classe ici>.class);

3. // Clés des préférences.

4. final ResourceBundle bundle = ResourceBundle.getBundle("test.strings");

5. final String[] keys = {

6. "file.overwrite.confirm",

7. "file.save.auto",

8. "load.last.file.on.open"

9. };

10. // Propriétés JavaFX associées à chaque clé.

11. final Map<String, ObservableValue<Boolean>> properties = new HashMap<>();

12. for (final String key : keys) {

13. final BooleanProperty property = new SimpleBooleanProperty();

14. // Valeur initiale

15. final boolean value = prefs.getBoolean(key, false);

16. property.setValue(value);

17. properties.put(key, property);

18. // Sauvegarder la valeur en cas de modification.

19. property.addListener(new ChangeListener<Boolean>() {

21. @Override

22. public void changed(ObservableValue<? extends Boolean> observableValue, Boolean oldValue, Boolean newValue) {

23. prefs.putBoolean(key, newValue);

24. }

25. });

26. }

27. // Permet de récupérer la propriété JavaFX associée à chaque clé.

28. final Callback<String, ObservableValue<Boolean>> propertyAccessor = new Callback<String, ObservableValue<Boolean>>() {

30. @Override

31. public ObservableValue<Boolean> call(String key) {

32. return properties.get(key);

33. }

34. };

35. // Permet d'afficher un label plus parlant à l'utilisateur.

36. final StringConverter<String> labelConverter = new StringConverter<String>() {

38. @Override

39. public String toString(String t) {

40. return bundle.getString(t);

41. }

43. // On n'utilise pas cette méthode.

44. @Override

45. public String fromString(String string) {

46. throw new UnsupportedOperationException("Not supported yet.");

47. }

48. };

49. final ListView<String> listView = new ListView<>();

50. // Fabrique à cellules.

51. listView.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {

53. @Override

54. public ListCell<String> call(ListView<String> p) {

55. final CheckBoxListCell<String> result = new CheckBoxListCell<>();

56. result.setConverter(labelConverter);

57. // La propriété sera bindée bidirectionnellement sur la case à cocher de la cellule.

58. result.setSelectedStateCallback(propertyAccessor);

59. return result;

60. }

61. });

62. listView.getItems().setAll(keys);

63. final StackPane root = new StackPane();

64. root.getChildren().add(listView);

65. final Scene scene = new Scene(root);

66. primaryStage.setScene(scene);

Code JDK8

1. // Permet d'accéder aux préférences utilisateur.

2. final Preferences prefs = Preferences.userNodeForPackage(<nom de votre classe ici>.class);

3. // Clés des préférences.

4. final ResourceBundle bundle = ResourceBundle.getBundle("test.strings");

5. final String[] keys = {

6. "file.overwrite.confirm",

7. "file.save.auto",

8. "load.last.file.on.open"

9. };

10. // Propriétés JavaFX associées à chaque clé.

11. final Map<String, ObservableValue<Boolean>> properties = new HashMap<>();

12. for (final String key : keys) {

13. final BooleanProperty property = new SimpleBooleanProperty();

14. // Valeur initiale

15. final boolean value = prefs.getBoolean(key, false);

16. property.setValue(value);

17. properties.put(key, property);

18. // Sauvegarder la valeur en cas de modification.

19. property.addListener((ObservableValue<? extends Boolean> observableValue, Boolean oldValue, Boolean newValue) -> {

20. prefs.putBoolean(key, newValue);

21. });

22. }

23. // Permet de récupérer la propriété JavaFX associée à chaque clé.

24. final Callback<String, ObservableValue<Boolean>> propertyAccessor = (String key) -> properties.get(key);

25. // Permet d'afficher un label plus parlant à l'utilisateur.

26. final StringConverter<String> labelConverter = new StringConverter<String>() {

28. @Override

29. public String toString(String t) {

30. return bundle.getString(t);

31. }

33. // On n'utilise pas cette méthode.

34. @Override

35. public String fromString(String string) {

36. throw new UnsupportedOperationException("Not supported yet.");

37. }

38. };

39. final ListView<String> listView = new ListView<>();

40. // Fabrique à cellules.

41. listView.setCellFactory((ListView<String> p) -> {

42. final CheckBoxListCell<String> result = new CheckBoxListCell<>();

43. result.setConverter(labelConverter);

44. // La propriété sera bindée bidirectionnellement sur la case à cocher de la cellule.

45. result.setSelectedStateCallback(propertyAccessor);

46. return result;

47. });

48. listView.getItems().setAll(keys);

49. final StackPane root = new StackPane();

50. root.getChildren().add(listView);

51. final Scene scene = new Scene(root);

52. primaryStage.setScene(scene);

Et voici le résultat ! Initialement, toutes les cases sont décochées, mais si vous en cochez une et que vous redémarrez l'application, vous constaterez que la valeur a été stockée dans les préférences utilisateur et réutilisée au lancement suivant.

Figure 16 - ListView contenant des CheckBox.

Si nous n'avions pas spécifié de convertisseur, au lieu d'un label humainement lisible, les clés auraient directement été affichées dans la liste graphique.

Petite astuce : la classe CheckBoxListCell dispose de plusieurs variantes de la méthode statique forListView() qui peuvent être appelées pour générer la fabrique à cellules en saisissant moins de code. Ainsi, en utilisant ces méthodes, le code devient plus aisé à lire :

1. // Fabrique à cellules.

2. listView.setCellFactory(CheckBoxListCell.forListView(propertyAccessor, labelConverter));

V-B - ComboBox<V>

Le contrôle javafx.scene.controle.ComboBox<V> permet d'afficher une boite déroulante dont il est possible d'éditer le contenu. Ce contrôle a la spécificité d'accepter deux types de cellules au lieu d'un seul.

- Version non éditable - en général le contrôle aura un fond gris :

Figure 17 - ComboBox non éditable affichant des nombres entiers.

- Version éditable - il est possible de saisir des valeurs sur le contrôle, car sa partie gauche se comporte comme un champ de saisie textuel :

Figure 18 - ComboBox éditable affichant des nombres entiers.

En y regardant de plus près, une boite déroulante est constituée de trois parties :

- la flèche qui indique qu'il est possible de dérouler la boite, elle-même dans un bouton. Cette partie peut être configurée via les CSS tant pour sa couleur que pour sa forme ;

- la partie « bouton », située à gauche de la flèche en général. Cette partie peut parfois être un champ d'édition. Ici elle est représentée par une cellule verte ;

- enfin, il y a la liste qui apparait dans un menu surgissant quand on déroule la boite. Cette liste ne contient jamais de lignes vides supplémentaires. Dans cette liste, les cellules se comportent exactement comme dans une ListView.

Figure 19 - Les diverses cellules d'une ComboBox.

Note : dans JavaFX 2.x, un bogue de mise en page fait que la partie bouton s'étend sur toute la largeur de la boite déroulante, y compris sous la partie réservée à la flèche. Il faudra faire attention, car le texte ou le contenu des cellules peut se retrouver caché par la partie contenant la flèche.

Dans JavaFX 8, le bouton et la partie contenant la flèche ne se chevauchent pas.

V-B-1 - Propriétés

Les propriétés importantes propres à ce contrôle sont les suivantes :

- buttonCell - de type ListCell<V> ; une cellule unique qui est utilisée pour la partie « bouton » du contrôle. Cette cellule est également amenée à servir d'éditeur lorsque le contenant est éditable ;

- cellFactory - de type Callback<ListView<V>, ListCell<V>> ; comme dans ListView, une fabrique à cellules qui est sert à produire une cellule utilisée dans les lignes visibles de la liste déroulante qui est affichée lorsque l'utilisateur active le contrôle ;

- converter - un convertisseur de type StringConverter<V> qui sera utilisé en mode édition pour convertir la valeur textuelle saisie par l'utilisateur en un objet de type V ;

- editable - permet de spécifier si la partie bouton peut être éditée ;

- editor - une propriété en lecture seule de type TextField qui retourne l'éditeur utilisé par la boite déroulante ;

- items - une ObservableList<V> contenant les objets à afficher ;

- placeHolder - un nœud graphique à afficher lorsque la boite déroulante n'a pas de contenu. JDK8 ou supérieur ;

- value - la valeur de type V actuellement sélectionnée ;

- visibleRowCount - Le nombre maximum de lignes à afficher lorsque l'utilisateur déroule la liste.

On peut donc faire varier les présentations en utilisant buttonCell et cellFactory : la boite déroulante n'est pas obligée d'arborer les mêmes présentations d'entre sa partie liste et sa partie bouton.

V-B-2 - En pratique

Prenons le fichier suivant qui nous présente des espèces de poissons. Ces informations pourraient tout aussi bien avoir été générées par une requête sur une base de données. Nous y retrouvons :

- le nom scientifique de l'espèce ;

- le nom anglais courant de l'espèce ;

- le nom français courant de l'espèce ;

- son statut de conservation sur la liste rouge de l'UICN. Cette liste représente l'état de conservation des espèces animales et végétales.

Fichier test.fishes.txt

1. Thunnus albacares|Yellowfin Tuna|Thon Jaune|NT

2. Thunnus obesus|Bigeye Tuna|Thon obèse|VU

3. Katsuwonus pelamis|Skipjack Tuna|Bonite à ventre rayé|LC

4. Thunnus alalunga|Albacore Tuna|Germon|NT

5. Kajikia audax|Striped Marlin|Marlin rayé|NT

6. Istiompax indica|Black Marlin||DD

Nous allons les lire et puis les stocker de manière très classique :

Fichier test.Fish.java

1. public class Fish {

2. String id;

3. String en;

4. String fr;

5. UICN3_1 conservationStatus;

6. }

Nous ajoutons également une énumération qui définit tous les niveaux de la version 3.1 de la liste rouge de l'UICN :

Fichier test.UICN3_1.java

1. public enum UICN3_1 {

3. EXTINCT("EX"),

4. EXTINCT_IN_THE_WILD("EW"),

5. CRITICALLY_ENDANGERED("CR"),

6. ENDANGERED("EN"),

7. VULNERABLE("VU"),

8. NEAR_THREATENED("NT"),

9. LEAST_CONCERN("LC"),

10. DATA_DEFICIENT("DD"),

11. NOT_EVALUATED("NE");

13. private final String code;

15. UICN3_1(final String code) {

16. this.code = code;

17. }

19. public String getCode() {

20. return code;

21. }

23. public static UICN3_1 parse(final String code) {

24. UICN3_1 result = null;

25. for (final UICN3_1 status : values()) {

26. if (status.code.equals(code)) {

27. result = status;

28. }

29. }

30. if (result == null) {

31. throw new IllegalArgumentException(code);

32. }

33. return result;

34. }

35. }

Et enfin nous chargeons le tout et l'incluons dans notre UI :

1. final List<Fish> fishList = new LinkedList<>();

2. fishList.add(null);

3. try {

4. try (final InputStream input = getClass().getResourceAsStream("fishes.txt")) {

5. try (final LineNumberReader in = new LineNumberReader(new InputStreamReader(input))) {

6. for (String line = in.readLine(); line != null; line = in.readLine()) {

7. line = line.trim();

8. if (line.isEmpty()) {

9. continue;

10. }

11. final String[] tokens = line.split("\\|");

12. final Fish fish = new Fish();

13. fish.id = tokens[0];

14. fish.en = tokens[1];

15. fish.fr = tokens[2];

16. fish.conservationStatus = UICN3_1.parse(tokens[3]);

17. fishList.add(fish);

18. }

19. }

20. }

21. } catch (IOException | IllegalArgumentException | IndexOutOfBoundsException ex) {

22. Logger.getLogger(getClass().getName()).log(Level.SEVERE, ex.getMessage(), ex);

23. }

24. final ComboBox<Fish> comboBox = new ComboBox<>();

25. comboBox.getItems().setAll(fishList);

26. comboBox.setPrefWidth(300);

27. comboBox.setMaxWidth(300);

28. comboBox.setVisibleRowCount(5);

29. comboBox.getSelectionModel().select(null);

30. fishList.clear();

31. final StackPane root = new StackPane();

32. root.getChildren().add(comboBox);

33. final Scene scene = new Scene(root);

34. primaryStage.setTitle("ComboBox");

35. primaryStage.setWidth(300);

36. primaryStage.setHeight(350);

37. primaryStage.setScene(scene);

On peut remarquer que j'ai inclus la valeur null dans la liste des valeurs possibles dans la boite déroulante.

Évidemment, en l'état l'affichage est assez limité :

Figure 20 - Affichage basique dans une ComboBox.

V-B-3 - Affichage riche

Nous allons désormais utiliser deux cellules différentes :

- une première cellule qui sera utilisée par le bouton, une description courte contenant juste le nom scientifique, le nom anglais et le nom français de l'espèce. Nous allons cependant utiliser un affichage complexe, car les trois mots seront affichés avec des styles de police différents ;

- une seconde cellule avec un affichage plus détaillé qui aura une présentation différente et arborera le niveau de conservation de l'espèce ainsi qu'une image montrant le poisson en question. Ici, nous allons utiliser un style plus complexe, proche de celui utilisé par Wikipédia (version anglaise) pour afficher le tableau résumé d'une espèce :

Figure 21- Extrait de la page Wikipedia anglaise « EnSkipjack tuna » en date du 28 avril 2014.

Encore une fois, pour simplifier le code de l'interface, nous allons utiliser des fichiers FXML pour définir l'apparence de nos cellules. Tout d'abord, nous définissons une classe qui étend ListCell<Fish> et dans laquelle nous chargeons nos FXML en fonction de l'apparence à utiliser. Cette cellule peut être configurée pour utiliser deux apparences :

Fichier test.FishListCell.java

1. public final class FishListCell extends ListCell<Fish> {

3. /**

4. * Définit les styles possibles pour cette cellule.

5. */

6. public enum Style {

8. BARE,

9. EXPANDED;

10. }

12. private Style style;

13. private Node renderer;

14. private FishListCellControllerBase rendererController;

16. public FishListCell(final Style style) throws IllegalArgumentException {

17. if (style == null) {

18. throw new IllegalArgumentException("style est null.");

19. }

20. this.style = style;

21. // Chargement du FXML.

22. try {

23. final String fxmlName = (style == Style.BARE) ? "FishListCell_Bare.fxml" : "FishListCell_Expanded.fxml";

24. final URL fxmlURL = getClass().getResource(fxmlName);

25. final FXMLLoader fxmlLoader = new FXMLLoader(fxmlURL);

26. renderer = (Node) fxmlLoader.load();

27. rendererController = (FishListCellControllerBase) fxmlLoader.getController();

28. } catch (IOException ex) {

29. Logger.getLogger(FishListCell.class.getName()).log(Level.SEVERE, ex.getMessage(), ex);

30. }

31. }

33. @Override

34. protected void updateItem(Fish value, boolean empty) {

35. super.updateItem(value, empty);

36. String text = null;

37. Node graphic = null;

38. if (!empty) {

39. if (value == null) {

40. text = (style != Style.BARE) ? "< Aucun >" : "Aucune sélection";

41. } else {

42. if (renderer != null) {

43. graphic = renderer;

44. rendererController.setFish(value);

45. } else {

46. text = String.valueOf(value);

47. }

48. }

49. }

50. setText(text);

51. setGraphic(graphic);

52. }

53. }

Nous aurions pu définir deux classes bien distinctes, mais cela aurait mené à de la duplication de code inutile pour le chargement du fichier FXML ou encore la configuration de la cellule pour l'affichage. Pour cette même raison, j'ai défini une classe mère commune au contrôleur de chaque FXML :

Fichier test.FishListCellControllerBase.java

1. package test;

3. import javafx.beans.property.ObjectProperty;

4. import javafx.beans.property.SimpleObjectProperty;

5. import javafx.beans.value.ChangeListener;

6. import javafx.beans.value.ObservableValue;

8. abstract class FishListCellControllerBase {

10. public FishListCellControllerBase() {

11. fishProperty().addListener(fishChangeListener);

12. }

14. // J'ai mis ici du code JDK7, mais il est aisément convertible en code JDK8.

15. // NetBeans ou votre IDE peut probablement le faire automatiquement à votre place.

16. private final ChangeListener<Fish> fishChangeListener = new ChangeListener<Fish>() {

17. @Override

18. public void changed(ObservableValue<? extends Fish> observableValue, Fish oldValue, Fish newValue) {

19. updateContent(newValue);

20. }

21. };

23. protected abstract void updateContent(Fish fish);

25. private final ObjectProperty<Fish> fish = new SimpleObjectProperty<>(this, "fish");

27. public final Fish getFish() {

28. return fish.get();

29. }

31. public final void setFish(final Fish value) {

32. fish.set(value);

33. }

35. public final ObjectProperty<Fish> fishProperty() {

36. return fish;

37. }

38. }

Pour chacune de ces apparences, nous définissions un fichier CSS, FXML ainsi que le contrôleur :

- BARE - l'apparence qui sera utilisée pour le bouton ;

Figure 22 - Création de la cellule dans SceneBuilder.

Fichier test.FishListCell_Bare.fxml

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

3. <?import java.lang.*?>

4. <?import java.net.*?>

5. <?import java.util.*?>

6. <?import javafx.scene.*?>

7. <?import javafx.scene.control.*?>

8. <?import javafx.scene.layout.*?>

10. <AnchorPane id="AnchorPane" prefHeight="-1.0" prefWidth="200.0" styleClass="fish-cell" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2" fx:controller="test.FishListCell_BareController">

11. <children>

12. <HBox prefHeight="-1.0" prefWidth="-1.0" styleClass="hbox" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">

13. <children>

14. <Label fx:id="idLabel" maxWidth="1.7976931348623157E308" minWidth="-Infinity" styleClass="title-label" text="Label" HBox.hgrow="ALWAYS" />

15. <Label fx:id="enLabel" maxWidth="-1.0" minWidth="-1.0" styleClass="i18n-label, en-label" text="Label" />

16. <Label fx:id="frLabel" maxWidth="-1.0" minWidth="-1.0" styleClass="i18n-label, fr-label" text="Label" />

17. </children>

18. </HBox>

19. </children>

20. <stylesheets>

21. <URL value="@FishListCell_Bare.css" />

22. </stylesheets>

23. </AnchorPane>

Nous avons attaché une feuille de style à notre FXML :

Fichier test.FishListCell_Bare.css

1. .fish-cell {

2. }

3. .fish-cell .hbox {

4. -fx-alignment: center-left;

5. -fx-spacing: 4;

6. -fx-padding: 0 3 0 0;

7. }

8. .fish-cell .label {

9. -fx-text-fill: black;

10. }

11. .fish-cell .title-label {

12. }

13. .fish-cell .i18n-label {

14. -fx-opacity: 0.33;

15. -fx-font-size: 0.77em;

16. -fx-alignment: center-right;

17. }

18. .fish-cell .en-label {

19. }

20. .fish-cell .fr-label {

21. -fx-font-style: italic;

22. }

Et il a bien sûr besoin d'un contrôleur :

Fichier test.FishListCell_BareController.java

1. package test;

3. import java.net.URL;

4. import java.util.ResourceBundle;

5. import javafx.fxml.FXML;

6. import javafx.fxml.Initializable;

7. import javafx.scene.control.Label;

9. public final class FishListCell_BareController extends FishListCellControllerBase implements Initializable {

10. @FXML

11. private Label idLabel;

13. @FXML

14. private Label enLabel;

16. @FXML

17. private Label frLabel;

19. /**

20. * Initializes the controller class.

21. */

22. @Override

23. public void initialize(URL url, ResourceBundle rb) {

24. }

26. @Override

27. protected void updateContent(Fish fish) {

28. idLabel.setText((fish == null) ? null : fish.id);

29. enLabel.setText((fish == null) ? null : fish.en);

30. frLabel.setText((fish == null) ? null : fish.fr);

31. }

32. }

- EXPANDED - l'apparence qui sera utilisée dans la liste.

Figure 23 - Création de la cellule dans SceneBuilder.

Fichier test.FishListCell_Expanded.fxml

1. <AnchorPane id="AnchorPane" prefHeight="-1.0" prefWidth="200.0" styleClass="fish-cell" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2" fx:controller="test.FishListCell_ExpandedController">

2. <children>

3. <VBox prefHeight="-1.0" prefWidth="-1.0" styleClass="content-vbox" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">

4. <children>

5. <Label fx:id="idLabel" maxWidth="1.7976931348623157E308" minWidth="-Infinity" styleClass="title-label" text="Label" />

6. <HBox prefHeight="-1.0" prefWidth="-1.0" styleClass="hbox" VBox.vgrow="NEVER">

7. <children>

8. <fx:include fx:id="conservationStatusIndicator" source="ConservationStatusIndicator.fxml" HBox.hgrow="NEVER" />

9. <Pane prefHeight="-1.0" prefWidth="-1.0" HBox.hgrow="ALWAYS" />

10. <ImageView fx:id="icon" fitHeight="50.0" fitWidth="0.0" pickOnBounds="true" preserveRatio="true" HBox.hgrow="NEVER" />

11. <Pane prefHeight="-1.0" prefWidth="-1.0" HBox.hgrow="ALWAYS" />

12. <VBox prefHeight="-1.0" prefWidth="-1.0" styleClass="i18n-vbox" HBox.hgrow="NEVER">

13. <children>

14. <Label fx:id="enLabel" maxWidth="1.7976931348623157E308" minWidth="-Infinity" styleClass="i18n-label, en-label" text="Label" />

15. <Label fx:id="frLabel" maxWidth="1.7976931348623157E308" minWidth="-Infinity" styleClass="i18n-label, fr-label" text="Label" />

16. </children>

17. </VBox>

18. </children>

19. </HBox>

20. </children>

21. </VBox>

22. </children>

23. <stylesheets>

24. <URL value="@FishListCell_Expanded.css" />

25. </stylesheets>

26. </AnchorPane>

Ce nœud graphique dispose également de sa propre feuille de style :

Fichier test.FishListCell_Expanded.css

1. .fish-cell {

2. }

3. .fish-cell .content-vbox {

4. -fx-alignment: center;

5. }

6. .fish-cell .hbox {

7. -fx-alignment: center-left;

8. }

9. .fish-cell .i18n-vbox {

10. -fx-spacing: 1;

11. -fx-padding: 0 3 0 0;

12. }

13. .fish-cell .title-label {

14. -fx-background-color: derive(darkkhaki, 40%);

15. -fx-alignment: center;

16. -fx-font-weight: bold;

17. -fx-font-size: 1.4em;

18. }

19. .fish-cell .i18n-label {

20. -fx-opacity: 0.66;

21. -fx-font-size: 1.2em;

22. -fx-alignment: center-right;

23. }

24. .fish-cell .en-label {

25. }

26. .fish-cell .fr-label {

27. -fx-font-style: italic;

28. }

Et du contrôleur idoine :

Fichier test.FishListCell_ExpandedController.java

1. package test;

3. import java.net.URL;

4. import java.util.ResourceBundle;

5. import javafx.fxml.FXML;

6. import javafx.fxml.Initializable;

7. import javafx.scene.control.Label;

8. import javafx.scene.image.Image;

9. import javafx.scene.image.ImageView;

11. public final class FishListCell_ExpandedController extends FishListCellControllerBase implements Initializable {

13. @FXML

14. private Label idLabel;

16. @FXML

17. private Label enLabel;

19. @FXML

20. private Label frLabel;

22. @FXML

23. private ImageView icon;

25. // La référence implicite au contrôleur du FXML inclus.

26. @FXML

27. private ConservationStatusIndicatorController conservationStatusIndicatorController;

29. @Override

30. public void initialize(URL url, ResourceBundle rb) {

31. }

33. @Override

34. protected void updateContent(Fish fish) {

35. idLabel.setText((fish == null) ? null : fish.id);

36. enLabel.setText((fish == null) ? null : fish.en);

37. frLabel.setText((fish == null) ? null : fish.fr);

38. URL url = null;

39. if (fish != null) {

40. // Chargement de l'image depuis le disque dur.

41. final String path = String.format("images/%s.png", fish.id.replaceAll("\\ ", "_"));

42. url = getClass().getResource(path);

43. }

44. final Image image = (url == null) ? null : new Image(url.toExternalForm());

45. icon.setImage(image);

46. conservationStatusIndicatorController.setConservationStatus((fish == null) ? null : fish.conservationStatus);

47. }

48. }

Vous pouvez vous apercevoir que notre FXML en référence un autre : pour représenter le statut de conservation, nous allons utiliser un nouveau contrôle que nous avons créé et qui permet d'afficher un badge de couleur ainsi que les deux lettres, sigle du statut.

Figure 24 - Création de l'indicateur de statut dans SceneBuilder.

Fichier test.ConservationStatusIndicator.fxml

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

3. <?import java.lang.*?>

4. <?import java.net.*?>

5. <?import java.util.*?>

6. <?import javafx.scene.*?>

7. <?import javafx.scene.control.*?>

8. <?import javafx.scene.layout.*?>

10. <AnchorPane fx:id="rootPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="30.0" prefWidth="30.0" styleClass="conservation-status-indicator" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2" fx:controller="test.ConservationStatusIndicatorController">

11. <children>

12. <StackPane fx:id="indicatorArea" prefHeight="-1.0" prefWidth="-1.0" styleClass="shape" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">

13. <children>

14. <Label fx:id="label" text="LA" />

15. </children>

16. </StackPane>

17. </children>

18. <stylesheets>

19. <URL value="@ConservationStatusIndicator.css" />

20. </stylesheets>

21. </AnchorPane>

Ce nouveau contrôle dispose de sa propre feuille de style, ce qui permet de faire varier son apparence en fonction du statut de conservation courant.

Fichier test.ConservationStatusIndicator.css

1. .conservation-status-indicator {

2. -fx-padding: 2;

3. -cs-border-color: gray;

4. -cs-fill-color: black;

5. -cs-text-fill: white;

6. }

7. .conservation-status-indicator .shape {

8. -fx-background-color: -cs-border-color, -cs-fill-color;

9. -fx-background-radius: 55, 54;

10. -fx-background-insets: 0, 1;

11. }

12. .conservation-status-indicator .label {

13. -fx-text-fill: -cs-text-fill;

14. -fx-font-weight: bold;

15. }

16. .ex-style {

17. -cs-border-color: gray;

18. -cs-fill-color: black;

19. -cs-text-fill: tomatored;

20. }

21. .ew-style {

22. -cs-border-color: gray;

23. -cs-fill-color: black;

24. -cs-text-fill: white;

25. }

26. .cr-style {

27. -cs-border-color: derive(tomatored, -30%);

28. -cs-fill-color: tomatored;

29. -cs-text-fill: derive(tomatored, 75%);

30. }

31. .en-style {

32. -cs-border-color: derive(orange, -30%);

33. -cs-fill-color: orange;

34. -cs-text-fill: derive(orange, 75%);

35. }

36. .vu-style {

37. -cs-border-color: derive(darkgoldenrod, -30%);

38. -cs-fill-color: darkgoldenrod;

39. -cs-text-fill: white;

40. }

41. .nt-style {

42. -cs-border-color: derive(teal, 30%);

43. -cs-fill-color: derive(teal, -30%);

44. -cs-text-fill: derive(teal, 75%);

45. }

46. .lc-style {

47. -cs-border-color: derive(teal, 30%);

48. -cs-fill-color: derive(teal, -30%);

49. -cs-text-fill: white;

50. }

51. .ne-style {

52. -cs-border-color: derive(teal, 30%);

53. -cs-fill-color: derive(teal, -30%);

54. -cs-text-fill: white;

55. }

56. .dd-style {

57. -cs-border-color: gray;

58. -cs-fill-color: white;

59. -cs-text-fill: black;

60. }

Et bien sûr, il dispose également de son propre contrôleur :

Fichier test.ConservationStatusIndicatorController.java, code JDK7

1. package test;

3. import java.net.URL;

4. import java.util.ResourceBundle;

5. import javafx.beans.property.ObjectProperty;

6. import javafx.beans.property.SimpleObjectProperty;

7. import javafx.beans.value.ChangeListener;

8. import javafx.beans.value.ObservableValue;

9. import javafx.fxml.FXML;

10. import javafx.fxml.Initializable;

11. import javafx.scene.control.Label;

12. import javafx.scene.layout.AnchorPane;

13. import javafx.scene.layout.StackPane;

15. public final class ConservationStatusIndicatorController implements Initializable {

17. @FXML

18. private AnchorPane rootPane;

20. @FXML

21. private StackPane indicatorArea;

23. @FXML

24. private Label label;

26. public ConservationStatusIndicatorController() {

27. conservationStatusProperty().addListener(conservationStatusChangeListener);

28. }

30. @Override

31. public void initialize(URL url, ResourceBundle rb) {

32. }

34. private final ChangeListener<UICN3_1> conservationStatusChangeListener = new ChangeListener<UICN3_1>() {

35. @Override

36. public void changed(ObservableValue<? extends UICN3_1> observableValue, UICN3_1 oldValue, UICN3_1 newValue) {

37. updateContent(oldValue, newValue);

38. }

39. };

41. protected void updateContent(UICN3_1 oldConservationStatus, UICN3_1 newConservationStatus) {

42. final String text = (newConservationStatus == null) ? null : newConservationStatus.getCode();

43. label.setText(text);

44. if (newConservationStatus != null) {

45. final String newStyle = String.format("%s-style", newConservationStatus.getCode().toLowerCase());

46. rootPane.getStyleClass().add(newStyle);

47. }

48. }

50. private final ObjectProperty<UICN3_1> conservationStatus = new SimpleObjectProperty<>(this, "conservationStatus");

52. public final UICN3_1 getConservationStatus() {

53. return conservationStatus.get();

54. }

56. public final void setConservationStatus(final UICN3_1 value) {

57. conservationStatus.set(value);

58. }

60. public final ObjectProperty<UICN3_1> conservationStatusProperty() {

61. return conservationStatus;

62. }

63. }

Fichier test.ConservationStatusIndicatorController.java, code JDK8

1. package test;

3. import java.net.URL;

4. import java.util.ResourceBundle;

5. import javafx.beans.property.ObjectProperty;

6. import javafx.beans.property.SimpleObjectProperty;

7. import javafx.beans.value.ChangeListener;

8. import javafx.beans.value.ObservableValue;

9. import javafx.fxml.FXML;

10. import javafx.fxml.Initializable;

11. import javafx.scene.control.Label;

12. import javafx.scene.layout.AnchorPane;

13. import javafx.scene.layout.StackPane;

15. public final class ConservationStatusIndicatorController implements Initializable {

17. @FXML

18. private AnchorPane rootPane;

20. @FXML

21. private StackPane indicatorArea;

23. @FXML

24. private Label label;

26. public ConservationStatusIndicatorController() {

27. conservationStatusProperty().addListener(conservationStatusChangeListener);

28. }

30. @Override

31. public void initialize(URL url, ResourceBundle rb) {

32. }

34. private final ChangeListener<UICN3_1> conservationStatusChangeListener = (ObservableValue<? extends UICN3_1> observableValue, UICN3_1 oldValue, UICN3_1 newValue) -> {

35. updateContent(oldValue, newValue);

36. };

38. protected void updateContent(UICN3_1 oldConservationStatus, UICN3_1 newConservationStatus) {

39. final String text = (newConservationStatus == null) ? null : newConservationStatus.getCode();

40. label.setText(text);

41. if (newConservationStatus != null) {

42. final String newStyle = String.format("%s-style", newConservationStatus.getCode().toLowerCase());

43. rootPane.getStyleClass().add(newStyle);

44. }

45. }

47. private final ObjectProperty<UICN3_1> conservationStatus = new SimpleObjectProperty<>(this, "conservationStatus");

49. public final UICN3_1 getConservationStatus() {

50. return conservationStatus.get();

51. }

53. public final void setConservationStatus(final UICN3_1 value) {

54. conservationStatus.set(value);

55. }

57. public final ObjectProperty<UICN3_1> conservationStatusProperty() {

58. return conservationStatus;

59. }

60. }

Désormais, il ne nous reste plus qu'à configurer notre boite déroulante pour qu'elle utilise les deux nouvelles cellules que nous venons de définir. Il suffit d'ajouter les lignes suivantes à notre code initial :

Code JDK7

1. comboBox.setButtonCell(new FishListCell(FishListCell.Style.BARE));

2. comboBox.setCellFactory(new Callback<ListView<Fish>, ListCell<Fish>>() {

4. @Override

5. public ListCell<Fish> call(ListView<Fish> p) {

6. return new FishListCell(FishListCell.Style.EXPANDED);

7. }

8. });

Code JDK8

1. comboBox.setButtonCell(new FishListCell(FishListCell.Style.BARE));

2. comboBox.setCellFactory((ListView<Fish> listView) -> new FishListCell(FishListCell.Style.EXPANDED));

Initialement, aucune espèce n'est sélectionnée, notre boite déroulante affiche la phrase « aucune sélection », ce qui correspond au fait que notre sélection initiale a une valeur égale à null :

Figure 25 - Cellule sans sélection.

Lorsque la boite est déroulée, c'est la cellule EXPANDED qui est utilisée. Le mot « Aucun » est utilisé pour afficher la valeur null, tandis que les autres valeurs utilisent l'affichage complexe et arborent le statut de conservation ainsi qu'une image de l'animal :

Figure 26 - Cellules dans la liste déroulante.

Lorsqu'une valeur non null est sélectionnée, c'est la cellule BARE qui est utilisée dans le bouton pour un affichage plus concis :

Figure 27 - Cellule avec sélection.

V-C - TableView<V>

Ce contrôle est un peu plus complexe à appréhender que les deux précédents de par sa nature. Une table contient bien une liste d'objets de type V, cependant ce n'est pas le cas de ses colonnes !

D'ailleurs en parlant de colonnes, nous serons amenés à manipuler une seconde classe tout aussi importante : javafx.scene.control.TableColumn<V, T>. En effet, chaque colonne de la table opère sur deux types :

- V qui est le type de l'objet contenu dans la table ;

- T qui est le type de l'objet à afficher dans la colonne.

V-C-1 - Propriétés

Les propriétés importantes propres à ce contrôle sont les suivantes :

- columnResizePolicy - un callback qui permet de spécifier si les colonnes prennent tout l'espace horizontal visible ou se contentent de leur taille prédéfinie. La class TableView contient deux constantes prédéfinies qu'il est conseillé d'utiliser :

- CONSTRAINED_RESIZE_POLICY - les colonnes prennent tout l'espace horizontal visible,

- UNCONSTRAINED_RESIZE_POLICY - utilisation des tailles prédéfinies ;

- comparator - une propriété en lecture seule qui contient un objet de type Comparator<V> lié au tri en cours sur la table. JDK8 ou supérieur ;

- editable - permet de spécifier si les cellules de la table peuvent être éditées ;

- editingCell - une propriété en lecture seule indiquant la position de la cellule en train d'être éditée ;

- fixedCellSize - permet de forcer une hauteur commune à toutes les cellules au lieu de calculer la hauteur cellule par cellule. Cette propriété est destinée à améliorer les performances de mise en page. JDK8 ou supérieur ;

- items - une ObservableList<V> contenant les objets à afficher ;

- onScrollToColumn - un callback appelé lorsque la méthode scrollToColumn(indice) ou scrollToColumn(colonne) est invoquée. JDK8 ou supérieur ;

- onScrollTo - un callback appelé lorsque la méthode scrollTo(indice) ou scrollTo(valeur) est invoquée. JDK8 ou supérieur ;

- onSort - un callback appelé lorsque le contenu du contrôle est trié. JDK8 ou supérieur ;

- placeHolder - un nœud graphique à afficher lorsque la table n'a pas de contenu. JDK8 ou supérieur ;

- rowFactory - une fabrique qui génère une cellule destinée à représenter une ligne entière de la table. Généralement on utilisera la fabrique par défaut ;

- selectionModel - gère la sélection dans la table. Généralement on utilisera le modèle par défaut ;

- sortPolicy - la politique de tri à appliquer sur le contenu de la table. JDK8 ou supérieur ;

- tableMenuButtonVisible - permet d'afficher un bouton autorisant l'utilisateur à accéder à un menu contrôlant la présence ou non de certaines colonnes.

Visiblement, ce n'est pas dans la table que se cachent les méthodes permettant d'accéder aux fabriques à cellules des colonnes. Il nous faut donc jeter un coup d'œil sur les propriétés de la classe TableColumn<V, T> :

- cellFactory - de type Callback<TableColumn<V, T>, TableCell<V, T>> ; une fabrique à cellules qui est utilisée pour produire une cellule destinée à afficher chaque valeur T de la colonne associée à chaque valeur V de la table ;

- cellValueFactory - très importante et à ne pas confondre avec la précédente : une fabrique qui, pour chaque valeur V de la table retourne une ObservableValue<T> pour cette colonne. Cette fabrique sert donc à peupler les valeurs de la colonne à partir de celles de la table ;

- onEditCancel - un callback appelé lorsque l'utilisateur annule son édition ;

- onEditCommit - un callback appelé lorsque l'utilisateur valide son édition ;

- onEditStart - un callback appelé lorsque l'utilisateur démarre son édition ;

- sortType - indique si la colonne est triée de manière ascendante ou descendante quand un tri est appliqué sur la table ;

- tableView - une propriété en lecture seule qui contient une référence vers la table parente.

V-C-2 - cellValueFactory

La propriété cellValueFactory est donc quelque chose de très important pour la colonne : c'est via cette fabrique que l'on peut extraire les objets de type T qui peuplent la colonne à partir des objets de type V qui sont présents dans la table.

Prenons par exemple la classe Truc suivante :

Fichier test.Truc.java

1. public class Truc {

2. ////////////////////////////////////////////////////////////////////////////

3. // Propriétés JavaFX.

4. private final BooleanProperty visible = new SimpleBooleanProperty();

6. public final boolean isVisible() {

7. return visible.get();

8. }

10. public final void setVisible(boolean value) {

11. visible.set(value);

12. }

14. public final BooleanProperty visibleProperty() {

15. return visible;

16. }

17. private final StringProperty name = new SimpleStringProperty();

19. public final String getName() {

20. return name.get();

21. }

23. public final void setName(String value) {

24. name.set(value);

25. }

27. public final StringProperty nameProperty() {

28. return name;

29. }

30. ////////////////////////////////////////////////////////////////////////////

31. // Propriétés JavaBeans observables.

32. private final PropertyChangeSupport propertyChangeSupport = new PropertyChangeSupport(this);

34. public final void addPropertyChangeListener(String property, PropertyChangeListener listener) {

35. propertyChangeSupport.addPropertyChangeListener(property, listener);

36. }

38. public final void removePropertyChangeListener(String property, PropertyChangeListener listener) {

39. propertyChangeSupport.removePropertyChangeListener(property, listener);

40. }

42. private final void firePropertyChange(String property, Object oldValue, Object newValue) {

43. propertyChangeSupport.firePropertyChange(property, oldValue, newValue);

44. }

45. private boolean opaque = false;

47. public boolean isOpaque() {

48. return opaque;

49. }

51. public void setOpaque(boolean value) {

52. final boolean oldValue = opaque;

53. opaque = value;

54. firePropertyChange("opaque", oldValue, opaque);

55. }

56. private String comment;

58. public String getComment() {

59. return comment;

60. }

62. public void setComment(String value) {

63. final String oldValue = comment;

64. comment = value;

65. firePropertyChange("comment", oldValue, comment);

66. }

68. ////////////////////////////////////////////////////////////////////////////

69. // Autres

71. public boolean administrator;

72. public String email;

73. }

Cette classe dispose de trois ensembles bien distincts de propriétés :

- des propriétés JavaFX :

- visible - un booléen,

- name - une chaine de texte ;

- des propriétés JavaBeans observables - c'est-à-dire que ces propriétés Java standard qui lèvent des évènements de type java.beans.PropertyChangeEvent lorsqu'elles sont modifiées. Il est de plus possible d'enregistrer des écouteurs de type java.beans.PropertyChangeListener sur notre objet de type Truc pour recevoir ces évènements :

- opaque - un booléen,

- comment - une chaine de texte ;

- des membres en accès direct - on aurait aussi pu mettre des getters couplés à des setters simples qui ne lèvent pas de PropertyChangeEvent en cas de modification :

- administrator - un booléen ;

- email - une chaine de texte.

Nous allons maintenant essayer d'afficher ces différentes propriétés dans une TableView<Truc>. Compte tenu de la nature de nos différentes propriétés, nous aurons des colonnes de type TableColumn<Truc, Boolean> et TableColumn<Truc, String>.

Commençons par créer notre table et par afficher notre fenêtre :

1. final TableView<Truc> tableView = new TableView<>();

2. tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);

3. final StackPane root = new StackPane();

4. root.getChildren().add(tableView);

5. final Scene scene = new Scene(root, 500, 150);

6. primaryStage.setTitle("TableView");

7. primaryStage.setScene(scene);

8. primaryStage.show();

Évidemment, pour le moment, le résultat est un peu vide à l'affichage : la table ne contient aucune colonne !

Figure 28 - Une TableView complètement vide.

Il nous faut aussi bien sûr ajouter les colonnes appropriées :

1. // Construction de la table.

2. final TableColumn<Truc, Boolean> visibleColumn = new TableColumn<>("Visible");

3. final TableColumn<Truc, String> nameColumn = new TableColumn<>("Nom");

4. final TableColumn<Truc, Boolean> opaqueColumn = new TableColumn<>("Opaque");

5. final TableColumn<Truc, String> commentColumn = new TableColumn<>("Commentaires");

6. final TableColumn<Truc, Boolean> administratorColumn = new TableColumn<>("Administrateur");

7. final TableColumn<Truc, String> emailColumn = new TableColumn<>("Mél");

8. tableView.getColumns().setAll(visibleColumn, nameColumn, opaqueColumn, commentColumn, administratorColumn, emailColumn);

Figure 29 - Ajout des colonnes.

De même qu'au moins une valeur !

1. final Truc t = new Truc();

2. t.setVisible(true);

3. t.setName("foo");

4. t.setOpaque(true);

5. t.setComment("Foo alors !");

6. t.administrator = false;

7. t.email = "foo@foo.foo";

8. tableView.getItems().add(t);

Figure 30 - Ajout des valeurs.

Nous avons bien quelque chose sur la première ligne de notre table, puisque nous pouvons la sélectionner, mais pour le moment rien ne s'affiche étant donné que les colonnes sont totalement incapables de récupérer les valeurs appropriées à partir de l'objet t.

V-C-2-a - Propriétés JavaFX

Quand on utilise des propriétés JavaFX, c'est bien simple : il n'y a pas grand-chose à faire ! Nous pouvons utiliser la classe javafx.scene.control.cell.PropertyValueFactory<V, T> pour créer une fabrique de propriétés utilisant la réflexion. Et tout fonctionne directement !

1. // Propriété visible.

2. visibleColumn.setCellValueFactory(new PropertyValueFactory<>("visible"));

3. // Propriété name.

4. nameColumn.setCellValueFactory(new PropertyValueFactory<>("name"));

Figure 31 - Ajout du support des propriétés JavaFX.

Si les propriétés visible et name sont modifiées ailleurs que dans la table, alors cette dernière se met à jour automatiquement avec les nouvelles valeurs.

V-C-2-b - Propriétés JavaBeans observables

Au premier abord, on peut penser qu'il suffit d'utiliser la classe PropertyValueFactory sur les propriétés opaque et comment et si on essaie, on verra que les valeurs correctes s'affichent. Mais en fait cela est trompeur : les valeurs initiales sont bien affichées ; cependant, en cas de modification hors de la table, cette dernière ne se met pas à jour ! Cela peut être gênant…

Nous allons devoir spécifier nos propres fabriques de valeurs pour ces deux propriétés et nous allons utiliser les builders présents dans l'API JavaFX qui permettent de créer un adaptateur entre les propriétés JavaBeans observables et les propriétés JavaFX :

Code JDK7

1. // Propriété opaque.

2. opaqueColumn.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Truc, Boolean>, ObservableValue<Boolean>>() {

3. @Override

4. public ObservableValue<Boolean> call(TableColumn.CellDataFeatures<Truc, Boolean> p) {

5. final Truc t = p.getValue();

6. try {

7. return JavaBeanBooleanPropertyBuilder.create().bean(t).name("opaque").build();

8. } catch (NoSuchMethodException ex) {

9. Logger.getLogger(MainFX4.class.getName()).log(Level.SEVERE, null, ex);

10. return null;

11. }

12. }

13. });

14. // Propriété comment.

15. commentColumn.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Truc, String>, ObservableValue<String>>() {

16. @Override

17. public ObservableValue<String> call(TableColumn.CellDataFeatures<Truc, String> p) {

18. final Truc t = p.getValue();

19. try {

20. return JavaBeanStringPropertyBuilder.create().bean(t).name("comment").build();

21. } catch (NoSuchMethodException ex) {

22. Logger.getLogger(MainFX4.class.getName()).log(Level.SEVERE, null, ex);

23. return null;

24. }

25. }

26. });

Code JDK8

1. // Propriété opaque.

2. opaqueColumn.setCellValueFactory((TableColumn.CellDataFeatures<Truc, Boolean> p) -> {

3. final Truc t = p.getValue();

4. try {

5. return JavaBeanBooleanPropertyBuilder.create().bean(t).name("opaque").build();

6. } catch (NoSuchMethodException ex) {

7. Logger.getLogger(MainFX4.class.getName()).log(Level.SEVERE, ex.getMessage(), ex);

8. return null;

9. }

10. });

11. // Propriété comment.

12. commentColumn.setCellValueFactory((TableColumn.CellDataFeatures<Truc, String> p) -> {

13. final Truc t = p.getValue();

14. try {

15. return JavaBeanStringPropertyBuilder.create().bean(t).name("comment").build();

16. } catch (NoSuchMethodException ex) {

17. Logger.getLogger(MainFX4.class.getName()).log(Level.SEVERE, ex.getMessage(), ex);

18. return null;

19. }

20. });

En utilisant cet adaptateur, la table est prévenue de toutes modifications extérieures des propriétés opaque et comment et le contenu des cellules de la ligne change en conséquence.

Figure 32 - Ajout du support des propriétés JavaBeans observables.

V-C-2-c - Autres

Nous allons créer des propriétés pour mettre en place un adaptateur destiné à convertir la valeur en provenance de l'objet vers une propriété JavaFX mais, ici il n'y a pas de secret : ces membres ou propriétés ne sont pas observables ; il est totalement impossible d'être tenu au courant d'une modification externe. De la même manière, si la table est éditable, une modification apportée sur la colonne n'est pas reportée sur l'objet.

Si on a affaire à des getters, on peut tenter l'utilisation de la classe PropertyValueFactory. Sinon, il va falloir procéder a mano :

Code JDK7

1. // Propriété administrator.

2. administratorColumn.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Truc, Boolean>, ObservableValue<Boolean>>() {

3. @Override

4. public ObservableValue<Boolean> call(TableColumn.CellDataFeatures<Truc, Boolean> p) {

5. final Truc t = p.getValue();

6. return new SimpleBooleanProperty(t.administrator);

7. }

8. });

9. // Propriété email.

10. emailColumn.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Truc, String>, ObservableValue<String>>() {

11. @Override

12. public ObservableValue<String> call(TableColumn.CellDataFeatures<Truc, String> p) {

13. final Truc t = p.getValue();

14. return new SimpleStringProperty(t.email);

15. }

16. });

Code JDK8

1. // Propriété administrator.

2. administratorColumn.setCellValueFactory((TableColumn.CellDataFeatures<Truc, Boolean> p) -> {

3. final Truc t = p.getValue();

4. return new SimpleBooleanProperty(t.administrator);

5. });

6. // Propriété email.

7. emailColumn.setCellValueFactory((TableColumn.CellDataFeatures<Truc, String> p) -> {

8. final Truc t = p.getValue();

9. return new SimpleStringProperty(t.email);

10. });

Nous avons enfin l'affichage complet de nos colonnes :

Figure 33 - Le contenu de nos deux dernières colonnes est enfin affiché.

V-C-3 - En pratique

Nous avons désormais des valeurs qui s'affichent dans notre table, même si la présentation laisse un peu à désirer. Revenons maintenant à nos moutons : l'utilisation des cellules pour changer l'apparence de la table. Nous avons enfin réussi à peupler une table d'instances de la classe Truc et à afficher des valeurs, certes, mais ça serait sans doute bien si nos valeurs booléennes étaient affichées par des cases à cocher, n'est-ce pas ?

V-C-3-a - Booléens

L'API JavaFX propose la classe prête à l'emploi javafx.scene.control.cell.CheckBoxTableCell qui peut être utilisée pour représenter des valeurs booléennes sous forme de cases à cocher. De plus, ici, nous n'aurons même pas besoin d'écrire une nouvelle classe ou un nouveau callback, puisque CheckBoxTableCell propose directement une fabrique statique. L'appel à la méthode forTableColumn() permet de générer le callback qui sera chargé de créer les cellules de ces colonnes. Il nous suffit donc d'insérer dans notre code :

visibleColumn.setCellFactory(CheckBoxTableCell.forTableColumn(visibleColumn));
opaqueColumn.setCellFactory(CheckBoxTableCell.forTableColumn(opaqueColumn));
administratorColumn.setCellFactory(CheckBoxTableCell.forTableColumn(administratorColumn));

pour que notre affichage se dote de cases à cocher dans les colonnes contenant des valeurs booléennes :

Figure 34 - Changement d'affichage pour les valeurs booléennes.

V-C-3-b - Édition

Nous allons maintenant rendre notre table éditable. Nous allons mettre des écouteurs sur les propriétés observables dans notre objet t ; cela nous permettra de vérifier que nous modifions réellement les valeurs contenues qu'il contient. Ces propriétés observables sont les propriétés visible, name, opaque et comment. Les propriétés administrator et email ne sont pas, quant à elles, directement observables en l'état donc nous ne pouvons pas enregistrer d'écouteurs dessus.

Code JDK7

1. t.visibleProperty().addListener(new ChangeListener<Boolean>() {

2. public void changed(ObservableValue<? extends Boolean> observableValue, Boolean oldValue, Boolean newValue) {

3. System.out.printf("Visible %b -> %b", oldValue, newValue).println();

4. }

5. });

6. t.nameProperty().addListener(new ChangeListener<String>() {

7. public void changed(ObservableValue<? extends String> observableValue, String oldValue, String newValue) {

8. System.out.printf("Nom %s -> %s", oldValue, newValue).println();

9. }

10. });

11. t.addPropertyChangeListener("opaque", new PropertyChangeListener() {

13. @Override

14. public void propertyChange(PropertyChangeEvent event) {

15. System.out.printf("Opaque %b -> %b", event.getOldValue(), event.getNewValue()).println();

16. }

17. });

18. t.addPropertyChangeListener("comment", new PropertyChangeListener() {

20. @Override

21. public void propertyChange(PropertyChangeEvent event) {

22. System.out.printf("Commentaire %s -> %s", event.getOldValue(), event.getNewValue()).println();

23. }

24. });

Code JDK8

1. t.visibleProperty().addListener((ObservableValue<? extends Boolean> observableValue, Boolean oldValue, Boolean newValue) -> System.out.printf("Visible %b -> %b", oldValue, newValue).println());

2. t.nameProperty().addListener((ObservableValue<? extends String> observableValue, String oldValue, String newValue) -> System.out.printf("Nom %s -> %s", oldValue, newValue).println());

3. t.addPropertyChangeListener("opaque", (PropertyChangeEvent event) -> System.out.printf("Opaque %b -> %b", event.getOldValue(), event.getNewValue()).println());

4. t.addPropertyChangeListener("comment", (PropertyChangeEvent event) -> System.out.printf("Commentaire %s -> %s", event.getOldValue(), event.getNewValue()).println());

Désormais, nous serons tenus au courant de toute modification sur ces propriétés par des affichages sur la console.

Pour qu'une valeur soit éditable dans la table, il faut que la table soit marquée comme éditable bien sûr, mais aussi que la colonne le soit. Commençons donc par insérer les lignes suivantes dans notre code :

1. visibleColumn.setEditable(true);

2. nameColumn.setEditable(true);

3. opaqueColumn.setEditable(true);

4. commentColumn.setEditable(true);

5. administratorColumn.setEditable(true);

6. emailColumn.setEditable(true);

7. tableView.setEditable(true);

Et… et c'est tout en ce qui concerne les propriétés visible et opaque ! Il est en effet possible, dans la table, de cocher et décocher les cases, ce qui provoque immédiatement des affichages dans la console similaires à :

1. Opaque true -> false

2. Opaque false -> true

3. Opaque true -> false

4. Opaque false -> true

5. Visible true -> false

6. Visible false -> true

7. Visible true -> false

8. Visible false -> true

Ce qui démontre que notre objet t reçoit bien les nouvelles valeurs booléennes qui ont été éditées dans la table.

Par contre, les colonnes contenant les valeurs des propriétés name, comment et email ne semblent pas vouloir être éditées : si on essaie de cliquer ou de double-cliquer sur le contenu de la cellule, rien ne se passe. Le problème se situe en fait au niveau de la cellule par défaut qui est produite : elle ne supporte pas l'édition. Ici aussi, l'API JavaFX vient à notre rescousse, nous allons utiliser la classe prête à l'emploi javafx.scene.control.cell.TextFieldTableCell. Cette classe permet d'utiliser un champ de saisie texte lors du passage en mode édition. Comme précédemment, il suffit d'appeler la méthode statique forTableColumn() pour disposer d'un callback prêt à l'emploi qui servira de fabrique de cellules. Insérons les lignes suivantes dans notre code :

Code JDK7

1. nameColumn.setCellFactory(TextFieldTableCell.<String>forTableColumn());

2. commentColumn.setCellFactory(TextFieldTableCell.<String>forTableColumn());

3. emailColumn.setCellFactory(TextFieldTableCell.<String>forTableColumn());

Code JDK8

1. nameColumn.setCellFactory(TextFieldTableCell.forTableColumn());

2. commentColumn.setCellFactory(TextFieldTableCell.forTableColumn());

3. emailColumn.setCellFactory(TextFieldTableCell.forTableColumn());

Au lancement, aucun changement n'est discernable, la table est totalement identique à ce que nous avions précédemment.

Figure 35 - Affichage au démarrage.

Cependant, si on double-clique sur une des colonnes contenant du texte, désormais, la cellule va basculer en mode édition.

Figure 36 - Passage en mode édition.

Les valeurs éditées dans les colonnes contenant les propriétés name et comment sont immédiatement reportées vers l'objet t une fois la validation effectuée, comme nous le montre l'affichage sur la console :

1. Nom foo -> Truc

2. Commentaire Foo alors !!!! -> Un chasseur sachant chasser...

Les propriétés administrator et email vont demander un peu plus de travail ! Bien qu'il soit possible d'éditer le contenu de la table, les modifications ne sont pas reportées vers l'objet t.

En ce qui concerne la propriété email, nous allons utiliser un callback sur la colonne. En effet, il nous suffit de placer un callback sur la propriété onEditCommit de la colonne : cette propriété est appelée lorsque l'utilisateur valide son édition. Nous pouvons donc récupérer la nouvelle valeur et la stocker dans notre objet t :

Code JDK7

1. emailColumn.setOnEditCommit(new EventHandler<TableColumn.CellEditEvent<Truc, String>>() {

3. @Override

4. public void handle(TableColumn.CellEditEvent<Truc, String> event) {

5. System.out.printf("Mél %s -> %s", event.getOldValue(), event.getNewValue()).println();

6. final Truc t = event.getRowValue();

7. t.email = event.getNewValue();

8. }

9. });

Code JDK8

1. emailColumn.setOnEditCommit((TableColumn.CellEditEvent<Truc, String> event) -> {

2. System.out.printf("Mél %s -> %s", event.getOldValue(), event.getNewValue()).println();

3. final Truc t = event.getRowValue();

4. t.email = event.getNewValue();

5. });

Désormais, la propriété email est correctement mise à jour en cas d'édition !

1. Mél foo@foo.foo ->

On serait tenté de faire quelque chose de similaire avec la propriété administrator. Cependant, si on le faisait, on se rendrait compte que le callback n'est jamais appelé. Le concepteur de CheckBoxTableCell a en effet estimé qu'il n'était pas utile que ce composant passe en mode édition lors de son activation. Après tout lors d'un clic, le changement de valeur est immédiatement reporté vers la propriété source dans la colonne.

Nous allons donc pallier ce problème en établissant un écouteur dans le code qui retourne une valeur observable à partir de notre objet t.

Code JDK7

1. // Propriété administrator.

2. administratorColumn.setCellValueFactory(new Callback<TableColumn.CellDataFeatures<Truc, Boolean>, ObservableValue<Boolean>>() {

3. @Override

4. public ObservableValue<Boolean> call(TableColumn.CellDataFeatures<Truc, Boolean> p) {

5. final Truc t = p.getValue();

6. final BooleanProperty result = new SimpleBooleanProperty(t.administrator);

7. result.addListener(new ChangeListener<Boolean>() {

8. @Override

9. public void changed(ObservableValue<? extends Boolean> observableValue, Boolean oldValue, Boolean newValue) {

10. t.administrator = newValue;

11. }

12. });

13. return result;

14. }

15. });

Code JDK8

1. // Propriété administrator.

2. administratorColumn.setCellValueFactory((TableColumn.CellDataFeatures<Truc, Boolean> p) -> {

3. final Truc t = p.getValue();

4. final BooleanProperty result = new SimpleBooleanProperty(t.administrator);

5. result.addListener((ObservableValue<? extends Boolean> observableValue, Boolean oldValue, Boolean newValue) -> {

6. t.administrator = newValue;

7. });

8. return result;

9. });

Désormais, en cas de modification de cette valeur dans la table, le membre correspondant dans l'objet est également correctement modifié !

V-C-3-c - Édition avancée

Pour le moment, nous avons vu comment utiliser les éditeurs fournis par l'API. Mais il est grand temps de se lancer dans la création d'éditeurs pour les cellules de notre table.

Les couleurs en JavaFX sont des objets immutables, c'est-à-dire qu'on ne peut pas les modifier après leur création. Pour cette même raison, les composants R (rouge), G (vert) et B (bleu) d'une couleur ne sont pas des propriétés observables. Cependant dans le cadre d'un éditeur, on peut être amené à vouloir modifier ces composantes. Commençons par définir une nouvelle classe qui représentera une couleur mutable. Cette nouvelle classe, MyColor, contiendra :

- color - une référence vers une couleur JavaFX. Modifier cette propriété modifie immédiatement les valeurs des autres propriétés ;

- red - la composante rouge. Modifier cette propriété modifie immédiatement la valeur de la propriété color sans toucher aux autres propriétés ;

- green - la composante vert. Modifier cette propriété modifie immédiatement la valeur de la propriété color sans toucher aux autres propriétés ;

- blue - la composante bleu. Modifier cette propriété modifie immédiatement la valeur de la propriété color sans toucher aux autres propriétés.

Fichier test.MyColor.java, code JDK7

1. package test;

3. import javafx.beans.property.DoubleProperty;

4. import javafx.beans.property.ObjectProperty;

5. import javafx.beans.property.SimpleDoubleProperty;

6. import javafx.beans.property.SimpleObjectProperty;

7. import javafx.beans.value.ChangeListener;

8. import javafx.beans.value.ObservableValue;

9. import javafx.scene.paint.Color;

11. public final class MyColor {

13. // Ce flag permet d'éviter des propagations de propriétés en cascade lors de l'édition des valeurs.

14. private boolean isEditing = false;

16. public MyColor() {

17. colorProperty().addListener(colorChangeListener);

18. redProperty().addListener(valueChangeListener);

19. greenProperty().addListener(valueChangeListener);

20. blueProperty().addListener(valueChangeListener);

21. }

23. public MyColor(Color color) {

24. this();

25. colorProperty().set(color);

26. }

28. ////////////////////////////////////////////////////////////////////////

29. /**

30. * Appelé lorsque la couleur change.

31. */

32. private ChangeListener<Color> colorChangeListener = new ChangeListener<Color>() {

34. @Override

35. public void changed(ObservableValue<? extends Color> observableValue, Color oldValue, Color newValue) {

36. if (isEditing) {

37. return;

38. }

39. isEditing = true;

40. redProperty().set((newValue == null) ? 0 : newValue.getRed());

41. greenProperty().set((newValue == null) ? 0 : newValue.getGreen());

42. blueProperty().set((newValue == null) ? 0 : newValue.getBlue());

43. isEditing = false;

44. }

45. };

47. /**

48. * Appelé lorsqu'une composante change.

49. */

50. private ChangeListener<Number> valueChangeListener = new ChangeListener<Number>() {

52. @Override

53. public void changed(ObservableValue<? extends Number> observableValue, Number oldValue, Number newValue) {

54. if (isEditing) {

55. return;

56. }

57. isEditing = true;

58. double red = redProperty().get();

59. double green = greenProperty().get();

60. double blue = blueProperty().get();

61. double alpha = 1;

62. final Color newColor = new Color(red, green, blue, alpha);

63. colorProperty().set(newColor);

64. isEditing = false;

65. }

66. };

68. ////////////////////////////////////////////////////////////////////////

69. /**

70. * La couleur.

71. */

72. private final ObjectProperty<Color> color = new SimpleObjectProperty<>(this, "color");

74. public final ObjectProperty<Color> colorProperty() {

75. return color;

76. }

78. public final Color getColor() {

79. return color.get();

80. }

82. public final void setColor(final Color value) {

83. color.set(value);

84. }

86. /**

87. * La composante rouge.

88. */

89. private final DoubleProperty red = new SimpleDoubleProperty(this, "red");

91. public final double getRed() {

92. return red.get();

93. }

95. public final void setRed(final double value) {

96. red.set(value);

97. }

99. public final DoubleProperty redProperty() {

100. return red;

101. }

102. /**

103. * La composante vert.

104. */

105. private final DoubleProperty green = new SimpleDoubleProperty(this, "green");

107. public final double getGreen() {

108. return green.get();

109. }

111. public final void setGreen(final double value) {

112. green.set(value);

113. }

115. public final DoubleProperty greenProperty() {

116. return green;

117. }

118. /**

119. * La composante bleu.

120. */

121. private final DoubleProperty blue = new SimpleDoubleProperty(this, "blue");

123. public final double getBlue() {

124. return blue.get();

125. }

127. public final void setBlue(final double value) {

128. blue.set(value);

129. }

131. public final DoubleProperty blueProperty() {

132. return blue;

133. }

134. }

Fichier test.MyColor.java, code JDK8

1. package test;

3. import javafx.beans.property.DoubleProperty;

4. import javafx.beans.property.ObjectProperty;

5. import javafx.beans.property.SimpleDoubleProperty;

6. import javafx.beans.property.SimpleObjectProperty;

7. import javafx.beans.value.ChangeListener;

8. import javafx.beans.value.ObservableValue;

9. import javafx.scene.paint.Color;

11. public final class MyColor {

13. // Ce flag permet d'éviter des propagations de propriétés en cascade lors de l'édition des valeurs.

14. private boolean isEditing = false;

16. public MyColor() {

17. colorProperty().addListener(colorChangeListener);

18. redProperty().addListener(valueChangeListener);

19. greenProperty().addListener(valueChangeListener);

20. blueProperty().addListener(valueChangeListener);

21. }

23. public MyColor(Color color) {

24. this();

25. colorProperty().set(color);

26. }

28. ////////////////////////////////////////////////////////////////////////

29. /**

30. * Appelé lorsque la couleur change.

31. */

32. private ChangeListener<Color> colorChangeListener = (ObservableValue<? extends Color> observableValue, Color oldValue, Color newValue) -> {

33. if (isEditing) {

34. return;

35. }

36. isEditing = true;

37. redProperty().set((newValue == null) ? 0 : newValue.getRed());

38. greenProperty().set((newValue == null) ? 0 : newValue.getGreen());

39. blueProperty().set((newValue == null) ? 0 : newValue.getBlue());

40. isEditing = false;

41. };

43. /**

44. * Appelé lorsqu'une composante change.

45. */

46. private ChangeListener<Number> valueChangeListener = (ObservableValue<? extends Number> observableValue, Number oldValue, Number newValue) -> {

47. if (isEditing) {

48. return;

49. }

50. isEditing = true;

51. double red1 = redProperty().get();

52. double green1 = greenProperty().get();

53. double blue1 = blueProperty().get();

54. double alpha = 1;

55. final Color newColor = new Color(red1, green1, blue1, alpha);

56. colorProperty().set(newColor);

57. isEditing = false;

58. };

60. ////////////////////////////////////////////////////////////////////////

61. /**

62. * La couleur.

63. */

64. private final ObjectProperty<Color> color = new SimpleObjectProperty<>(this, "color");

66. public final ObjectProperty<Color> colorProperty() {

67. return color;

68. }

70. public final Color getColor() {

71. return color.get();

72. }

74. public final void setColor(final Color value) {

75. color.set(value);

76. }

78. /**

79. * La composante rouge.

80. */

81. private final DoubleProperty red = new SimpleDoubleProperty(this, "red");

83. public final double getRed() {

84. return red.get();

85. }

87. public final void setRed(final double value) {

88. red.set(value);

89. }

91. public final DoubleProperty redProperty() {

92. return red;

93. }

94. /**

95. * La composante vert.

96. */

97. private final DoubleProperty green = new SimpleDoubleProperty(this, "green");

99. public final double getGreen() {

100. return green.get();

101. }

103. public final void setGreen(final double value) {

104. green.set(value);

105. }

107. public final DoubleProperty greenProperty() {

108. return green;

109. }

110. /**

111. * La composante bleu.

112. */

113. private final DoubleProperty blue = new SimpleDoubleProperty(this, "blue");

115. public final double getBlue() {

116. return blue.get();

117. }

119. public final void setBlue(final double value) {

120. blue.set(value);

121. }

123. public final DoubleProperty blueProperty() {

124. return blue;

125. }

126. }

Et bien évidemment, il nous faut désormais une table pour afficher ces couleurs :

1. final TableView<MyColor> tableView = new TableView<>();

2. tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);

3. //

4. final TableColumn<MyColor, Color> colorColumn = new TableColumn<>("Couleur");

5. colorColumn.setCellValueFactory(new PropertyValueFactory<>("color"));

6. final TableColumn<MyColor, Double> redColumn = new TableColumn<>("Rouge");

7. redColumn.setCellValueFactory(new PropertyValueFactory<>("red"));

8. final TableColumn<MyColor, Double> greenColumn = new TableColumn<>("Vert");

9. greenColumn.setCellValueFactory(new PropertyValueFactory<>("green"));

10. final TableColumn<MyColor, Double> blueColumn = new TableColumn<>("Bleu");

11. blueColumn.setCellValueFactory(new PropertyValueFactory<>("blue"));

12. tableView.getColumns().setAll(colorColumn, redColumn, greenColumn, blueColumn);

13. //

14. final StackPane root = new StackPane();

15. root.getChildren().add(tableView);

16. final Scene scene = new Scene(root, 500, 250);

17. primaryStage.setTitle("TableView");

18. primaryStage.setScene(scene);

19. ////////////////////////////////////////////////////////////////////////

20. tableView.getItems().setAll(new MyColor(Color.WHITE), new MyColor(Color.BLACK),

21. new MyColor(Color.GOLD),

22. new MyColor(Color.RED), new MyColor(Color.GREEN), new MyColor(Color.BLUE),

23. new MyColor(Color.MAGENTA), new MyColor(Color.YELLOW), new MyColor(Color.CYAN)

24. );

Ce qui nous donne le résultat suivant :

Figure 37 - Un affichage de couleurs pas très coloré…

Pour le moment, même si nous récupérons correctement les valeurs stockées, nous avons uniquement du texte qui s'affiche. Nous allons commencer par essayer d'afficher un rectangle de couleur dans la première colonne. Nous allons donc définir une nouvelle cellule capable de dessiner un rectangle coloré.

Fichier test.ColorTableCell.java, code JDK7

1. package test;

3. import javafx.scene.control.TableCell;

4. import javafx.scene.control.TableColumn;

5. import javafx.scene.layout.Region;

6. import javafx.scene.paint.Color;

7. import javafx.util.Callback;

9. public final class ColorTableCell<T> extends TableCell<T, Color> {

11. private final Region graphic = new Region();

13. public ColorTableCell() {

14. graphic.setId("graphic");

15. }

17. @Override

18. protected void updateItem(Color value, boolean empty) {

19. super.updateItem(value, empty);

20. setText(null);

21. String style = null;

22. if (value != null && !empty) {

23. final int red = (int) (value.getRed() * 255);

24. final int green = (int) (value.getGreen() * 255);

25. final int blue = (int) (value.getBlue() * 255);

26. style = String.format("-fx-background-color: rgb(%d, %d, %d)", red, green, blue);

27. }

28. graphic.setStyle(style);

29. setGraphic(graphic);

30. }

32. /**

33. * Fabrique statique.

34. */

35. public static <T> Callback<TableColumn<T, Color>, TableCell<T, Color>> forTableColumn() {

36. return new Callback<TableColumn<T, Color>, TableCell<T, Color>>() {

38. @Override

39. public TableCell<T, Color> call(TableColumn<T, Color> param) {

40. return new ColorTableCell<>();

41. }

42. };

43. }

44. }

Fichier test.ColorTableCell.java, code JDK8

1. package test;

3. import javafx.scene.control.TableCell;

4. import javafx.scene.control.TableColumn;

5. import javafx.scene.layout.Region;

6. import javafx.scene.paint.Color;

7. import javafx.util.Callback;

9. public final class ColorTableCell<T> extends TableCell<T, Color> {

11. private final Region graphic = new Region();

13. public ColorTableCell() {

14. graphic.setId("graphic");

15. }

17. @Override

18. protected void updateItem(Color value, boolean empty) {

19. super.updateItem(value, empty);

20. setText(null);

21. String style = null;

22. if (value != null && !empty) {

23. final int red = (int) (value.getRed() * 255);

24. final int green = (int) (value.getGreen() * 255);

25. final int blue = (int) (value.getBlue() * 255);

26. style = String.format("-fx-background-color: rgb(%d, %d, %d)", red, green, blue);

27. }

28. graphic.setStyle(style);

29. setGraphic(graphic);

30. }

32. /**

33. * Fabrique statique.

34. */

35. public static <T> Callback<TableColumn<T, Color>, TableCell<T, Color>> forTableColumn() {

36. return (TableColumn<T, Color> tableColumn) -> new ColorTableCell<>();

37. }

38. }

Ici, nous utilisons une instance de Region en tant que graphique de la cellule. Cette région sera colorée grâce aux CSS lorsque la méthode updateItem() est appelée sur la couleur contenue dans la colonne. Nous avons également doté cette classe d'une fabrique statique ce qui permettra d'alléger un peu notre code principal.

Nous pouvons désormais invoquer notre cellule en faisant :

1. colorColumn.setCellFactory(ColorTableCell.forTableColumn());

Ceci fait, la première colonne de notre table affiche désormais des rectangles de couleur au lieu de chaines de texte :

Figure 38 - Utilisation de cellules permettant d'afficher la couleur.

Maintenant, nous allons rendre cette colonne éditable :

1. tableView.setEditable(true);

2. colorColumn.setEditable(true);

Pour le moment, il ne se passe pas grand-chose si on double-clique sur une des cellules colorées. La cellule ne passe pas en mode édition, car sa classe mère TableCell ne supporte aucun éditeur. C'est donc à nous de jouer désormais pour ajouter cette fonctionnalité. Pour éviter de trop encombrer cet article avec du code, nous allons essayer d'utiliser ici un contrôle déjà présent dans l'API plutôt que d'en créer un nouveau.

Le contrôle javafx.scene.control.ColorPicker permet de sélectionner et d'éditer une couleur : il s'agit d'une boite déroulante qui affiche un carré coloré et le nom de la couleur. Son menu surgissant affiche une présélection de couleur et il est possible de faire apparaitre une boite de dialogue contenant des options d'édition avancées. Ce contrôle semble donc tout à fait adapté pour notre besoin et nous allons essayer de l'insérer dans notre cellule lorsque celle-ci passe en mode édition.

Figure 39 - Les différents états d'un ColorPicker.

Telle quelle, la classe TableCell ne supporte aucun éditeur, mais dispose tout de même de toute l'infrastructure nécessaire pour supporter le mode édition. Plusieurs méthodes sont fournies à cet effet :

- cancelEdit() - une méthode appelée lorsque l'utilisateur annule son édition ;

- commitEdit() - une méthode appelée lorsque l'utilisateur valide son édition. Cette méthode prend en paramètre la nouvelle valeur après édition ;

- starEdit() - une méthode appelée lorsque l'utilisateur démarre son édition.

Nous allons donc surcharger ces méthodes en prenant garde à chaque fois d'appeler la super méthode. Ce faisant, lorsque l'édition démarre dans notre cellule, un ColorPicker sera inséré en tant que graphique. Lorsque l'édition est annulée ou, au contraire, lorsque l'utilisateur a validé une nouvelle couleur, nous devrons réinsérer la région colorée en tant que graphique de la cellule. De plus, lorsque l'utilisateur a choisi une nouvelle couleur, nous devons propager la nouvelle valeur.

Nous allons insérer les lignes de code suivantes dans le code de notre classe ColorTableCell :

Code JDK7

1. private ColorPicker colorPicker;

3. /**

4. * Cette méthode est appelée lorsque l'édition commence.

5. */

6. @Override

7. public void startEdit() {

8. if (!isEditable()

9. || !getTableView().isEditable()

10. || !getTableColumn().isEditable()) {

11. return;

12. }

13. super.startEdit();

14. configureEditorOn();

15. }

17. /**

18. * Cette méthode est appelée lorsque l'édition est validée.

19. */

20. @Override

21. public void commitEdit(Color value) {

22. super.commitEdit(value);

23. configureEditorOff();

24. }

26. /**

27. * Cette méthode est appelée lorsque l'édition est annulée.

28. */

29. @Override

30. public void cancelEdit() {

31. super.cancelEdit();

32. configureEditorOff();

33. }

35. /**

36. * Configuration de l'éditeur lorsque l'édition commence.

37. */

38. private void configureEditorOn() {

39. if (colorPicker == null) {

40. colorPicker = new ColorPicker();

41. colorPicker.setMaxWidth(Double.MAX_VALUE);

42. }

43. colorPicker.setValue(getItem());

44. colorPicker.valueProperty().addListener(editorValueChangeListener);

45. colorPicker.setOnKeyPressed(escHandler);

46. setGraphic(colorPicker);

47. colorPicker.arm();

48. }

50. /**

51. * Configuration de l'éditeur lorsque l'édition commence.

52. */

53. private void configureEditorOff() {

54. colorPicker.valueProperty().removeListener(editorValueChangeListener);

55. colorPicker.setOnKeyPressed(null);

56. setGraphic(graphic);

57. }

59. /**

60. * Permet de suivre les changements de valeur de l'éditeur.

61. */

62. private final ChangeListener<Color> editorValueChangeListener = new ChangeListener<Color>() {

64. @Override

65. public void changed(ObservableValue<? extends Color> observable, Color oldValue, Color newValue) {

66. commitEdit(newValue);

67. }

68. };

70. /**

71. * Permet de réagir à la touche ESC.

72. */

73. private final EventHandler<KeyEvent> escHandler = new EventHandler<KeyEvent>() {

75. @Override

76. public void handle(KeyEvent keyEvent) {

77. switch (keyEvent.getCode()) {

78. case ESCAPE:

79. cancelEdit();

80. break;

81. }

82. }

83. };

Code JDK8

1. private ColorPicker colorPicker;

4. /**

5. * Cette méthode est appelée lorsque l'édition commence.

6. */

7. @Override

8. public void startEdit() {

9. if (!isEditable()

10. || !getTableView().isEditable()

11. || !getTableColumn().isEditable()) {

12. return;

13. }

14. super.startEdit();

15. configureEditorOn();

16. }

18. /**

19. * Cette méthode est appelée lorsque l'édition est validée.

20. */

21. @Override

22. public void commitEdit(Color value) {

23. super.commitEdit(value);

24. configureEditorOff();

25. }

27. /**

28. * Cette méthode est appelée lorsque l'édition est annulée.

29. */

30. @Override

31. public void cancelEdit() {

32. super.cancelEdit();

33. configureEditorOff();

34. }

36. /**

37. * Configuration de l'éditeur lorsque l'édition commence.

38. */

39. private void configureEditorOn() {

40. if (colorPicker == null) {

41. colorPicker = new ColorPicker();

42. colorPicker.setMaxWidth(Double.MAX_VALUE);

43. }

44. colorPicker.setValue(getItem());

45. colorPicker.valueProperty().addListener(editorValueChangeListener);

46. colorPicker.setOnKeyPressed(escHandler);

47. setGraphic(colorPicker);

48. colorPicker.arm();

49. }

51. /**

52. * Configuration de l'éditeur lorsque l'édition se termine.

53. */

54. private void configureEditorOff() {

55. colorPicker.valueProperty().removeListener(editorValueChangeListener);

56. colorPicker.setOnKeyPressed(null);

57. setGraphic(graphic);

58. }

60. /**

61. * Permet de suivre les changements de valeur de l'éditeur.

62. */

63. private final ChangeListener<Color> editorValueChangeListener = (ObservableValue<? extends Color> observableValue, Color oldValue, Color newValue) -> {

64. commitEdit(newValue);

65. };

67. /**

68. * Permet de réagir à la touche ESC.

69. */

70. private final EventHandler<KeyEvent> escHandler = (KeyEvent keyEvent) -> {

71. switch (keyEvent.getCode()) {

72. case ESCAPE:

73. cancelEdit();

74. break;

75. }

76. };

Lorsque la méthode startEdit() est appelée, nous initialisons notre éditeur et ajoutons un écouteur sur sa propriété value qui permet de savoir quand l'utilisateur effectue une sélection. Nous ajoutons également un callback pour capturer la touche clavier ESC et annuler l'édition si besoin. Enfin nous remplaçons le graphique de la cellule par notre éditeur.

Lorsque la méthode cancelEdit() est appelée, nous effectuons un petit nettoyage et nous restaurons le graphique initial de la cellule, c'est-à-dire la région colorée.

Lorsque la valeur change dans l'éditeur, nous appelons la méthode commitEdit() avec la nouvelle valeur comme paramètre. Cela permettra de propager les changements jusqu'à l'objet source. Puis nous effectuons les mêmes actions qu'en cas d'annulation pour restaurer l'affichage initial.

Figure 40 - Utilisation de l'éditeur dans la table.

Désormais, si l'utilisateur double-clique sur une cellule, notre cellule passe correctement en mode édition. De plus, s'il choisit une couleur dans notre ColorPicker, le contenu de notre table est correctement mis à jour et les changements de valeurs sont également propagés aux autres colonnes. Avec un petit peu de travail côté CSS, il serait même possible de faire que l'apparence de notre éditeur soit un peu mieux intégrée avec celle de notre table, mais c'est un boulot pour un autre article…

Figure 41 - En sortie d'éditeur, la nouvelle couleur est appliquée dans la table.

Il nous est désormais possible de définir également des éditeurs customisés pour les trois autres colonnes. Cependant, ici, nous allons utiliser une approche différente : nous allons utiliser le composant javafx.scene.control.Slider en tant qu'éditeur. Étant donné que nous allons éditer la valeur « au vol » en déplaçant le bouton de la réglette, nous n'entrons jamais en mode édition. Nous allons donc utiliser une approche similaire à celle utilisée par CheckBoxTableCell : nous allons effectuer une liaison bidirectionnelle directement sur la propriété contenue dans la cellule.

1. package test;

3. import javafx.beans.binding.Bindings;

4. import javafx.beans.property.DoubleProperty;

5. import javafx.beans.value.ObservableValue;

6. import javafx.scene.Node;

7. import javafx.scene.control.Slider;

8. import javafx.scene.control.TableCell;

9. import javafx.scene.control.TableColumn;

10. import javafx.util.Callback;

12. public final class ColorComponentTableCell<T> extends TableCell<T, Double> {

14. /**

15. * La dernière propriété sur laquelle l'éditeur a été lié.

16. */

17. private ObservableValue<Number> numberProperty;

18. /**

19. * L'éditeur.

20. */

21. private Slider slider = new Slider();

23. public ColorComponentTableCell() {

24. slider.setMin(0);

25. slider.setMax(1);

26. }

28. @Override

29. protected void updateItem(Double value, boolean empty) {

30. super.updateItem(value, empty);

31. setText(null);

32. Node graphic = null;

33. if (value != null && !empty) {

34. graphic = slider;

35. // On casse la liaison avec la propriété précédente.

36. if (numberProperty instanceof DoubleProperty) {

37. slider.valueProperty().unbindBidirectional((DoubleProperty) numberProperty);

38. }

39. // On établit une liaison avec la propriété actuelle.

40. final int row = getIndex();

41. final ObservableValue observableValue = getTableColumn().getCellObservableValue(row);

42. if (observableValue instanceof DoubleProperty) {

43. numberProperty = observableValue;

44. slider.valueProperty().bindBidirectional((DoubleProperty) numberProperty);

45. }

46. // La réglette n'est pas activable si la cellule ou la colonne ou la table ne sont pas éditables.

47. slider.disableProperty().bind(Bindings.not(

48. getTableView().editableProperty().and(getTableColumn().editableProperty()).and(editableProperty())

49. ));

50. }

51. setGraphic(graphic);

52. }

54. /**

55. * Fabrique statique.

56. */

57. public static <T> Callback<TableColumn<T, Double>, TableCell<T, Double>> forTableColumn() {

58. return (TableColumn<T, Double> tableColumn) -> new ColorComponentTableCell<T>();

59. }

60. }

Il nous suffit alors d'insérer dans le code principal :

1. redColumn.setCellFactory(ColorComponentTableCell.forTableColumn());

2. greenColumn.setCellFactory(ColorComponentTableCell.forTableColumn());

3. blueColumn.setCellFactory(ColorComponentTableCell.forTableColumn());

Avec pour résultat, le fait que notre table se pare de réglettes sur les colonnes affichant les propriétés red, green et blue :

Figure 42 - Mise en place d'instances de Slider pour changer les composantes.

Si l'utilisateur bouge le bouton d'une des réglettes, la colonne color est automatiquement mise à jour avec la nouvelle valeur de la couleur. Si une nouvelle couleur est éditée dans la première colonne, les réglettes des trois autres colonnes sont automatiquement positionnées sur les bonnes valeurs.

Figure 43 - Changer les composantes modifie la couleur affichée dans la première colonne.

V-C-3-d - Lignes

Note : dans l'exemple suivant, je vais utiliser la classe java.util.Optional disponible à partir du JDK8 ainsi que l'API permettant d'activer une pseudoclasse sur un sélecteur CSS. Cette API n'est publique qu'à partir de JavaFX 8. Le code qui sera présenté dans cette section sera donc au format JDK 8.

Il arrive que dans certains cas, il soit nécessaire de définir la fabrique qui retourne une ligne de la table, par exemple :

- on veut afficher une couleur de fond sur une ligne entière pour montrer des données importantes ou indiquer un changement d'état ;

- on veut gérer un menu contextuel sur cette ligne.

Il est tout à fait possible de gérer ces cas de figure via la méthode habituelle en définissant chaque fabrique de cellules de chaque colonne. Cependant cette méthode n'est pas très efficace : elle oblige à effectuer beaucoup de duplications de code dans chaque cellule ainsi qu'au niveau des CSS.

Dans notre exemple nous allons créer un gestionnaire de téléchargements. Tout d'abord créons une classe Download qui va représenter un téléchargement :

Fichier test.Download.java

1. package test;

3. import java.io.File;

4. import java.net.URL;

5. import javafx.beans.property.ReadOnlyLongProperty;

6. import javafx.beans.property.ReadOnlyLongWrapper;

8. public final class Download {

10. private final URL url;

11. private final File localFile;

13. public Download(URL url, File localFolder) {

14. this.url = url;

15. localFile = new File(localFolder, getName());

16. }

18. public String getCodeBase() {

19. final String path = url.getFile().substring(0, url.getFile().lastIndexOf('/'));

20. final String base = url.getProtocol() + "://" + url.getHost() + path;

21. return base;

22. }

24. public String getName() {

25. final String path = url.getFile().substring(url.getFile().lastIndexOf('/') + 1, url.getFile().length());

26. return path;

27. }

29. public File getLocalFile() {

30. return localFile;

31. }

33. private final ReadOnlyLongWrapper size = new ReadOnlyLongWrapper(this, "size", -1);

35. public long getSize() {

36. return size.get();

37. }

39. void setSize(final long value) {

40. size.set(value);

41. }

43. public final ReadOnlyLongProperty sizeProperty() {

44. return size.getReadOnlyProperty();

45. }

47. private final ReadOnlyLongWrapper progress = new ReadOnlyLongWrapper(this, "progress", -1);

49. public final long getProgress() {

50. return progress.get();

51. }

53. void setProgress(final long value) {

54. progress.set(value);

55. }

57. public final ReadOnlyLongProperty progressProperty() {

58. return progress.getReadOnlyProperty();

59. }

60. }

Dans cette classe, nous avons diverses propriétés publiques en lecture seule telles que la taille maximale à télécharger ou encore la progression actuelle dans le téléchargement. Cependant, il existe des setters visibles par les autres classes du package, entre autres notre gestionnaire de téléchargements qui se chargera de rapatrier les données sur l'ordinateur local. Ces valeurs sont initialement à -1, en effet notre gestionnaire de téléchargement devra accéder au site distant pour par exemple trouver la taille du fichier à récupérer et ensuite procéder au transfert des octets.

Nous allons maintenant créer une table pour afficher les téléchargements :

1. final TableView<Download> tableView = new TableView<>();

2. tableView.setEditable(false);

3. tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);

4. // Colonne non.

5. final TableColumn<Download, String> nameColumn = new TableColumn<>("Nom");

6. nameColumn.setCellValueFactory((TableColumn.CellDataFeatures<Download, String> p) -> {

7. final Download download = p.getValue();

8. return new SimpleStringProperty(download.getName());

9. });

10. // Colonne site.

11. final TableColumn<Download, String> baseCodeColumn = new TableColumn<>("Site");

12. baseCodeColumn.setCellValueFactory((TableColumn.CellDataFeatures<Download, String> p) -> {

13. final Download download = p.getValue();

14. return new SimpleStringProperty(download.getCodeBase());

15. });

16. // Colonne fichier local

17. final TableColumn<Download, String> outputColumn = new TableColumn<>("Fichier local");

18. outputColumn.setCellValueFactory((TableColumn.CellDataFeatures<Download, String> p) -> {

19. final Download download = p.getValue();

20. return new SimpleStringProperty(download.getLocalFile().getAbsolutePath());

21. });

22. // Colonne taille.

23. final TableColumn<Download, Number> sizeColumn = new TableColumn<>("Taille");

24. sizeColumn.setCellValueFactory((TableColumn.CellDataFeatures<Download, Number> p) -> {

25. final Download download = p.getValue();

26. return download.sizeProperty();

27. });

28. sizeColumn.setCellFactory((TableColumn<Download, Number> p) -> {

29. return new TableCell<Download, Number>() {

31. @Override

32. protected void updateItem(Number value, boolean empty) {

33. super.updateItem(value, empty);

34. String text = null;

35. if (value != null && !empty) {

36. text = (value.doubleValue() < 0) ? "Calcul en cours..." : value.toString();

37. }

38. setText(text);

39. }

40. };

41. });

42. // Colonne progression.

43. final TableColumn<Download, Number> progressColumn = new TableColumn<>("Progression");

44. progressColumn.setCellValueFactory((TableColumn.CellDataFeatures<Download, Number> p) -> {

45. final Download download = p.getValue();

46. final DoubleBinding progressProperty = new DoubleBinding() {

47. {

48. bind(download.progressProperty(), download.sizeProperty());

49. }

51. @Override

52. public void dispose() {

53. unbind(download.progressProperty(), download.sizeProperty());

54. }

56. @Override

57. protected double computeValue() {

58. long progress = download.getProgress();

59. long size = download.getSize();

60. double result = (size < 0 || progress < 0) ? -1 : progress / (double) size;

61. return result;

62. }

63. };

64. return progressProperty;

65. });

66. progressColumn.setCellFactory((TableColumn<Download, Number> p) -> {

67. return new TableCell<Download, Number>() {

68. private final ProgressBar progressBar = new ProgressBar();

70. {

71. progressBar.setMaxWidth(Double.MAX_VALUE);

72. }

74. @Override

75. protected void updateItem(Number value, boolean empty) {

76. super.updateItem(value, empty);

77. String text = null;

78. Node graphic = null;

79. if (value != null && !empty) {

80. if (value.doubleValue() >= 1) {

81. final Label label = new Label("Terminé !");

82. label.setMaxWidth(Double.MAX_VALUE);

83. HBox.setHgrow(label, Priority.ALWAYS);

84. final Button button = new Button("Ouvrir");

85. button.setStyle("-fx-padding: 3; -fx-font-size: 0.8em;");

86. button.setOnAction((ActionEvent t) -> {

87. final Download download = getTableView().getItems().get(getIndex());

88. final File file = download.getLocalFile();

89. getHostServices().showDocument(file.toURI().toString());

90. });

91. final HBox content = new HBox(label, button);

92. content.setAlignment(Pos.BASELINE_LEFT);

93. graphic = content;

95. } else {

96. progressBar.setProgress(value.doubleValue());

97. graphic = progressBar;

98. }

99. }

100. setText(text);

101. setGraphic(graphic);

102. }

103. };

104. });

105. // Peuplement des colonnes de la table.

106. tableView.getColumns().setAll(nameColumn, baseCodeColumn, sizeColumn, outputColumn, progressColumn);

107. //

108. final StackPane root = new StackPane();

109. root.getChildren().add(tableView);

110. final Scene scene = new Scene(root, 850, 300);

111. primaryStage.setTitle("TableView");

112. primaryStage.setScene(scene);

113. primaryStage.show();

Ici, la colonne qui renseigne la taille affiche « Calcul en cours… » tant que cette dernière n'a pas été initialisée par le gestionnaire de téléchargements. Notre colonne qui affiche la progression contient une cellule qui affiche une barre de progression montrant l'avancement du téléchargement de notre fichier. Une fois le téléchargement accompli, elle affichera un label « Terminé » ainsi qu'un bouton permettant d'ouvrir le fichier local.

Nous allons maintenant peupler la table avec des téléchargements à effectuer :

1. // Peuplement de la table.

2. final String[] values = {

3. "http://cygwin.com/setup-x86.exe",

4. "http://cygwin.com/setup-x86_64.exe",

5. "http://cran.r-project.org/src/base/R-3/R-3.1.0.tar.gz"

6. };

7. final File downloadFolder = new File(System.getProperty("user.home"), "Downloads");

8. for (String value : values) {

9. try {

10. final URL url = new URL(value);

11. final Download download = new Download(url, downloadFolder);

12. tableView.getItems().add(download);

13. } catch (MalformedURLException ex) {

14. Logger.getLogger(Test_TableView.class.getName()).log(Level.SEVERE, ex.getMessage(), ex);

15. }

16. }

Désormais notre table prend vie :

Figure 44 - Colonne avec barre de progression.

Coder un vrai gestionnaire de téléchargements serait hors de propos pour ce didacticiel, donc je vais me contenter de coder ici un timer qui va simuler le téléchargement de la première ligne de la table.

1. // Simulation du téléchargement.

2. final PauseTransition simulatorTimer = new PauseTransition(Duration.seconds(5));

3. simulatorTimer.setOnFinished((ActionEvent t) -> {

4. final Download download = tableView.getItems().get(0);

5. // Initialisation de la taille et de la progréssion.

6. download.setSize(300000);

7. download.setProgress(0);

8. final SequentialTransition downloadAnimation = new SequentialTransition();

9. final PauseTransition downloadAction = new PauseTransition(Duration.millis(150));

10. downloadAction.setOnFinished((final ActionEvent actionEvent) -> {

11. final long progress = download.getProgress();

12. final long size = download.getSize();

13. final int increment = 5000;

14. if (progress + increment >= size) {

15. download.setProgress(size);

16. downloadAnimation.stop();

17. } else {

18. download.setProgress(progress + increment);

19. }

20. });

21. downloadAnimation.getChildren().setAll(downloadAction);

22. downloadAnimation.setCycleCount(PauseTransition.INDEFINITE);

23. downloadAnimation.play();

24. });

25. simulatorTimer.play();

Après une pause de cinq secondes lors de l'affichage de la fenêtre, nous initialisons la taille de notre premier fichier à 300 000 octets puis nous lançons une animation qui va boucler en incrémentant la progression jusqu'à atteindre la taille maximale.

Lorsque le faux téléchargement est en cours, nous obtenons l'apparence suivante :

Figure 45 - Téléchargement en cours.

Puis lorsque la simulation est finie :

Figure 46 - Téléchargement terminé.

Nous allons maintenant effectuer les deux modifications suivantes :

- lorsqu'un téléchargement est fini, le fond de la ligne passe en vert et les labels qui s'y trouvent deviennent gras ;

- nous allons implémenter un menu contextuel qui, lors d'un clic droit sur la ligne, permettra d'effectuer des actions telles que retirer un téléchargement de la liste, le mettre en pause ou encore le relancer…

Pour cela, définissons la classe suivante qui servira de cellule pour chaque ligne :

Fichier test.DownloadTableRow.java

1. package test;

3. import java.net.URL;

4. import java.util.Optional;

5. import javafx.animation.PauseTransition;

6. import javafx.beans.InvalidationListener;

7. import javafx.beans.Observable;

8. import javafx.css.PseudoClass;

9. import javafx.event.ActionEvent;

10. import javafx.scene.control.ContextMenu;

11. import javafx.scene.control.MenuItem;

12. import javafx.scene.control.SeparatorMenuItem;

13. import javafx.scene.control.TableRow;

14. import javafx.util.Duration;

16. public final class DownloadTableRow extends TableRow<Download> {

18. /**

19. * La valeur actuelle.

20. * Ici, on utilise la classe Optional donc value n'est jamais null.

21. */

22. private Optional<Download> value = Optional.empty();

23. private final ContextMenu contextMenu = new ContextMenu();

25. public DownloadTableRow() {

26. super();

27. getStyleClass().add("download-table-row");

28. // Chargement de la feuille de style.

29. final URL cssURL = getClass().getResource("DownloadTableRow.css");

30. getStylesheets().add(cssURL.toExternalForm());

31. //

32. final MenuItem restartItem = new MenuItem("Relancer");

33. restartItem.setId("restartItem");

34. restartItem.setOnAction((final ActionEvent actionEvent) -> value.ifPresent((final Download d) -> System.out.printf("Relancer %s", d.getName()).println()));

35. final MenuItem pauseItem = new MenuItem("Mettre en pause");

36. pauseItem.setId("pauseItem");

37. pauseItem.setOnAction((final ActionEvent actionEvent) -> value.ifPresent((final Download d) -> System.out.printf("Mise en pause de %s", d.getName()).println()));

38. final MenuItem resumeItem = new MenuItem("Reprendre");

39. resumeItem.setId("resumeItem");

40. resumeItem.setOnAction((final ActionEvent actionEvent) -> value.ifPresent((final Download d) -> System.out.printf("Reprise de %s", d.getName()).println()));

41. final MenuItem deleteItem = new MenuItem("Retirer de la liste");

42. deleteItem.setId("deleteItem");

43. deleteItem.setOnAction((final ActionEvent actionEvent) -> value.ifPresent((final Download d) -> System.out.printf("Suppression de %s", d.getName()).println()));

44. //

45. contextMenu.setId("contextMenu");

46. contextMenu.getItems().add(restartItem);

47. contextMenu.getItems().add(new SeparatorMenuItem());

48. contextMenu.getItems().add(pauseItem);

49. contextMenu.getItems().add(resumeItem);

50. contextMenu.getItems().add(new SeparatorMenuItem());

51. contextMenu.getItems().add(deleteItem);

52. }

54. @Override

55. protected void updateItem(final Download download, final boolean empty) {

56. super.updateItem(download, empty);

57. value.ifPresent((final Download d) -> {

58. // Retirer l'écouteur de l'ancienne valeur.

59. d.progressProperty().removeListener(invalidationListener);

60. setContextMenu(null);

61. });

62. value = Optional.ofNullable(download);

63. value.ifPresent((final Download d) -> {

64. // Placer l'écouteur sur la nouvelle valeur.

65. d.progressProperty().addListener(invalidationListener);

66. setContextMenu(contextMenu);

67. });

68. requestUpdateStyle();

69. }

71. //

72. private final InvalidationListener invalidationListener = (final Observable observable) -> requestUpdateStyle();

74. private PauseTransition styleWaitTimer;

75. private final Duration styleWaitDuration = Duration.millis(350);

77. // Démarrer une mise à jour du style.

78. // Comme cette méthode va être appelée régulièrement, on bufferise les appels.

79. private void requestUpdateStyle() {

80. if (styleWaitTimer == null) {

81. styleWaitTimer = new PauseTransition(styleWaitDuration);

82. styleWaitTimer.setOnFinished((final ActionEvent actionEvent) -> {

83. styleWaitTimer = null;

84. updateStyle();

85. });

86. styleWaitTimer.playFromStart();

87. }

88. }

90. // Mise à jour du style.

91. private void updateStyle() {

92. final PseudoClass finishedPseudoClass = PseudoClass.getPseudoClass("finished");

93. pseudoClassStateChanged(finishedPseudoClass, false);

94. value.ifPresent((final Download d) -> {

95. final long progress = d.getProgress();

96. final long size = d.getSize();

97. final boolean enableStyle = (size != -1) && (progress == size);

98. pseudoClassStateChanged(finishedPseudoClass, enableStyle);

99. });

100. }

101. }

Ainsi que le fichier CSS suivant :

Fichier test.DownloadTableRow.java

1. .download-table-row {

2. }

3. .download-table-row:finished {

4. -base-color: greenyellow;

5. -fx-font-weight: bold;

6. -fx-background-color: linear-gradient(to bottom, derive(-base-color, 50%) 0%, derive(-base-color, -10%) 100%);

7. }

Et enfin, nous activons notre nouvelle cellule de ligne dans la table en spécifiant la fabrique appropriée :

1. tableView.setRowFactory((final TableView<Download> p) -> new DownloadTableRow());

Une fois de plus, l'ajout d'une simple ligne de code peut changer drastiquement le fonctionnement et l'apparence de notre table !

Figure 47 - Intégration de nos modifications sur les lignes.

VI - Arbres

Cette seconde série de contrôles utilise une unique référence pour stocker la racine de l'arborescence à afficher (même dans le cas d'une TreeTableView). Ces contrôles disposent donc tous de la propriété suivante :

- root - un objet de type TreeItem<V> contenant le nœud racine de l'arborescence à afficher.

VI-A - Stockage des données

La classe javafx.scene.control.TreeItem<V> est la classe de base permettant de stocker des éléments dans une arborescence graphique. Cette classe n'a rien de graphique en soi ; elle sert juste de conteneur pour construire l'arborescence d'objets V. Par la suite, cette arborescence sera affichée dans le contrôle via des cellules de manière similaire à ce que nous avons vu jusqu'à présent.

À l'heure actuellement, cette classe dispose d'une seule classe fille : javafx.scene.control.CheckBoxTreeItem<V>. Cette seconde classe est une classe prête à l'emploi destinée à produire des arbres contenant des cases à cocher (CheckBox) qui pourront être sélectionnées ou désélectionnées par l'utilisateur. Il faut l'utiliser en conjonction avec la classe cellule CheckBoxTreeCell<V>.

VI-A-1 - Propriétés

Les propriétés importantes de la classe TreeItem<V> sont :

- expanded - indique si cette branche a été déroulée pour afficher ses enfants ;

- graphic - un nœud graphique qui sera affiché dans la cellule chargée du rendu de cette branche ;

- leaf - une propriété en lecture seule qui retourne true si cet objet est une feuille dans l'arborescence ;

- parent - une propriété en lecture seule qui contient une référence sur la branche parente de cet objet ;

- value - cette propriété contient une référence vers l'objet de type V contenu dans cette branche.

Les propriétés importantes de la classe CheckBoxTreeItem<V> sont :

- independent - cette propriété permet de spécifier si le fait de cocher cet objet influe sur l'état de son parent ou de ses enfants ;

- indeterminate - cette propriété spécifie si l'objet supporte l'état non déterminé : les CheckBox dans JavaFX peuvent être configurées pour avoir trois états (sélectionné, non sélectionné ou indéterminé) au lieu de seulement deux (sélectionné ou non sélectionné) ;

- selected - cette propriété spécifie si l'objet est sélectionné ou non.

VI-A-2 - Arbre dynamique

Par défaut, TreeItem<V> demande à créer toute l'arborescence d'objets V en mémoire avant de l'afficher à l'écran. Cependant, dans certains cas, on peut être amené à charger un arbre dynamiquement, c'est-à-dire à le construire en mémoire au fur et à mesure qu'on étend ses branches plutôt que de le charger intégralement au départ.

C'est le cas, par exemple, d'un explorateur de fichiers avec une vue arborescente : une branche représente généralement un répertoire, un disque ou encore un point de montage, tandis qu'une feuille est généralement un fichier, un lien ou un périphérique. Il est hors de question de construire un arbre qui représente intégralement le contenu de tout un système de fichiers qui s'étale sur un ou plusieurs disques ou même sur le réseau :

- le coût en utilisation mémoire serait trop important pour stocker l'intégralité des fichiers et répertoires, chacun occupant exactement une instance de TreeItem ;

- Le temps même des opérations nécessaires pour parcourir ou indexer le système de fichiers dans son intégralité serait prohibitif.

Pour ce type d'utilisation, on utilisera donc une construction dynamique de l'arbre : lorsqu'une branche est étendue, on génère ses branches et feuilles filles. Lorsqu'une branche est repliée, on effectue un nettoyage et on vide la liste de ses enfants.

Malheureusement, ici la propriété leaf de la classe TreeItem est en lecture seule, son setter est en accès privé de même que le membre qui stocke la propriété. La documentation de la classe nous indique seulement qu'il est possible de surcharger la méthode isLeaf(), ce qui est un peu contraire au principe d'utilisation des propriétés. Il n'existe donc pas de bonne solution en l'état pour les API javaFX 2.x et 8.

Un hack simple et qui fonctionne (pour le moment) consiste à surcharger la méthode isLeaf() et à ajouter un écouteur sur la propriété expanded de notre TreeItem et ensuite d'agir en conséquence suivant les changements d'état de cette propriété. Un squelette pour créer un tel nœud « dynamique » pour TreeView et TreeTableView serait similaire à :

Code JDK7

1. public abstract class DynamicTreeItem<T> extends TreeItem<T> {

3. private final Callback<T, Boolean> hasChildren;

5. /**

6. * Crée une nouvelle instance qui sera une feuille.

7. * @param value La valeur.

8. */

9. public DynamicTreeItem(final T value) {

10. this(value, null);

11. }

13. /**

14. * Crée une nouvelle instance.

15. * @param value La valeur.

16. * @param hasChildren Un callback qui permet de déterminer si la valeur a des enfants. Peut être null.

17. */

18. public DynamicTreeItem(final T value, final Callback<T, Boolean> hasChildren) {

19. super(value);

20. this.hasChildren = hasChildren;

21. expandedProperty().addListener(expandedChangeListener);

22. }

24. /**

25. * Génère les enfants.

26. */

27. protected abstract void generateChildren();

29. /**

30. * Libère les enfants.

31. */

32. protected abstract void clearChildren();

34. /**

35. * Écouteur qui permet de savoir si la branche est étendue ou repliée.

36. */

37. private final ChangeListener<Boolean> expandedChangeListener = new ChangeListener<Boolean>() {

39. @Override

40. public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {

41. if (newValue) {

42. // Extension de la branche, on génère les enfants.

43. generateChildren();

44. } else {

45. // Contraction de la branche, on libère les enfants.

46. clearChildren();

47. }

48. }

49. };

51. @Override

52. public boolean isLeaf() {

53. final T value = getValue();

54. return (value == null || hasChildren == null) ? true : !hasChildren.call(value);

55. }

56. }

Code JDK8

1. public abstract class DynamicTreeItem<T> extends TreeItem<T> {

3. private final Callback<T, Boolean> hasChildren;

5. /**

6. * Crée une nouvelle instance qui sera une feuille.

7. * @param value La valeur.

8. */

9. public DynamicTreeItem(final T value) {

10. this(value, null);

11. }

13. /**

14. * Crée une nouvelle instance.

15. * @param value La valeur.

16. * @param hasChildren Un callback qui permet de déterminer si la valeur a des enfants. Peut être null.

17. */

18. public DynamicTreeItem(final T value, final Callback<T, Boolean> hasChildren) {

19. super(value);

20. this.hasChildren = hasChildren;

21. expandedProperty().addListener(expandedChangeListener);

22. }

24. /**

25. * Génère les enfants.

26. */

27. protected abstract void generateChildren();

29. /**

30. * Libère les enfants.

31. */

32. protected abstract void clearChildren();

34. private final ChangeListener<Boolean> expandedChangeListener = (ObservableValue<? extends Boolean> observableValue, Boolean oldValue, Boolean newValue) -> {

35. if (newValue) {

36. // Extension de la branche, on génère les enfants.

37. generateChildren();

38. } else {

39. // Contraction de la branche, on libère les enfants.

40. clearChildren();

41. }

42. };

44. @Override

45. public boolean isLeaf() {

46. final T value = getValue();

47. return (value == null || hasChildren == null) ? true : !hasChildren.call(value);

48. }

49. }

Même ainsi, puisque notre surcharge de la méthode isLeaf() est « figée » il n'est pas aisé d'avoir un objet dont l'état leaf varie au fil du temps, car l'API actuelle ne permet pas de faire remonter cette information vers l'affichage de manière dynamique.

Dans notre exemple d'explorateur de fichiers : il vaut mieux supposer que les répertoires sont toujours des branches et ce de manière à pouvoir tout le temps explorer leur contenu. On ne peut pas avoir un répertoire qui est une branche quand il contient des fichiers, mais qui devient une feuille lorsque le dernier fichier qu'il contient est effacé puis qui redevient une branche quand on y crée de nouveaux fichiers.

Une requête a été déposée sur le Jira de JavaFX (enregistrement obligatoire) concernant la modification de l'API TreeView pour permettre l'accès à la propriété leaf de manière plus classique (via un setter ou une propriété en lecture-écriture). Cependant le correctif, s'il survient un jour, est actuellement ciblé sur JavaFX 9.

VI-B - TreeView<V>

Le contrôle TreeView<V> est donc destiné à afficher une arborescence d'objets de type V à l'écran. Dans la configuration par défaut, la racine de l'arbre apparait en haut de l'affichage :

Figure 48 - Affichage standard d'un TreeView.

Il est possible de configurer ce contrôle de manière à cacher le nœud racine de l'arbre ce qui donne l'illusion que l'arbre graphique dispose de plusieurs racines :

Figure 49 - Affichage en cachant le nœud racine.

Dans un TreeView, les cellules sont agencées les unes sous les autres comme dans une ListView configurée pour une orientation verticale. Chaque cellule est étendue pour remplir tout l'espace horizontal occupé par l'arbre graphique. Cependant, dans une cellule, le contenu peut être décalé et décoré pour indiquer le niveau de branche sur laquelle se trouve la donnée :

Figure 50 - Cellules dans un TreeView

VI-B-1 - Propriétés

Les propriétés importantes de ce contrôle sont :

- cellFactory - de type Callback<TreeView<V>, TreeCell<V>> ; une fabrique à cellules qui est utilisée pour produire une cellule destinée à afficher une ligne de l'arbre pour afficher chaque valeur visible ;

- editable - permet de spécifier si les lignes de l'arbre peuvent être éditées ;

- editingItem - une propriété en lecture seule qui retourne la branche en train d'être éditée ;

- expandedItemCount - une propriété en lecture seule qui retourne la somme du nombre de branches étendues et leurs enfants. Le nombre retourné est le nombre total de nœuds potentiellement visibles dans l'arbre ;

- fixedCellSize - permet de forcer une hauteur commune à toutes les cellules au lieu de calculer la hauteur cellule par cellule. Cette propriété est destinée à améliorer les performances de mise en page. JDK8 ou supérieur ;

- onEditCancel - un callback appelé lorsque l'utilisateur annule son édition ;

- onEditCommit - un callback appelé lorsque l'utilisateur valide son édition ;

- onEditStart - un callback appelé lorsque l'utilisateur démarre son édition ;

- onScrollTo - un callback appelé lorsque la méthode scrollTo(indice) est invoquée. JDK8 ou supérieur ;

- root - un objet de type TreeItem<V> contenant le nœud racine de l'arborescence ;

- selectionModel - gère la sélection dans l'arbre. Généralement, on utilisera le modèle par défaut ;

- showRoot - spécifie si la racine de l'arborescence doit être affichée ou non.

VI-B-2 - Apparence de la flèche

Il est possible de modifier l'apparence de la flèche qui permet de déplier ou de replier une branche en utilisant les CSS. De cette manière, on peut changer la couleur ou même la forme utilisée en spécifiant une nouvelle forme SVG ; il est également possible de remplacer le contenu du nœud par une image.

Par exemple, la feuille de style suivante remplace la flèche habituellement présente par deux images. L'icône en forme de sera affichée quand une branche peut être dépliée tandis que l'icône en forme de sera affichée quand une branche peut être repliée.

Fichier test.TreeView-DisclosureIcon.css

1. .tree-cell  > .tree-disclosure-node > .arrow {

2. -fx-background-image: url("treecell-expand.png");

3. -fx-padding: 4.5;

4. -fx-shape: null;

5. }

6. .tree-cell:expanded  > .tree-disclosure-node > .arrow {

7. -fx-background-image: url("treecell-contract.png");

8. -fx-rotate: 0;

9. }

Ensuite, nous attachons la feuille de style à notre scène. Les deux images et le fichier CSS sont ici placés dans le même package que le code de lancement de l'application.

final URL cssURL = getClass().getResource("TreeView-DisclosureIcon.css");
scene.getStylesheets().add(cssURL.toExternalForm());

Désormais la flèche noire est remplacée par deux icônes et  :

Figure 51 - Changement de l'affichage des branches.

VI-B-3 - En pratique

Nous allons maintenant réaliser une liste de contacts pour simuler le genre d'affichage qu'on peut trouver dans des logiciels de messagerie instantanée ou de VoIP, par exemple, dans Skype ou encore Yahoo! Messenger.

Dans un vrai logiciel, la liste de nos contacts sera transmise par notre serveur, mais ici nous allons la lire depuis un fichier texte placé dans le projet. Commençons par définir le fichier contenant nos contacts ainsi que leurs catégories.

Fichier test.contacts.txt

1. Amis

2. Cécile|AVAILABLE|Il fait beau aujourd'hui !|avatar4.png

3. Marthy|AVAILABLE|Matin calin...|avatar7.png

4. Boulot

5. Henri Durant|BUSY|Encore en réunion...|avatar3.png

6. Nadhia|OFFLINE|En vacances !|avatar2.png

7. Contacts Facebook

8. Jack25|AWAY|idle|avatar5.png

9. Contacts Skype

10. Claude|OFFLINE|Dans le cirage après la teuf d'hier soir !|avatar6.png

11. Carnet d'adresses

12. Maman|AWAY|On est parti faire les courses.|avatar1.png

13. Papa|OFFLINE|Ta mère a voulu qu'on aille faire les courses...|avatar1.png

Nous avons également besoin de définir les classes nécessaires au stockage en mémoire. La classe Entity est une classe abstraite parente des classes :

- Entity.Group - la classe qui sert à définir les catégories de classement des contacts ;

- Entity.Contact - la classe qui sert à définir un contact. Elle contient diverses informations comme le message du jour ou l'avatar du contact. Cette classe contient également l'énumération Entity.Contact.State qui sert à définir le statut en ligne.

Fichier test.Entity.java, code JDK7

1. package test;

3. import java.util.LinkedList;

4. import javafx.beans.property.ListProperty;

5. import javafx.beans.property.ObjectProperty;

6. import javafx.beans.property.ReadOnlyIntegerProperty;

7. import javafx.beans.property.ReadOnlyIntegerWrapper;

8. import javafx.beans.property.ReadOnlyObjectProperty;

9. import javafx.beans.property.ReadOnlyObjectWrapper;

10. import javafx.beans.property.ReadOnlyStringProperty;

11. import javafx.beans.property.ReadOnlyStringWrapper;

12. import javafx.beans.property.SimpleListProperty;

13. import javafx.beans.property.SimpleObjectProperty;

14. import javafx.beans.property.SimpleStringProperty;

15. import javafx.beans.property.StringProperty;

16. import javafx.beans.value.ChangeListener;

17. import javafx.beans.value.ObservableValue;

18. import javafx.collections.FXCollections;

19. import javafx.collections.ListChangeListener;

20. import javafx.collections.ObservableList;

21. import javafx.scene.image.Image;

23. public abstract class Entity {

25. private static int ID_GENERATOR = 0;

27. public Entity() {

28. id.set(String.valueOf(ID_GENERATOR++));

29. }

31. private final ReadOnlyStringWrapper id = new ReadOnlyStringWrapper(this, "id");

33. public String getId() {

34. return id.get();

35. }

37. public ReadOnlyStringProperty idProperty() {

38. return id.getReadOnlyProperty();

39. }

41. private final ReadOnlyObjectWrapper<Group> parent = new ReadOnlyObjectWrapper<>(this, "parent");

43. public final Group getParent() {

44. return parent.get();

45. }

47. protected final void setParent(final Group value) {

48. parent.set(value);

49. }

51. public final ReadOnlyObjectProperty<Group> parentProperty() {

52. return parent.getReadOnlyProperty();

53. }

55. private final StringProperty displayName = new SimpleStringProperty(this, "displayName");

57. public final String getDisplayName() {

58. return displayName.get();

59. }

61. public final void setDisplayName(final String value) {

62. displayName.set(value);

63. }

65. public final StringProperty displayNameProperty() {

66. return displayName;

67. }

69. public static class Group extends Entity {

71. public Group() {

72. getChildren().addListener(new ListChangeListener<Contact>() {

74. @Override

75. public void onChanged(ListChangeListener.Change<? extends Contact> change) {

76. while (change.next()) {

77. for (Contact removed : change.getRemoved()) {

78. removed.setParent(null);

79. removed.stateProperty().removeListener(stateChangeListener);

80. if (removed.getState() != Contact.State.OFFLINE) {

81. connected.set(connected.get() - 1);

82. }

83. }

84. for (Contact added : change.getAddedSubList()) {

85. added.setParent(Group.this);

86. if (added.getState() != Contact.State.OFFLINE) {

87. connected.set(connected.get() + 1);

88. }

89. added.stateProperty().addListener(stateChangeListener);

90. }

91. }

92. }

93. });

94. }

96. private final ListProperty<Contact> children = new SimpleListProperty<>(FXCollections.observableList(new LinkedList<Contact>()));

98. public final ObservableList<Contact> getChildren() {

99. return children;

100. }

102. private final ReadOnlyIntegerWrapper connected = new ReadOnlyIntegerWrapper(this, "connected");

104. public final int getConnected() {

105. return connected.get();

106. }

108. public ReadOnlyIntegerProperty connectedProperty() {

109. return connected.getReadOnlyProperty();

110. }

112. private final ChangeListener<Contact.State> stateChangeListener = new ChangeListener<Contact.State>() {

114. @Override

115. public void changed(ObservableValue<? extends Contact.State> observableValue, Contact.State oldValue, Contact.State newValue) {

116. if (newValue == Contact.State.OFFLINE) {

117. connected.set(connected.get() - 1);

118. } else if (oldValue == Contact.State.OFFLINE) {

119. connected.set(connected.get() + 1);

120. }

121. }

122. };

123. }

125. public static class Contact extends Entity {

127. public enum State {

129. AVAILABLE,

130. AWAY,

131. BUSY,

132. OFFLINE;

133. }

135. private final StringProperty statusMessage = new SimpleStringProperty(this, "statusMessage");

137. public final String getStatusMessage() {

138. return statusMessage.get();

139. }

141. public final void setStatusMessage(final String value) {

142. statusMessage.set(value);

143. }

145. public final StringProperty statusMessageProperty() {

146. return statusMessage;

147. }

149. private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper(this, "state", State.OFFLINE);

151. public final State getState() {

152. return state.get();

153. }

155. public final void setState(final State value) {

156. final State v = (value == null) ? State.OFFLINE : value;

157. state.set(v);

158. }

160. public final ReadOnlyObjectProperty<State> stateProperty() {

161. return state.getReadOnlyProperty();

162. }

164. private final ObjectProperty<Image> avatar = new SimpleObjectProperty(this, "avatar");

166. public final Image getAvatar() {

167. return avatar.get();

168. }

170. public final void setAvatar(final Image value) {

171. avatar.set(value);

172. }

174. public final ObjectProperty<Image> avatarProperty() {

175. return avatar;

176. }

177. }

178. }

Fichier test.Entity.java, code JDK8

1. package test;

3. import java.util.LinkedList;

4. import javafx.beans.property.ListProperty;

5. import javafx.beans.property.ObjectProperty;

6. import javafx.beans.property.ReadOnlyIntegerProperty;

7. import javafx.beans.property.ReadOnlyIntegerWrapper;

8. import javafx.beans.property.ReadOnlyObjectProperty;

9. import javafx.beans.property.ReadOnlyObjectWrapper;

10. import javafx.beans.property.ReadOnlyStringProperty;

11. import javafx.beans.property.ReadOnlyStringWrapper;

12. import javafx.beans.property.SimpleListProperty;

13. import javafx.beans.property.SimpleObjectProperty;

14. import javafx.beans.property.SimpleStringProperty;

15. import javafx.beans.property.StringProperty;

16. import javafx.beans.value.ChangeListener;

17. import javafx.beans.value.ObservableValue;

18. import javafx.collections.FXCollections;

19. import javafx.collections.ListChangeListener;

20. import javafx.collections.ObservableList;

21. import javafx.scene.image.Image;

23. public abstract class Entity {

25. private static int ID_GENERATOR = 0;

27. public Entity() {

28. id.set(String.valueOf(ID_GENERATOR++));

29. }

31. private final ReadOnlyStringWrapper id = new ReadOnlyStringWrapper(this, "id");

33. public String getId() {

34. return id.get();

35. }

37. public ReadOnlyStringProperty idProperty() {

38. return id.getReadOnlyProperty();

39. }

41. private final ReadOnlyObjectWrapper<Group> parent = new ReadOnlyObjectWrapper<>(this, "parent");

43. public final Group getParent() {

44. return parent.get();

45. }

47. protected final void setParent(final Group value) {

48. parent.set(value);

49. }

51. public final ReadOnlyObjectProperty<Group> parentProperty() {

52. return parent.getReadOnlyProperty();

53. }

55. private final StringProperty displayName = new SimpleStringProperty(this, "displayName");

57. public final String getDisplayName() {

58. return displayName.get();

59. }

61. public final void setDisplayName(final String value) {

62. displayName.set(value);

63. }

65. public final StringProperty displayNameProperty() {

66. return displayName;

67. }

69. public static class Group extends Entity {

71. public Group() {

72. getChildren().addListener((ListChangeListener.Change<? extends Contact> change) -> {

73. while (change.next()) {

74. change.getRemoved().forEach((Contact removed) -> {

75. removed.setParent(null);

76. removed.stateProperty().removeListener(stateChangeListener);

77. if (removed.getState() != Contact.State.OFFLINE) {

78. connected.set(connected.get() - 1);

79. }

80. });

81. change.getAddedSubList().forEach((Contact added) -> {

82. added.setParent(Group.this);

83. if (added.getState() != Contact.State.OFFLINE) {

84. connected.set(connected.get() + 1);

85. }

86. added.stateProperty().addListener(stateChangeListener);

87. });

88. }

89. });

90. }

92. private final ListProperty<Contact> children = new SimpleListProperty<>(FXCollections.observableList(new LinkedList<Contact>()));

94. public final ObservableList<Contact> getChildren() {

95. return children;

96. }

98. private final ReadOnlyIntegerWrapper connected = new ReadOnlyIntegerWrapper(this, "connected");

100. public final int getConnected() {

101. return connected.get();

102. }

104. public ReadOnlyIntegerProperty connectedProperty() {

105. return connected.getReadOnlyProperty();

106. }

108. private final ChangeListener<Contact.State> stateChangeListener = (ObservableValue<? extends Contact.State> observableValue, Contact.State oldValue, Contact.State newValue) -> {

109. if (newValue == Contact.State.OFFLINE) {

110. connected.set(connected.get() - 1);

111. } else if (oldValue == Contact.State.OFFLINE) {

112. connected.set(connected.get() + 1);

113. }

114. };

116. }

118. public static class Contact extends Entity {

120. public enum State {

122. AVAILABLE,

123. AWAY,

124. BUSY,

125. OFFLINE;

126. }

128. private final StringProperty statusMessage = new SimpleStringProperty(this, "statusMessage");

130. public final String getStatusMessage() {

131. return statusMessage.get();

132. }

134. public final void setStatusMessage(final String value) {

135. statusMessage.set(value);

136. }

138. public final StringProperty statusMessageProperty() {

139. return statusMessage;

140. }

142. private final ReadOnlyObjectWrapper<State> state = new ReadOnlyObjectWrapper(this, "state", State.OFFLINE);

144. public final State getState() {

145. return state.get();

146. }

148. public final void setState(final State value) {

149. final State v = (value == null) ? State.OFFLINE : value;

150. state.set(v);

151. }

153. public final ReadOnlyObjectProperty<State> stateProperty() {

154. return state.getReadOnlyProperty();

155. }

157. private final ObjectProperty<Image> avatar = new SimpleObjectProperty(this, "avatar");

159. public final Image getAvatar() {

160. return avatar.get();

161. }

163. public final void setAvatar(final Image value) {

164. avatar.set(value);

165. }

167. public final ObjectProperty<Image> avatarProperty() {

168. return avatar;

169. }

170. }

171. }

Nous allons maintenant charger nos contacts et les afficher dans un TreeView<Entity>. Nous en profitons également pour charger les images qui serviront d'avatar à nos contacts :

1. final List<Entity> entities = new LinkedList<>();

2. try {

3. try (final InputStream input = getClass().getResourceAsStream("contacts.txt")) {

4. Entity.Group lastGroup = null;

5. try (final LineNumberReader in = new LineNumberReader(new InputStreamReader(input))) {

6. for (String line = in.readLine(); line != null; line = in.readLine()) {

7. line = line.trim();

8. if (line.isEmpty()) {

9. continue;

10. }

11. final String[] tokens = line.split("\\|");

12. if (tokens.length == 1) {

13. final Entity.Group group = new Entity.Group();

14. group.setDisplayName(tokens[0]);

15. entities.add(group);

16. lastGroup = group;

17. } else {

18. if (lastGroup == null) {

19. continue;

20. }

21. final Entity.Contact contact = new Entity.Contact();

22. contact.setDisplayName(tokens[0]);

23. contact.setState(Entity.Contact.State.valueOf(tokens[1]));

24. contact.setStatusMessage(tokens[2]);

25. final String avatarPath = "images/"+tokens[3];

26. final URL avatarURL = getClass().getResource(avatarPath);

27. final Image avatarImage = (avatarURL == null) ? null : new Image(avatarURL.toExternalForm());

28. contact.setAvatar(avatarImage);

29. lastGroup.getChildren().add(contact);

30. entities.add(contact);

31. }

32. }

33. }

34. }

35. } catch (IOException | IllegalArgumentException | IndexOutOfBoundsException ex) {

36. Logger.getLogger(getClass().getName()).log(Level.SEVERE, ex.getMessage(), ex);

37. }

38. // Peuplement de la structure de l'arbre.

39. final TreeItem<Entity> rootItem = new TreeItem<>();

40. TreeItem<Entity> groupItem = null;

41. while (!entities.isEmpty()) {

42. final Entity currentEntity = entities.remove(0);

43. final TreeItem<Entity> currentItem = new TreeItem<>(currentEntity);

44. if (currentEntity instanceof Entity.Group) {

45. rootItem.getChildren().add(currentItem);

46. currentItem.setExpanded(true);

47. groupItem = currentItem;

48. } else if (groupItem != null) {

49. groupItem.getChildren().add(currentItem);

50. }

51. }

52. final TreeView<Entity> treeView = new TreeView<>();

53. treeView.setRoot(rootItem);

54. treeView.setShowRoot(false);

55. final StackPane root = new StackPane();

56. root.getChildren().add(treeView);

57. final Scene scene = new Scene(root);

58. primaryStage.setTitle("TreeView");

59. primaryStage.setWidth(300);

60. primaryStage.setHeight(450);

61. primaryStage.setScene(scene);

Pour le moment, notre arbre se contente d'un affichage textuel classique : seules sont affichées les valeurs retournées par la méthode toString() des objets contenus dans l'arbre :

Figure 52 - Affichage par défaut.

Nous avons donc deux types d'objets à représenter : les groupes et les contacts.

- Pour les groupes, nous allons nous contenter de quelque chose de simple : une ligne de texte contenant le nom du groupe et un indicateur qui affiche le nombre de contacts en ligne par rapport au nombre total de membres du groupe. Nous utiliserons directement du code Java dans la cellule.

- Pour les contacts, nous allons avoir un affichage plus complexe : le nom ou surnom du contact, l'avatar, une icône montrant le statut de connexion et le message du jour du contact. Ici, compte tenu du fait que c'est un peu plus complexe à mettre en place, nous allons à nouveau utiliser du FXML et les CSS pour éviter de vous noyer dans du code servant uniquement à monter une interface graphique.

Figure 53 - Création de la cellule dans SceneBuilder.

Il est très facile de définir la future apparence de notre cellule en utilisant SceneBuilder ou en écrivant directement le FXML suivant :

Fichier test.ContactRenderer.fxml

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

3. <?import java.lang.*?>

4. <?import java.net.*?>

5. <?import java.util.*?>

6. <?import javafx.scene.*?>

7. <?import javafx.scene.control.*?>

8. <?import javafx.scene.image.*?>

9. <?import javafx.scene.layout.*?>

11. <AnchorPane fx:id="rootPane" prefHeight="-1.0" prefWidth="200.0" styleClass="contact-renderer" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2" fx:controller="test.ContactRendererController">

12. <children>

13. <HBox prefHeight="-1.0" prefWidth="-1.0" styleClass="hbox" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">

14. <children>

15. <StackPane fx:id="avatarPane" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" styleClass="avatar-region">

16. <children>

17. <ImageView fx:id="avatarIcon" fitHeight="32" fitWidth="32" pickOnBounds="true" preserveRatio="true" />

18. </children>

19. </StackPane>

20. <Region fx:id="statusIndicator" maxHeight="-Infinity" maxWidth="-Infinity" prefHeight="-1.0" prefWidth="-1.0" styleClass="status-indicator" />

21. <VBox prefHeight="-1.0" prefWidth="-1.0" styleClass="vbox" HBox.hgrow="ALWAYS">

22. <children>

23. <Label fx:id="nameLabel" styleClass="name-label" text="Lorem ipsum" VBox.vgrow="NEVER" />

24. <Label fx:id="messageLabel" maxHeight="-1.0" styleClass="message-label" text="Lorem ipsum dolor sit amet, consectetur adipiscing elit." wrapText="true" VBox.vgrow="ALWAYS" />

25. <Pane fx:id="filler" prefHeight="-1.0" prefWidth="-1.0" VBox.vgrow="ALWAYS" />

26. </children>

27. </VBox>

28. </children>

29. </HBox>

30. </children>

31. <stylesheets>

32. <URL value="@ContactRenderer.css" />

33. </stylesheets>

34. </AnchorPane>

Nous allons jouer avec les CSS pour modifier l'apparence de la région statusIndicator en fonction du statut en ligne du contact. Bien que cette région soit initialement de forme carrée, nous pouvons lui donner des bords arrondis grâce aux CSS de manière à lui donner une forme de diode ou d'orbe. Nous pouvons faire varier sa couleur via la variable -base-color. Nous aurions pu tout aussi bien définir des styles pour chaque état du statut de connexion et utiliser des images bitmap prégénérées qui auraient été référencées dans la feuille de style pour chaque style.

Fichier test.ContactRenderer.css

1. .contact-renderer {

2. }

3. .hbox {

4. -fx-spacing: 3;

5. }

6. .vbox {

7. -fx-spacing: 3;

8. }

9. .status-indicator {

10. -base-color: gold;

11. -fx-padding: 5;

12. -fx-background-color: derive(-base-color, -40%), derive(-base-color, 60%), linear-gradient(to bottom, derive(-base-color, 90%), derive(-base-color, -30%));

13. -fx-background-radius: 5, 4, 3;

14. -fx-background-insets: 0, 1, 2;

15. }

16. .avatar-region {

17. -fx-background-color: whitesmoke;

18. }

19. .name-label {

20. -fx-font-size: 0.9em;

21. }

22. .message-label {

23. -fx-font-size: 0.85em;

24. -fx-font-style: italic;

25. -fx-opacity: 0.66;

26. }

27. .offline {

28. -fx-opacity: 0.33;

29. }

Et enfin, voici le contrôleur du FXML : lorsque ce dernier reçoit un nouveau contact, il adapte son affichage pour présenter les informations à l'écran.

Fichier test.ContactRendererController.java, code JDK7

1. package test;

3. import java.net.URL;

4. import java.util.ResourceBundle;

5. import javafx.beans.binding.Bindings;

6. import javafx.beans.binding.ObjectBinding;

7. import javafx.beans.property.ObjectProperty;

8. import javafx.beans.property.SimpleObjectProperty;

9. import javafx.beans.value.ChangeListener;

10. import javafx.beans.value.ObservableValue;

11. import javafx.fxml.FXML;

12. import javafx.fxml.Initializable;

13. import javafx.scene.control.Label;

14. import javafx.scene.image.Image;

15. import javafx.scene.image.ImageView;

16. import javafx.scene.layout.AnchorPane;

17. import javafx.scene.layout.Region;

19. public final class ContactRendererController implements Initializable {

21. @FXML

22. private AnchorPane rootPane;

23. @FXML

24. private ImageView avatarIcon;

25. @FXML

26. private Region statusIndicator;

27. @FXML

28. private Label nameLabel;

29. @FXML

30. private Label messageLabel;

32. private ObjectBinding<Entity.Contact.State> stateProperty;

34. public ContactRendererController() {

35. stateProperty = Bindings.<Entity.Contact.State>select(contact, "state");

36. }

38. @Override

39. public void initialize(URL url, ResourceBundle rb) {

40. avatarIcon.imageProperty().bind(Bindings.<Image>select(contact, "avatar"));

41. nameLabel.textProperty().bind(Bindings.selectString(contact, "displayName"));

42. messageLabel.textProperty().bind(Bindings.selectString(contact, "statusMessage"));

43. stateProperty.addListener(new ChangeListener<Entity.Contact.State>() {

45. @Override

46. public void changed(ObservableValue<? extends Entity.Contact.State> observableValue, Entity.Contact.State oldValue, Entity.Contact.State newValue) {

47. final Entity.Contact.State state = (newValue == null) ? Entity.Contact.State.OFFLINE : newValue;

48. rootPane.getStyleClass().remove("offline");

49. String style = null;

50. switch (state) {

51. case AVAILABLE:

52. style = "-base-color: green;";

53. break;

54. case AWAY:

55. style = "-base-color: gold;";

56. break;

57. case BUSY:

58. style = "-base-color: red;";

59. break;

60. case OFFLINE:

61. rootPane.getStyleClass().add("offline");

62. style = "-base-color: gray;";

63. break;

64. }

65. statusIndicator.setStyle(style);

66. }

67. });

68. }

70. private final ObjectProperty<Entity.Contact> contact = new SimpleObjectProperty<>(this, "contact");

72. public final Entity.Contact getContact() {

73. return contact.get();

74. }

76. public final void setContact(final Entity.Contact value) {

77. contact.set(value);

78. }

80. public final ObjectProperty<Entity.Contact> contactProperty() {

81. return contact;

82. }

83. }

Fichier test.ContactRendererController.java, code JDK8

1. package test;

3. import java.net.URL;

4. import java.util.ResourceBundle;

5. import javafx.beans.binding.Bindings;

6. import javafx.beans.binding.ObjectBinding;

7. import javafx.beans.property.ObjectProperty;

8. import javafx.beans.property.SimpleObjectProperty;

9. import javafx.beans.value.ObservableValue;

10. import javafx.fxml.FXML;

11. import javafx.fxml.Initializable;

12. import javafx.scene.control.Label;

13. import javafx.scene.image.Image;

14. import javafx.scene.image.ImageView;

15. import javafx.scene.layout.AnchorPane;

16. import javafx.scene.layout.Region;

18. public final class ContactRendererController implements Initializable {

20. @FXML

21. private AnchorPane rootPane;

22. @FXML

23. private ImageView avatarIcon;

24. @FXML

25. private Region statusIndicator;

26. @FXML

27. private Label nameLabel;

28. @FXML

29. private Label messageLabel;

31. private ObjectBinding<Entity.Contact.State> stateProperty;

33. public ContactRendererController() {

34. stateProperty = Bindings.<Entity.Contact.State>select(contact, "state");

35. }

37. @Override

38. public void initialize(URL url, ResourceBundle rb) {

39. avatarIcon.imageProperty().bind(Bindings.<Image>select(contact, "avatar"));

40. nameLabel.textProperty().bind(Bindings.selectString(contact, "displayName"));

41. messageLabel.textProperty().bind(Bindings.selectString(contact, "statusMessage"));

42. stateProperty.addListener((ObservableValue<? extends Entity.Contact.State> observableValue, Entity.Contact.State oldValue, Entity.Contact.State newValue) -> {

43. final Entity.Contact.State state = (newValue == null) ? Entity.Contact.State.OFFLINE : newValue;

44. rootPane.getStyleClass().remove("offline");

45. String style = null;

46. switch (state) {

47. case AVAILABLE:

48. style = "-base-color: green;";

49. break;

50. case AWAY:

51. style = "-base-color: gold;";

52. break;

53. case BUSY:

54. style = "-base-color: red;";

55. break;

56. case OFFLINE:

57. rootPane.getStyleClass().add("offline");

58. style = "-base-color: gray;";

59. break;

60. }

61. statusIndicator.setStyle(style);

62. });

63. }

65. private final ObjectProperty<Entity.Contact> contact = new SimpleObjectProperty<>(this, "contact");

67. public final Entity.Contact getContact() {

68. return contact.get();

69. }

71. public final void setContact(final Entity.Contact value) {

72. contact.set(value);

73. }

75. public final ObjectProperty<Entity.Contact> contactProperty() {

76. return contact;

77. }

78. }

Maintenant que nous avons notre renderer de contact, nous définissons une classe cellule qui se chargera d'afficher correctement l'entité actuelle :

- lorsque nous avons un groupe, nous utilisons directement la propriété text de la cellule, sans aucun graphique ;

- lorsque nous avons un contact, le texte est mis à null et nous utilisons notre renderer en tant que graphique de la cellule.

Fichier test.EntityTreeCell.java, code JDK7

1. package test;

3. import java.io.IOException;

4. import java.net.URL;

5. import java.util.logging.Level;

6. import java.util.logging.Logger;

7. import javafx.beans.binding.Bindings;

8. import javafx.beans.binding.IntegerBinding;

9. import javafx.beans.binding.StringBinding;

10. import javafx.fxml.FXMLLoader;

11. import javafx.scene.Node;

12. import javafx.scene.control.TreeCell;

13. import javafx.scene.control.TreeView;

14. import javafx.util.Callback;

16. public final class EntityTreeCell extends TreeCell<Entity> {

18. public static Callback<TreeView<Entity>, TreeCell<Entity>> forTreeView() {

19. return new Callback<TreeView<Entity>, TreeCell<Entity>>() {

21. @Override

22. public TreeCell<Entity> call(TreeView<Entity> param) {

23. return new EntityTreeCell();

24. }

25. };

26. }

28. @Override

29. protected void updateItem(final Entity value, final boolean empty) {

30. super.updateItem(value, empty);

31. // Nous utilisons un binding quand nous affichons un groupe, il est donc important de le casser.

32. textProperty().unbind();

33. setText(null);

34. setStyle(null);

35. setGraphic(null);

36. if (value != null && !empty) {

37. if (value instanceof Entity.Group) {

38. clearContactRenderer();

39. configureForGroup((Entity.Group) value);

40. } else {

41. configureForContact((Entity.Contact) value);

42. }

43. } else {

44. clearContactRenderer();

45. }

46. }

48. private void configureForGroup(final Entity.Group value) {

49. // Ce binding nous permet de connaitre le nombre total de contacts dans un groupe.

50. final IntegerBinding contactNumberProperty = Bindings.size(value.getChildren());

51. // Ce binding nous permet d'afficher <nom du groupe> (<contacts connectés>/<total contacts>)

52. final StringBinding labelProperty = new StringBinding() {

53. {

54. bind(value.displayNameProperty(), value.connectedProperty(), contactNumberProperty);

55. }

57. @Override

58. public void dispose() {

59. unbind(value.displayNameProperty(), value.connectedProperty(), contactNumberProperty);

60. super.dispose();

61. }

63. @Override

64. protected String computeValue() {

65. final String displayName = value.getDisplayName();

66. final int connected = value.getConnected();

67. final int size = value.getChildren().size();

68. StringBuilder result = new StringBuilder();

69. result.append(displayName);

70. if (size > 0) {

71. result.append(String.format(" (%d/%d)", connected, size));

72. }

73. return result.toString();

74. }

75. };

76. textProperty().bind(labelProperty);

77. setStyle("-fx-font-weight: bold; -fx-text-fill: purple;");

78. }

80. private Node contactRenderer = null;

81. private ContactRendererController contactRendererController = null;

83. private void configureForContact(final Entity.Contact value) {

84. try {

85. if (contactRenderer == null) {

86. final URL fxmlURL = getClass().getResource("ContactRenderer.fxml");

87. final FXMLLoader fxmlLoader = new FXMLLoader(fxmlURL);

88. contactRenderer = (Node) fxmlLoader.load();

89. contactRendererController = (ContactRendererController) fxmlLoader.getController();

90. }

91. contactRendererController.setContact(value);

92. setGraphic(contactRenderer);

93. } catch (IOException ex) {

94. Logger.getLogger(EntityTreeCell.class.getName()).log(Level.SEVERE, ex.getMessage(), ex);

95. }

96. }

98. private void clearContactRenderer() {

99. if (contactRenderer != null) {

100. contactRendererController.setContact(null);

101. contactRenderer = null;

102. contactRendererController = null;

103. }

104. }

105. }

Fichier test.EntityTreeCell.java, code JDK8

1. package test;

3. import java.io.IOException;

4. import java.net.URL;

5. import java.util.logging.Level;

6. import java.util.logging.Logger;

7. import javafx.beans.binding.Bindings;

8. import javafx.beans.binding.StringBinding;

9. import javafx.fxml.FXMLLoader;

10. import javafx.scene.Node;

11. import javafx.scene.control.TreeCell;

12. import javafx.scene.control.TreeView;

13. import javafx.util.Callback;

15. public final class EntityTreeCell extends TreeCell<Entity> {

17. public static Callback<TreeView<Entity>, TreeCell<Entity>> forTreeView() {

18. return (final TreeView<Entity> treeView) -> new EntityTreeCell();

19. }

21. @Override

22. protected void updateItem(final Entity value, final boolean empty) {

23. super.updateItem(value, empty);

24. // Nous utilisons un binding quand nous affichons un groupe, il est donc important de le casser.

25. textProperty().unbind();

26. setText(null);

27. setStyle(null);

28. setGraphic(null);

29. if (value != null && !empty) {

30. if (value instanceof Entity.Group) {

31. clearContactRenderer();

32. configureForGroup((Entity.Group) value);

33. } else {

34. configureForContact((Entity.Contact) value);

35. }

36. } else {

37. clearContactRenderer();

38. }

39. }

41. private void configureForGroup(final Entity.Group value) {

42. // Ce binding nous permet de connaitre le nombre total de contacts dans un groupe.

43. final IntegerBinding contactNumberProperty = Bindings.size(value.getChildren());

44. // Ce binding nous permet d'afficher <nom du groupe> (<contacts connectés>/<total contacts>)

45. final StringBinding labelProperty = new StringBinding() {

46. {

47. bind(value.displayNameProperty(), value.connectedProperty(), contactNumberProperty);

48. }

50. @Override

51. public void dispose() {

52. unbind(value.displayNameProperty(), value.connectedProperty(), contactNumberProperty);

53. super.dispose();

54. }

56. @Override

57. protected String computeValue() {

58. final String displayName = value.getDisplayName();

59. final int connected = value.getConnected();

60. final int size = value.getChildren().size();

61. StringBuilder result = new StringBuilder();

62. result.append(displayName);

63. if (size > 0) {

64. result.append(String.format(" (%d/%d)", connected, size));

65. }

66. return result.toString();

67. }

68. };

69. textProperty().bind(labelProperty);

70. setStyle("-fx-font-weight: bold; -fx-text-fill: purple;");

71. }

73. private Node contactRenderer = null;

74. private ContactRendererController contactRendererController = null;

76. private void configureForContact(final Entity.Contact value) {

77. try {

78. if (contactRenderer == null) {

79. final URL fxmlURL = getClass().getResource("ContactRenderer.fxml");

80. final FXMLLoader fxmlLoader = new FXMLLoader(fxmlURL);

81. contactRenderer = (Node) fxmlLoader.load();

82. contactRendererController = (ContactRendererController) fxmlLoader.getController();

83. }

84. contactRendererController.setContact(value);

85. setGraphic(contactRenderer);

86. } catch (IOException ex) {

87. Logger.getLogger(EntityTreeCell.class.getName()).log(Level.SEVERE, ex.getMessage(), ex);

88. }

89. }

91. private void clearContactRenderer() {

92. if (contactRenderer != null) {

93. contactRendererController.setContact(null);

94. contactRenderer = null;

95. contactRendererController = null;

96. }

97. }

98. }

Nous devons alors insérer la ligne de code suivante (une seule et unique) dans notre code principal pour activer notre nouvel affichage :

1. treeView.setCellFactory(EntityTreeCell.forTreeView());

Le résultat obtenu est quand même plus intéressant !

Figure 54 - Affichage final.

VI-C - TreeTableView<V>

Note : TreeTableView étant un contrôle qui n'est disponible qu'à partir de JavaFX 8 ; le code de cette section sera donc uniquement au format JDK8.

TreeTableView est un contrôle qui affiche un arbre dans la première colonne d'une table. Il se comporte comme TreeView pour ce qui est de la structure arborescente des données et des extensions/contractions des branches, mais aussi avec les mêmes subtilités liées aux besoins en fabriques de données pour les colonnes que celles que nous avons vues à propos de TableView.

Figure 55 - Un TreeTableView.

Tout comme c'était le cas pour TableView, nous serons amenés à manipuler une seconde classe tout aussi importante : javafx.scene.control.TreeTableColumn<V, T>. Chaque colonne de la table opère sur deux types :

- V qui est le type de l'objet contenu dans la table ;

- T qui est le type de l'objet à afficher dans la colonne.

VI-C-1 - Propriétés

Les propriétés importantes propres à ce contrôle sont les suivantes :

- columnResizePolicy - un callback qui permet de spécifier si les colonnes prennent tout l'espace horizontal visible ou se contentent de leur taille prédéfinie. La class TreeTableView contient deux constantes prédéfinies qu'il est conseillé d'utiliser :

- CONSTRAINED_RESIZE_POLICY - les colonnes prennent tout l'espace horizontal visible,

- UNCONSTRAINED_RESIZE_POLICY - utilisation des tailles prédéfinies ;

- comparator - une propriété en lecture seule qui contient un objet de type Comparator<V> lié au tri en cours sur la table ;

- editable - permet de spécifier si les cellules de la table peuvent être éditées ;

- editingCell - une propriété en lecture seule indiquant la position de la cellule en train d'être éditée ;

- expandedItemCount - une propriété en lecture seule qui retourne la somme du nombre de branches étendues et leurs enfants. Le nombre retourné est le nombre total de nœuds potentiellement visibles dans l'arbre ;

- fixedCellSize - permet de forcer une hauteur commune à toutes les cellules au lieu de calculer la hauteur cellule par cellule. Cette propriété est destinée à améliorer les performances de mise en page ;

- items - une ObservableList<V> contenant les objets à afficher ;

- onScrollToColumn - un callback appelé lorsque la méthode scrollToColumn(indice) ou scrollToColumn(colonne) est invoquée ;

- onScrollTo - un callback appelé lorsque la méthode scrollTo(indice) ou scrollTo(valeur) est invoquée ;

- onSort - un callback appelé lorsque le contenu du contrôle est trié ;

- placeHolder - un nœud graphique à afficher lorsque la table n'a pas de contenu ;

- rowFactory - une fabrique qui génère une cellule destinée à représenter une ligne entière de la table. Généralement, on utilisera la fabrique par défaut ;

- selectionModel - gère la sélection dans la table. Généralement, on utilisera le modèle par défaut ;

- sortPolicy - la politique de tri à appliquer sur le contenu de la table ;

- tableMenuButtonVisible - permet d'afficher un bouton autorisant l'utilisateur à accéder à un menu contrôlant la présence ou non de certaines colonnes ;

- treeColumn - référence la colonne qui affiche un arbre (par défaut la première colonne de la table).

Que nous complétons avec les propriétés de la classe TreeTableColumn<V, T> :

- cellFactory - de type Callback<TreeTableColumn<V, T>, TreeTableCell<V, T>> ; une fabrique à cellules qui est utilisée pour produire une cellule destinée à afficher chaque valeur T de la colonne associée à chaque valeur V de la table ;

- cellValueFactory - très importante et à ne pas confondre avec la précédente : une fabrique qui, pour chaque valeur V de la table retourne une ObservableValue<T> pour cette colonne. Cette fabrique sert donc à peupler les valeurs de la colonne à partir de celles de la table ;

- onEditCancel - un callback appelé lorsque l'utilisateur annule son édition ;

- onEditCommit - un callback appelé lorsque l'utilisateur valide son édition ;

- onEditStart - un callback appelé lorsque l'utilisateur démarre son édition ;

- sortType - indique si la colonne est triée de manière ascendante ou descendante quand un tri est appliqué sur la table ;

- treeTableView - une propriété en lecture seule qui contient une référence vers la table parente.

VI-C-2 - En pratique

Nous allons réaliser un mini-explorateur de fichiers qui permettra d'afficher les détails de la structure arborescente d'un répertoire. Nous allons en profiter pour délaisser (un petit peu) la vieillissante classe java.io.File et nous intéresser aux NIO2 introduits dans le JDK7 avec java.nio.file.Path et toutes les classes associées.

Commençons par nous créer une classe nommée MyFile qui servira de façade aux appels vers la couche NIO2… et également vers Swing (!). Nous allons en effet récupérer quelques informations en provenance de l'OS qui vont nous être fournies par la classe javax.swing.filechooser.FileSystemView . Cette classe est utilisée par JFileChooser, le sélectionneur de fichiers de Swing, pour récupérer quelques données natives telles que l'icône représentant le fichier ou son type.

Note : à cause de cette dépendance, ce projet ne pourra donc pas fonctionner dans les environnements sur lesquels Swing n'est pas présent (ex. : Java Embedded sur Raspberry Pi).

Également, pour conserver un code relativement simple à lire, nous ne monitorerons pas les modifications sur le fichier ou le répertoire. Cela aurait demandé la création et la mise en place de watch services sur les répertoires avec toute la gestion de tâches asynchrones qui va avec, ce qui est un peu en dehors du but de ce tutoriel.

Fichier test.MyFile.java

1. package test;

3. import java.awt.Graphics2D;

4. import java.awt.image.BufferedImage;

5. import java.io.IOException;

6. import java.lang.ref.WeakReference;

7. import java.nio.file.Files;

8. import java.nio.file.Path;

9. import java.nio.file.attribute.BasicFileAttributes;

10. import java.nio.file.attribute.FileTime;

11. import java.nio.file.attribute.PosixFilePermission;

12. import java.util.Comparator;

13. import java.util.LinkedList;

14. import java.util.List;

15. import java.util.Set;

16. import java.util.logging.Level;

17. import java.util.logging.Logger;

18. import java.util.stream.Collectors;

19. import javafx.embed.swing.SwingFXUtils;

20. import javafx.scene.image.Image;

21. import javax.swing.Icon;

22. import javax.swing.filechooser.FileSystemView;

24. public final class MyFile {

26. private final Path path;

28. public MyFile(final Path path) {

29. this.path = path;

30. }

32. public Path getPath() {

33. return path;

34. }

36. public final String getName() {

37. return path.toFile().getName();

38. }

40. public final boolean isFile() {

41. return Files.isRegularFile(path);

42. }

44. public final boolean isDirectory() {

45. return Files.isDirectory(path);

46. }

48. public boolean isReadable() {

49. return Files.isReadable(path);

50. }

52. public boolean isWritable() {

53. return Files.isWritable(path);

54. }

56. public boolean isExecutable() {

57. return Files.isExecutable(path);

58. }

60. public boolean isSymbolicLink() {

61. return Files.isSymbolicLink(path);

62. }

64. public FileTime getLastAccessTime() {

65. try {

66. final BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);

67. return attributes.lastAccessTime();

68. } catch (IOException ex) {

69. Logger.getLogger(MyFile.class.getName()).log(Level.SEVERE, ex.getMessage(), ex);

70. }

71. return null;

72. }

74. public FileTime getCreationTime() {

75. try {

76. final BasicFileAttributes attributes = Files.readAttributes(path, BasicFileAttributes.class);

77. return attributes.creationTime();

78. } catch (IOException ex) {

79. Logger.getLogger(MyFile.class.getName()).log(Level.SEVERE, ex.getMessage(), ex);

80. }

81. return null;

82. }

84. public FileTime getModificationTime() {

85. try {

86. return Files.getLastModifiedTime(path);

87. } catch (IOException ex) {

88. Logger.getLogger(MyFile.class.getName()).log(Level.SEVERE, ex.getMessage(), ex);

89. }

90. return null;

91. }

93. public long getSize() {

94. try {

95. return Files.isRegularFile(path) ? Files.size(path) : 0;

96. } catch (IOException ex) {

97. Logger.getLogger(MyFile.class.getName()).log(Level.SEVERE, ex.getMessage(), ex);

98. }

99. return 0;

100. }

102. public List<MyFile> getChildren() {

103. final List<MyFile> result = new LinkedList<>();

104. try {

105. // Nous allons classer les répertoires avant les fichiers.

106. final Comparator<Path> pathComparator = (final Path path1, final Path path2) -> {

107. if ((Files.isDirectory(path1) && Files.isDirectory(path2)) || (Files.isRegularFile(path1) && Files.isRegularFile(path2))) {

108. return path1.toString().compareTo(path2.toString());

109. } else if (Files.isDirectory(path1)) {

110. return -1;

111. } else {

112. return 1;

113. }

114. };

115. final List<Path> children = Files.list(path).sorted(pathComparator).collect(Collectors.toList());

116. children.forEach((child) -> result.add(new MyFile(child)));

117. } catch (IOException ex) {

118. Logger.getLogger(MyFile.class.getName()).log(Level.SEVERE, ex.getMessage(), ex);

119. }

120. return result;

121. }

123. public Set<PosixFilePermission> getPermissions() {

124. try {

125. return Files.getPosixFilePermissions(path);

126. } catch (UnsupportedOperationException | IOException ex) {

127. Logger.getLogger(MyFile.class.getName()).log(Level.SEVERE, ex.getMessage(), ex);

128. }

129. return null;

130. }

132. private String displayName = null;

134. public String getSystemDisplayName() {

135. if (displayName == null && path != null) {

136. final FileSystemView fileSystemView = FileSystemView.getFileSystemView();

137. displayName = fileSystemView.getSystemDisplayName(path.toFile());

138. }

139. return displayName;

140. }

142. private String description = null;

144. public String getSystemTypeDescription() {

145. if (description == null && path != null) {

146. final FileSystemView fileSystemView = FileSystemView.getFileSystemView();

147. description = fileSystemView.getSystemTypeDescription(path.toFile());

148. }

149. return description;

150. }

152. private WeakReference<Image> systemIcon = null;

154. public Image getSystemIcon() {

155. if ((systemIcon == null || systemIcon.get() == null) && path != null) {

156. final FileSystemView fileSystemView = FileSystemView.getFileSystemView();

157. // On récupère l'icône.

158. final Icon icon = fileSystemView.getSystemIcon(path.toFile());

159. // On dessine cette icône dans une nouvelle image Java2D.

160. final BufferedImage swingImage = new BufferedImage(icon.getIconWidth(), icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB);

161. final Graphics2D g2d = swingImage.createGraphics();

162. try {

163. icon.paintIcon(null, g2d, 0, 0);

164. } finally {

165. g2d.dispose();

166. }

167. // Que l'on convertie ensuite en image JavaFX.

168. final Image fxImage = SwingFXUtils.toFXImage(swingImage, null);

169. systemIcon = new WeakReference<>(fxImage);

170. }

171. return (systemIcon == null) ? null : systemIcon.get();

172. }

173. }

Bien entendu, nous devons stocker ces objets dans des instances de TreeItem<MyFile>. Cependant, comme je vous l'ai indiqué précédemment, en général on ne charge pas entièrement une arborescence de fichiers en mémoire. Nous allons donc utiliser des nœuds dynamiques dans notre arbre !

Fichier test.MyFileTreeItem.java

1. package test;

3. import java.util.LinkedList;

4. import java.util.List;

5. import javafx.beans.value.ChangeListener;

6. import javafx.beans.value.ObservableValue;

7. import javafx.scene.control.TreeItem;

9. public final class MyFileTreeItem extends TreeItem<MyFile> {

11. public MyFileTreeItem(final MyFile value) {

12. super(value);

13. expandedProperty().addListener(expandedChangeListener);

14. }

16. /**

17. * Nettoyage complet du noeud.

18. * <br/>Il est totalement inutilisable après cet appel.

19. */

20. private void dispose() {

21. expandedProperty().removeListener(expandedChangeListener);

22. clearChildren();

23. setValue(null);

24. }

26. public void refresh() {

27. clearChildren();

28. generateChildren();

29. }

31. private void generateChildren() {

32. final MyFile file = getValue();

33. if (file == null) {

34. return;

35. }

36. final List<MyFileTreeItem> newItems = new LinkedList<>();

37. file.getChildren().forEach((final MyFile childFile) -> {

38. final MyFileTreeItem childItem = new MyFileTreeItem(childFile);

39. newItems.add(childItem);

40. });

41. getChildren().setAll(newItems);

42. newItems.clear();

43. }

45. private void clearChildren() {

46. getChildren().forEach((final TreeItem<MyFile> treeItem) -> {

47. final MyFileTreeItem childItem = (MyFileTreeItem) treeItem;

48. childItem.dispose();

49. });

50. getChildren().clear();

51. }

53. private final ChangeListener<Boolean> expandedChangeListener = (ObservableValue<? extends Boolean> observableValue, Boolean oldValue, Boolean newValue) -> {

54. // Extension.

55. if (newValue) {

56. generateChildren();

57. } // Contraction.

58. else {

59. clearChildren();

60. }

61. };

63. // Et la surchage de isLeaf().

64. // @return Toujours {@code false} si le fichier actuel est un répertoire.

65. @Override

66. public boolean isLeaf() {

67. final MyFile file = getValue();

68. return (file == null) ? true : !file.isDirectory();

69. }

70. }

Maintenant nous allons charger un répertoire (par exemple, celui contenant ce projet) et l'afficher dans un TreeTableView. La suite est assez simple et ressemble à ce que nous avons précédemment fait pour TableView en ce qui concerne les fabriques de données :

1. final TreeTableView treeTableView = new TreeTableView();

2. treeTableView.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);

3. //

4. final TreeTableColumn<MyFile, String> nameColumn = new TreeTableColumn<>("Nom");

5. nameColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<MyFile, String> p) -> {

6. final MyFile file = p.getValue().getValue();

7. return new SimpleStringProperty(file.getSystemDisplayName());

8. });

9. //

10. final TreeTableColumn<MyFile, String> typeColumn = new TreeTableColumn<>("Type");

11. typeColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<MyFile, String> p) -> {

12. final MyFile file = p.getValue().getValue();

13. return new SimpleStringProperty(file.getSystemTypeDescription());

14. });

15. //

16. final TreeTableColumn<MyFile, Long> sizeColumn = new TreeTableColumn<>("Taille");

17. sizeColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<MyFile, Long> p) -> {

18. final MyFile file = p.getValue().getValue();

19. return new SimpleObjectProperty<>(file.getSize());

20. });

21. //

22. final TreeTableColumn<MyFile, FileTime> createdColumn = new TreeTableColumn<>("Création");

23. createdColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<MyFile, FileTime> p) -> {

24. final MyFile file = p.getValue().getValue();

25. return new SimpleObjectProperty<>(file.getCreationTime());

26. });

27. //

28. final TreeTableColumn<MyFile, FileTime> modifiedColumn = new TreeTableColumn<>("Modifié");

29. modifiedColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<MyFile, FileTime> p) -> {

30. final MyFile file = p.getValue().getValue();

31. return new SimpleObjectProperty<>(file.getModificationTime());

32. });

33. //

34. final TreeTableColumn<MyFile, FileTime> lastAccessColumn = new TreeTableColumn<>("Dernier accès");

35. lastAccessColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<MyFile, FileTime> p) -> {

36. final MyFile file = p.getValue().getValue();

37. return new SimpleObjectProperty<>(file.getLastAccessTime());

38. });

39. //

40. final TreeTableColumn<MyFile, Boolean> readableColumn = new TreeTableColumn<>("Lisible ?");

41. readableColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<MyFile, Boolean> p) -> {

42. final MyFile file = p.getValue().getValue();

43. return new SimpleBooleanProperty(file.isReadable());

44. });

45. //

46. final TreeTableColumn<MyFile, Boolean> writableColumn = new TreeTableColumn<>("Modifiable ?");

47. writableColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<MyFile, Boolean> p) -> {

48. final MyFile file = p.getValue().getValue();

49. return new SimpleBooleanProperty(file.isWritable());

50. });

51. //

52. final TreeTableColumn<MyFile, Boolean> executableColumn = new TreeTableColumn<>("Exécutable ?");

53. executableColumn.setCellValueFactory((TreeTableColumn.CellDataFeatures<MyFile, Boolean> p) -> {

54. final MyFile file = p.getValue().getValue();

55. return new SimpleBooleanProperty(file.isExecutable());

56. });

57. executableColumn.setCellFactory(CheckBoxTreeTableCell.forTreeTableColumn(executableColumn));

58. //

59. treeTableView.getColumns().setAll(nameColumn, typeColumn, sizeColumn, createdColumn, modifiedColumn, lastAccessColumn, readableColumn, writableColumn, executableColumn);

60. //

61. final Path rootPath = new File(System.getProperty("user.dir")).toPath();

62. final MyFile rootFile = new MyFile(rootPath);

63. final TreeItem<MyFile> rootItem = new MyFileTreeItem(rootFile);

64. treeTableView.setRoot(rootItem);

65. //

66. final StackPane root = new StackPane();

67. root.getChildren().add(treeTableView);

68. final Scene scene = new Scene(root);

69. primaryStage.setScene(scene);

70. primaryStage.setWidth(950);

71. primaryStage.setTitle("TreeTableView");

Initialement notre affichage apparait avec la seule racine visible, mais une fois la branche dépliée, vous devriez obtenir un affichage similaire à :

Figure 56 - Affichage standard.

VI-C-2-a - Booléens

Vous connaissez désormais l'astuce ! Pour afficher des cases à cocher sur les colonnes contenant des valeurs booléennes, l'API met à disposition des cellules prêtes à l'emploi. Ici c'est la classe javafx.scene.control.cell.CheckBoxTreeTableCell qu'il faut utiliser. Si nous insérons les lignes suivantes dans notre code :

1. readableColumn.setCellFactory(CheckBoxTreeTableCell.forTreeTableColumn(readableColumn));

2. writableColumn.setCellFactory(CheckBoxTreeTableCell.forTreeTableColumn(writableColumn));

3. executableColumn.setCellFactory(CheckBoxTreeTableCell.forTreeTableColumn(executableColumn));

La différence saute tout de suite aux yeux :

Figure 57 - Insertion de cellules pour les valeurs booléennes.

Note : mes tests sont effectués sous Windows donc les fichiers auxquels j'accède ont tous les droits d'accès. De même, mon système est en anglais, ce qui explique le contenu de la seconde colonne.

VI-C-2-b - Icônes

Nous allons maintenant créer de nouveaux types de cellules pour afficher les autres données. Commençons par la première colonne, celle qui affiche le nom du fichier et arrangeons-nous pour y afficher également les icônes des fichiers et des dossiers.

Fichier test.MyFileNameTreeTableCell.java

1. package test;

3. import javafx.scene.Node;

4. import javafx.scene.control.TreeTableCell;

5. import javafx.scene.control.TreeTableColumn;

6. import javafx.scene.image.ImageView;

7. import javafx.util.Callback;

9. public final class MyFileNameTreeTableCell extends TreeTableCell<MyFile, String> {

11. private final ImageView imageView = new ImageView();

13. @Override

14. protected void updateItem(String value, boolean empty) {

15. super.updateItem(value, empty);

16. String text = null;

17. Node graphic = null;

18. if (value != null && !empty) {

19. text = value;

20. final MyFile file = getTreeTableRow().getItem();

21. if (file != null) {

22. imageView.setImage(file.getSystemIcon());

23. graphic = imageView;

24. }

25. }

26. setText(text);

27. setGraphic(graphic);

28. }

30. // Fabrique statique.

31. public static Callback<TreeTableColumn<MyFile, String>, TreeTableCell<MyFile, String>> forTreeTableColumn() {

32. return (TreeTableColumn<MyFile, String> p) -> new MyFileNameTreeTableCell();

33. }

34. }

Nous activerons cette nouvelle cellule en ajoutant la ligne suivante à notre code :

1. nameColumn.setCellFactory(MyFileNameTreeTableCell.forTreeTableColumn());

Ce qui nous donne désormais l'affichage suivant :

Figure 58 - Ajout des icônes des fichiers.

VI-C-2-c - Date

Occupons-nous désormais des affichages des dates. Ici nos appels aux méthodes statiques de la classe Files dans notre classe MyFile retournent des objets de type FileTime. Il s'agit d'une nouvelle classe introduite dans le JDK8 et qui utilise la nouvelle API de temps. Heureusement pour nous, il nous est possible de les convertir en millisecondes depuis l'Epoch (la date de référence qui marque le début du calendrier UNIX) et ensuite de les convertir en Date Java puis en texte via les formateurs habituels :

Fichier test.FileTimeTreeTableCell.java

1. package test;

4. import java.nio.file.attribute.FileTime;

5. import java.text.DateFormat;

6. import java.util.Date;

7. import javafx.scene.control.TreeTableCell;

8. import javafx.scene.control.TreeTableColumn;

9. import javafx.util.Callback;

11. public final class FileTimeTreeTableCell<T> extends TreeTableCell<T, FileTime> {

13. private static final DateFormat FORMAT = DateFormat.getDateTimeInstance();

15. @Override

16. protected void updateItem(FileTime value, boolean empty) {

17. super.updateItem(value, empty);

18. String text = null;

19. if (value != null && !empty) {

20. final long millis = value.toMillis();

21. final Date date = new Date(millis);

22. text = FORMAT.format(date);

23. }

24. setText(text);

25. }

27. // Fabrique statique.

28. public static <T> Callback<TreeTableColumn<T, FileTime>, TreeTableCell<T, FileTime>> forTreeTableColumn() {

29. return (TreeTableColumn<T, FileTime> column) -> new FileTimeTreeTableCell<>();

30. }

31. }

Que nous activons via les lignes de code suivantes :

1. createdColumn.setCellFactory(FileTimeTreeTableCell.forTreeTableColumn());

2. modifiedColumn.setCellFactory(FileTimeTreeTableCell.forTreeTableColumn());

3. lastAccessColumn.setCellFactory(FileTimeTreeTableCell.forTreeTableColumn());

Avec pour résultat :

Figure 59 - Ajout de la prise en charge des dates.

VI-C-2-d - Taille

Enfin nous allons terminer cet aperçu en créant une cellule qui permettra de formater correctement la taille du fichier :

Fichier test.MyFileSizeTreeTableCell.java

1. package test;

3. import javafx.scene.control.TreeTableCell;

4. import javafx.scene.control.TreeTableColumn;

5. import javafx.util.Callback;

7. public final class MyFileSizeTreeTableCell extends TreeTableCell<MyFile, Long> {

9. public static enum Style {

11. BINARY,

12. INTERNATIONAL_SYSTEM

13. }

14. private final Style style;

16. public MyFileSizeTreeTableCell(final Style style) {

17. this.style = (style == null) ? Style.INTERNATIONAL_SYSTEM : style;

18. }

20. @Override

21. protected void updateItem(Long value, boolean empty) {

22. super.updateItem(value, empty);

23. String text = null;

24. if (value != null && !empty) {

25. final MyFile file = getTreeTableRow().getItem();

26. if (file != null && !file.isDirectory()) {

27. int base = (style == Style.INTERNATIONAL_SYSTEM) ? 1000 : 1024;

28. int scale = (int) Math.floor(Math.log10(value));

29. int power = scale / 3;

30. double val = value.doubleValue() / Math.pow(base, power);

31. String unit = null;

32. switch (power) {

33. case 0:

34. unit = "o";

35. break;

36. case 1:

37. unit = (style == Style.INTERNATIONAL_SYSTEM) ? "Ko" : "Kio";

38. break;

39. case 2:

40. unit = (style == Style.INTERNATIONAL_SYSTEM) ? "Mo" : "Mio";

41. break;

42. case 3:

43. unit = (style == Style.INTERNATIONAL_SYSTEM) ? "Go" : "Gio";

44. break;

45. case 4:

46. default:

47. unit = (style == Style.INTERNATIONAL_SYSTEM) ? "To" : "Tio";

48. power = 4;

49. }

50. final String pattern = (val == (int) val) ? "%.0f %s" : "%.2f %s";

51. text = String.format(pattern, val, unit);

52. }

53. }

54. setText(text);

55. }

57. // Fabrique statique.

58. public static Callback<TreeTableColumn<MyFile, Long>, TreeTableCell<MyFile, Long>> forTableColumn() {

59. return forTableColumn(null);

60. }

62. // Fabrique statique.

63. public static Callback<TreeTableColumn<MyFile, Long>, TreeTableCell<MyFile, Long>> forTableColumn(final Style style) {

64. return (TreeTableColumn<MyFile, Long> treeTableColumn) -> new MyFileSizeTreeTableCell(style);

65. }

66. }

L'activation se résume une fois de plus à une simple ligne :

1. sizeColumn.setCellFactory(MyFileSizeTreeTableCell.forTableColumn());

Et le résultat donne :

Figure 60 - Ajout du support de la taille des fichiers.

VII - Conclusion

Nous avons fait un rapide tour d'horizon des contrôles JavaFX actuellement disponibles et qui utilisent l'API Cell. Bien que nous nous soyons attachés à voir des cas particuliers pour chacun de ces contrôles, les techniques présentées ici et là pour un contrôle donné sont généralement applicables aux autres. C'est désormais à votre tour de jouer pour rendre vos applications plus riches et vivantes !

VIII - Liens

- Flowless - une nouvelle implémentation d'un gestionnaire de vue virtualisée. Propose également un comparateur de performances entre cette nouvelle implémentation et celles de l'API JavaFX.

IX - Remerciements

Je tiens à remercier toute l'équipe du forum Développez ainsi que Mickael Baron pour ses suggestions et sa relecture du présent article. Je tiens également à remercier Claude Leloup et f-leb pour leurs corrections orthographiques.

X - Annexe

X-A - Code source et exemples

Vous pouvez retrouver un projet NetBeans contenant les tests et exemples se rapportant à cet article sur https://fabrice-bouye.developpez.com/tutoriels/javafx/customisation-controle-virtualise-api-cell-javafx/fichiers/code_source_javafx_cell.zip