Tutoriel sur les extracteurs en JavaFX

Comment recevoir des notifications de mise à jour des éléments contenus dans des listes observables

Fabrice Bouyé

Cet article a pour but de vous expliquer comment utiliser des extracteurs et recevoir des notifications de mise à jour provenant des éléments qui sont contenus dans une liste observable.

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

- Titre : Tutoriel sur les extracteurs en JavaFX

- Auteur : Fabrice Bouyé

- Parution : 27 octobre 2019

- 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 ® 2017 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. Liste observable en JavaFX

- III. Utilisation normale

- III-A. Modèle

- III-B. Création de la liste

- III-C. Écouteur

- III-D. Test

- IV. Suivi des mises à jour

- IV-A. Création de la liste

- IV-A-1. Extracteur

- IV-A-2. Peuplement

- IV-B. Test

- V. Exemple

- V-A. Montage de l’UI

- V-A-1. Table & éditeur

- V-A-2. Liste

- V-A-3. Zone de message

- V-A-4. Scène

- V-B. Réaction aux modifications de la liste

- V-C. Publication du message

- VI. Code source

- VII. Conclusion

- VIII. Remerciements

I - Introduction

Les listes observables sont partout présentes dans JavaFX, elles sont littéralement à la base du concept du SceneGraph qui permet d’afficher des nœuds graphiques dans une interface utilisateur. En effet chaque nœud parent, groupe ou gestionnaire de mise en page dispose d’une liste observable de nœuds enfants. Au niveau des contrôles, ces listes sont également utilisées par les boites déroulantes et autres listes, arbres et tables graphiques.

De base, ces listes observables permettent de recevoir, via des écouteurs de type ListChangeListener, des événements de notification classiques tels que d’ajout, retrait, remplacement ou encore permutation des éléments contenus dans une de ces listes. Bref, tout ce qui se rapporte aux manipulations classiques d’une liste.

Cependant, il existe un autre type de notification : la notification de mise à jour qui permet d’être averti lorsque la propriété observable d’un élément de la liste est modifiée. Cette fonctionnalité peu connue permet d’être averti que l’état d’un des objets de la liste a changé ! Elle nécessite l’utilisation d’un nouveau concept : l’extracteur de propriétés.

II - Liste observable en JavaFX

Nous allons effectuer un rapide rafraichissement sur la manière d’utiliser les listes observables. La classe ObservableList ainsi que les autres collections observables et les événements et classes utilitaires qui leur sont attachées font partie du module javafx.base et n’ont donc aucune dépendance graphique.

Contrairement aux listes Java classiques qui sont muettes lorsque leur contenu est modifié, une ObservableList de JavaFX émet des événements de type ListChangeListener.Change qui peuvent être reçus par un écouteur de type ListChangeListener. Cet événement peut avoir plusieurs états :

Type

Description

La taille de la liste change ?

Les indices de la liste changent ?

Méthodes

Added

Ajout d’un élément dans la liste

Oui

Oui, décalage vers la droite à partir de l’indice d’ajout

add();

Removed

Suppression d’un élément dans la liste

Oui

Oui, décalage vers la gauche à partir de l’indice de suppression

remove();

Replaced

Remplacement d’un élément dans la liste par une valeur externe

Non

Non

set();

Permutated

Permutation de deux éléments dans la liste

Non

Non, les indices des deux valeurs sont cependant permutés

sort();

Updated

Mise à jour d’une propriété d’un élément de la liste

Non

Non

n/a

À noter qu’un événement de replacement peut être vu comme une composition d’un retrait suivi d’un ajout. Tous les types d’événements, sauf celui de mise à jour, impliquent l’utilisation de méthodes définies dans l’interface java.util.List.

Ces événements existent pour tenir notre programme informé de toutes modifications dans notre liste. C’est, bien sûr, très utile pour des interfaces graphiques que ce soit pour changer le contenu d’une liste déroulante ou encore pour rafraichir un graphe de scène et c’est bien pour cela que le cœur de la partie graphique de JavaFX est construit autour de ce concept. Cependant cela a aussi son utilité pour créer des programmes non graphiques pour, par exemple, réagir automatiquement à l’arrivée d’un nouveau message dans une file d’attente d’une couche business ou encore réagir au changement de statut d’un client connecté sur un serveur sans devoir tout coder à la main.

III - Utilisation normale

Nous allons maintenant créer un exemple simple permettant de tester les propagations d’événements lorsque nous manipulons une liste observable.

III-A - Modèle

Commençons par créer le type d’objets que nous allons stocker dans la liste observable. Ce même modèle nous servira également plus tard lorsque nous utiliserons des extracteurs.

Nous allons créer une classe Person qui peut, par exemple, représenter un utilisateur tel que stocké dans une base de données. Cette personne disposera de deux propriétés observables, son nom (name) et son prénom (surname) que nous pourrons éditer dans une interface graphique.

1. public final class Person {

3. public Person(final String name, final String surname) {

4. setName(name);

5. setSurname(surname);

6. }

8. private final StringProperty name = new SimpleStringProperty(this, "name"); // NOI18N.

10. public final String getName() {

11. return name.get();

12. }

14. public final void setName(final String value) {

15. name.set(value);

16. }

18. public final StringProperty nameProperty() {

19. return name;

20. }

22. private final StringProperty surname = new SimpleStringProperty(this, "surname"); // NOI18N.

24. public final String getSurname() {

25. return surname.get();

26. }

28. public final void setSurname(final String value) {

29. surname.set(value);

30. }

32. public final StringProperty surnameProperty() {

33. return surname;

34. }

36. @Override

37. public String toString() {

38. String name = getName();

39. name = (name == null) ? "" : name.trim(); // NOI18N.

40. String surname = getSurname();

41. surname = (surname == null) ? "" : surname.trim(); // NOI18N.

42. final StringBuilder builder = new StringBuilder();

43. if (!name.isEmpty()) {

44. builder.append(name.trim());

45. }

46. if (!name.isEmpty() && !surname.isEmpty()) {

47. builder.append(" ");

48. }

49. if (!surname.isEmpty()) {

50. builder.append(surname.trim());

51. }

52. return builder.toString();

53. }

54. }

Note : la méthode toString() est ici uniquement destinée à aider le débogage en faisant des affichages en mode console.

III-B - Création de la liste

Il nous est alors possible d’instancier puis d’ajouter des personnes dans une liste observable, d’installer un écouteur sur cette liste et puis de faire quelques tests sur son contenu :

1. // Création de la liste.

2. final ObservableList<Person> persons = FXCollections.observableArrayList(

3. new Person("Dupond", "Valérie"), // NOI18N.

4. new Person("Higgins", "Clark"), // NOI18N.

5. new Person("Pantou", "Maurice"), // NOI18N.

6. new Person("Parmentier", "Yvette")); // NOI18N.

Ou encore :

1. // Création de la liste.

2. final ObservableList<Person> persons = FXCollections.observableArrayList();

3. persons.setAll(

4. new Person("Dupond", "Valérie"), // NOI18N.

5. new Person("Higgins", "Clark"), // NOI18N.

6. new Person("Pantou", "Maurice"), // NOI18N.

7. new Person("Parmentier", "Yvette")); // NOI18N.

III-C - Écouteur

Nous connectons ensuite sur la liste un écouteur de type ListChangeListener<Person> et nous traitons l’événement de modification reçu :

Code JDK7

1. // Écouteur.

2. persons.addListener(new ListChangeListener<Person>() {

3. @Override

4. public void onChanged(ListChangeListener.Change<? extends Person> change) {

5. // Rappel : plusieurs modifications peuvent être agrégées dans un seul événement.

6. while (change.next()) {

7. String changeLabel = "?"; // NOI18N.

8. if (change.wasReplaced()) {

9. changeLabel = "replaced"; // NOI18N.

10. } else if (change.wasAdded()) {

11. changeLabel = "added"; // NOI18N.

12. } else if (change.wasRemoved()) {

13. changeLabel = "removed"; // NOI18N.

14. } else if (change.wasPermutated()) {

15. changeLabel = "permutated"; // NOI18N.

16. } else if (change.wasUpdated()) {

17. changeLabel = "updated"; // NOI18N.

18. }

19. final String pattern = String.format("Element %s was %s%n", "%d", changeLabel); // NOI18N.

20. if (change.wasAdded() || change.wasReplaced() || change.wasUpdated()) {

21. // Parcours exclusif.

22. for (int index = change.getFrom(); index < change.getTo(); index++) {

23. System.out.printf(pattern, index);

24. }

25. } else {

26. // Parcours inclusif.

27. for (int index = change.getFrom(); index <= change.getTo(); index++) {

28. System.out.printf(pattern, index);

29. }

30. }

31. }

32. }

33. });

Ou encore :

Code JDK8

1. // Écouteur.

2. persons.addListener (ListChangeListener.Change<? extends Person> change) -> {

3. // Rappel : plusieurs modifications peuvent être agrégées dans un seul événement.

4. while (change.next()) {

5. String changeLabel = "?"; // NOI18N.

6. if (change.wasReplaced()) {

7. changeLabel = "replaced"; // NOI18N.

8. } else if (change.wasAdded()) {

9. changeLabel = "added"; // NOI18N.

10. } else if (change.wasRemoved()) {

11. changeLabel = "removed"; // NOI18N.

12. } else if (change.wasPermutated()) {

13. changeLabel = "permutated"; // NOI18N.

14. } else if (change.wasUpdated()) {

15. changeLabel = "updated"; // NOI18N.

16. }

17. final String pattern = String.format("Element %s was %s%n", "%d", changeLabel); // NOI18N.

18. if (change.wasAdded() || change.wasReplaced() || change.wasUpdated()) {

19. // Parcours exclusif.

20. IntStream.range(change.getFrom(), change.getTo())

21. .forEach(index -> System.out.printf(pattern, index));

22. } else {

23. // Parcours inclusif.

24. IntStream.rangeClosed(change.getFrom(), change.getTo())

25. .forEach(index -> System.out.printf(pattern, index));

26. }

27. }

28. });

III-D - Test

Nous pouvons ensuite tester les différents types d’opérations :

Code JDK7

1. // Opérations.

2. System.out.println(persons);

3. // Retrait.

4. System.out.println("== Remove ==");

5. persons.remove(0);

6. System.out.println(persons);

7. // Ajout.

8. System.out.println("== Add ==");

9. persons.add(0, new Person("Dupond", "Valérie")); // NOI18N.

10. System.out.println(persons);

11. // Remplacement.

12. System.out.println("== Replace ==");

14. persons.set(0, new Person("Langworth", "Cathy")); // NOI18N.

15. System.out.println(persons);

16. // Tri -> permutation.

17. System.out.println("== Permute ==");

18. Collections.sort(persons, new Comparator<Person>() {

19. @Override

20. public int compare(Person p1, Person p2) {

21. int result = p1.getName().compareTo(p2.getName());

22. if (result == 0) {

23. result = p1.getSurname().compareTo(p2.getSurname());

24. }

25. return result;

26. }

27. });

28. System.out.println(persons);

29. // Mise à jour.

30. System.out.println("== Update ==");

31. persons.get(0).setName("Smith"); // NOI18N.

32. persons.get(0).setSurname("Anny"); // NOI18N.

33. System.out.println(persons);

Ou :

Code JDK8

1. // Opérations.

2. System.out.println(persons);

3. // Retrait.

4. System.out.println("== Remove ==");

5. persons.remove(0);

6. System.out.println(persons);

7. // Ajout.

8. System.out.println("== Add ==");

9. persons.add(0, new Person("Dupond", "Valérie")); // NOI18N.

10. System.out.println(persons);

11. // Remplacement.

12. System.out.println("== Replace ==");

14. persons.set(0, new Person("Langworth", "Cathy")); // NOI18N.

15. System.out.println(persons);

16. // Tri -> permutation.

17. System.out.println("== Permute ==");

18. persons.sort((p1, p2) -> {

19. int result = p1.getName().compareTo(p2.getName());

20. if (result == 0) {

21. result = p1.getSurname().compareTo(p2.getSurname());

22. }

23. return result;

24. });

25. System.out.println(persons);

26. // Mise à jour.

27. System.out.println("== Update ==");

28. persons.get(0).setName("Smith"); // NOI18N.

29. persons.get(0).setSurname("Anny"); // NOI18N.

30. System.out.println(persons);

Ce qui nous permet d’obtenir le résultat suivant :

[Dupond Valérie, Higgins Clark, Pantou Maurice, Parmentier Yvette]
== Remove ==
Element 0 was removed
[Higgins Clark, Pantou Maurice, Parmentier Yvette]
== Add ==
Element 0 was added
[Dupond Valérie, Higgins Clark, Pantou Maurice, Parmentier Yvette]
== Replace ==
Element 0 was replaced
[Langworth Cathy, Higgins Clark, Pantou Maurice, Parmentier Yvette]
== Permute ==
Element 0 was permutated
Element 1 was permutated
Element 2 was permutated
Element 3 was permutated
Element 4 was permutated
[Higgins Clark, Langworth Cathy, Pantou Maurice, Parmentier Yvette]
== Update ==
[Smith Anny, Langworth Cathy, Pantou Maurice, Parmentier Yvette]

Nous voyons bien que tous les types de modifications ont été répercutés sauf l’événement de mise à jour.

IV - Suivi des mises à jour

Le suivi des événements de mise à jour est en effet optionnel, comme spécifié dans la documentation de la classe ListChangeListener.Change, et la plupart des listes observables ne permettent pas de l’effectuer.

IV-A - Création de la liste

Nous allons donc changer la manière dont nous créons une instance de liste observable. Nous allons cette fois-ci utiliser la variante de la méthode de fabrique observableArrayList()(1) de la classe utilitaire FXCollections qui prend en paramètre un extracteur de valeurs observables… c’est-à-dire un objet qui pour chaque valeur contenue dans la liste va retourner ses propriétés d’intérêt.

IV-A-1 - Extracteur

Dans notre cas, le type de l’extracteur est Callback<Person, Observable[]> : un objet dont nous devons surcharger la méthode call(), qui prend une instance de la classe Person en paramètre tandis qu’elle retournera un tableau des valeurs observables qui ne seront autres que les propriétés intéressantes de notre classe Person, ses propriétés name et surmane.

Code JDK7

1. final ObservableList<Person> persons = FXCollections.observableArrayList(new Callback<Person, Observable[]>() {

2. @Override

3. public Observable[] call(final Person person) {

4. return new ObservableValue[]{person.nameProperty(), person.surnameProperty()};

5. }

6. });

Ce que nous pouvons, bien évidemment, grandement simplifier en écrivant une expression lambda avec le JDK8 : une simple fonction qui prend une personne en paramètre et sort un tableau contenant ses propriétés observables en retour !

Code JDK8

final ObservableList<Person> persons = FXCollections.observableArrayList(person -> new ObservableValue[]{person.nameProperty(), person.surnameProperty()});

Lorsqu’une nouvelle personne est ajoutée dans la liste observable, cette dernière va invoquer l’extracteur qui va retourner les propriétés name et surname de la personne. La liste créera alors le câblage nécessaire pour surveiller l’état de ces propriétés et propagera leurs modifications de valeur si besoin. L’extracteur sera utilisé de manière similaire lorsque l’élément est retiré de la liste pour supprimer toute trace des écouteurs des propriétés (et éviter ainsi une fuite mémoire).

IV-A-2 - Peuplement

La liste ainsi créée étant initialement vide, nous devons ensuite la peupler avec son contenu :

persons.setAll(
new Person("Dupond", "Valérie"), // NOI18N.
new Person("Higgins", "Clark"), // NOI18N.
new Person("Pantou", "Maurice"), // NOI18N.
new Person("Parmentier", "Yvette")); // NOI18N.

IV-B - Test

Voilà, nous sommes désormais fins prêts pour effectuer à nouveau nos tests. Les tests de retrait, d’ajout, remplacement et permutation produisent bien sûr les mêmes résultats, mais lorsque nous arrivons aux tests de mise à jour :

1. persons.get(0).setName("Smith"); // NOI18N.

2. persons.get(0).setSurname("Anny"); // NOI18N.

3. System.out.println(persons);

Cette fois-ci la sortie montre bien que nous recevons des événements de modification sur l’élément à l’indice 0.

Element 0 was updated
Element 0 was updated
[Smith Anny, Langworth Cathy, Pantou Maurice, Parmentier Yvette]

Ici, nous n’avons pas modifié le contenu de la liste, nous avons modifié les valeurs des propriétés d’un des objets de la liste ce qui a provoqué des événements de mise à jour pour cet indice.

Et si nous modifions les propriétés de deux éléments distincts :

1. persons.get(1).setSurname("Andy"); // NOI18N.

2. persons.get(2).setName("Cervantes"); // NOI18N.

3. System.out.println(persons);

Nous avons alors des notifications pour chacun des deux éléments impactés :

Element 1 was updated
Element 2 was updated
[Smith Anny, Langworth Andy, Cervantes Maurice, Parmentier Yvette]

V - Exemple

Nous allons maintenant créer une petite interface de saisie pour nous permettre d’éditer les personnes. Nous allons conserver l’exemple simple, mais cela devrait vous donner une idée de ce qu’il est possible de faire avec les extracteurs. Il est tout à fait possible de faire sans extracteur, mais cela demande bien plus de code, d’écouteurs ou de binding sur des propriétés.

Note : le code suivant sera au format JDK10.

Cette interface de test se composera de trois parties :

- À gauche, une table (composant TableView) affiche une vue détaillée des utilisateurs. Un éditeur est placé sous elle et permet d’ajouter, supprimer ou de modifier un utilisateur. C’est cette table qui affichera notre liste observable, en effet, souvenez-vous qu’en JavaFX TableView est construite autour d’une liste observable d’objets et chaque colonne de la table représente une vue spécialisée de cette liste. Ainsi la colonne nom affiche une vue sur la propriété name d’un objet de type Person tandis que la colonne prénom affiche une vue sur la propriété surname de cette même classe.

- Au centre, une liste (composant ListView) se contente d’afficher les utilisateurs. Il s’agit d’une simple vue sur la liste d’objets qui sont affichés dans la table de manière à pouvoir rapidement contrôler les données.

- Enfin à droite nous avons un affichage du résumé des derniers traitements effectués sur nos utilisateurs. Le contenu de ce résumé sera modifié en fonction des événements levés par la liste observable affichée dans la table.

V-A - Montage de l’UI

Nous allons maintenant créer chacune des trois parties composant notre UI en commençant par la table et son éditeur. Il s’agit du genre de composants que vous serez amené à créer et utiliser dans vos propres formulaires et UI.

V-A-1 - Table & éditeur

Commençons par initialiser la table graphique :

Code JDK10

1. // Colonne nom.

2. final var nameColumn = new TableColumn<Person, String>("Nom"); // NOI18N.

3. nameColumn.setCellValueFactory(feature -> feature.getValue().nameProperty());

4. // Ou :

5. //nameColumn.setCellValueFactory(new PropertyValueFactory<>("name")); // NOI18N.

6. // Colonne prénom.

7. final var surnameColumn = new TableColumn<Person, String>("Prénom"); // NOI18N.

8. surnameColumn.setCellValueFactory(feature -> feature.getValue().surnameProperty());

9. // Ou :

10. //surnameColumn.setCellValueFactory(new PropertyValueFactory<>("surname")); // NOI18N.

11. // Table.

12. final var personTableView = new TableView<Person>();

13. personTableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);

14. personTableView.getColumns().addAll(nameColumn, surnameColumn);

15. personTableView.setItems(persons);

16. VBox.setVgrow(personTableView, Priority.ALWAYS);

Nous créons ensuite un éditeur permettant de créer une nouvelle personne, de changer le nom et le prénom ou encore d’effacer une personne existante. Pour rendre l’éditeur actif, nous allons placer un écouteur sur le modèle de sélection de la table, ce qui permettra de réagir lorsque l’utilisateur sélectionne une nouvelle personne avec la souris ou le clavier :

Code JDK10

1. final var persons = personTableView.getItems();

2. // Nom.

3. final var nameField = new TextField();

4. GridPane.setConstraints(nameField, 1, 0);

5. GridPane.setHgrow(nameField, Priority.ALWAYS);

6. final var nameLabel = new Label("Nom"); // NOI18N.

7. nameLabel.setLabelFor(nameField);

8. GridPane.setConstraints(nameLabel, 0, 0);

9. // Prénom.

10. final var surnameField = new TextField();

11. GridPane.setConstraints(surnameField, 1, 1);

12. GridPane.setHgrow(surnameField, Priority.ALWAYS);

13. final var surnameLabel = new Label("Prénom"); // NOI18N.

14. surnameLabel.setLabelFor(surnameField);

15. GridPane.setConstraints(surnameLabel, 0, 1);

16. // Binding booléens.

17. final var hasNoName = nameField.textProperty().isEmpty();

18. final var hasNoSurame = surnameField.textProperty().isEmpty();

19. final var selectionIsNull = personTableView.getSelectionModel().selectedItemProperty().isNull();

20. // Ajout d'une personne.

21. final var addButton = new Button("Ajouter"); // NOI18N.

22. addButton.disableProperty().bind(hasNoName.or(hasNoSurame));

23. addButton.setOnAction(event -> {

24. final var name = nameField.getText().trim();

25. final var surname = surnameField.getText().trim();

26. final var person = new Person(name, surname);

27. persons.add(person);

28. personTableView.getSelectionModel().select(person);

29. });

30. // Mise à jour d'une personne.

31. final var updateButton = new Button("Modifier"); // NOI18N.

32. updateButton.disableProperty().bind(selectionIsNull.or(hasNoName.or(hasNoSurame)));

33. updateButton.setOnAction(event -> {

34. final var name = nameField.getText().trim();

35. final var surname = surnameField.getText().trim();

36. final var person = personTableView.getSelectionModel().getSelectedItem();

37. person.setName(name);

38. person.setSurname(surname);

39. });

40. // Suppression d'une personne.

41. final var removeButton = new Button("Supprimer"); // NOI18N.

42. removeButton.disableProperty().bind(selectionIsNull);

43. removeButton.setOnAction(event -> {

44. final int index = personTableView.getSelectionModel().getSelectedIndex();

45. persons.remove(index);

46. });

47. // Regroupement des boutons.

48. final var buttonHBox = new HBox();

49. buttonHBox.setSpacing(6);

50. buttonHBox.setAlignment(Pos.CENTER_RIGHT);

51. buttonHBox.getChildren().setAll(addButton, updateButton, removeButton);

52. GridPane.setConstraints(buttonHBox, 0, 3, 2, 1);

53. // Mettre à jour l'éditeur lors d'une sélection dans la table.

54. personTableView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> {

55. // Retrait de l'ancien élément.

56. Optional.ofNullable(oldValue)

57. .ifPresent(value -> {

58. nameField.setText(null);

59. surnameField.setText(null);

60. });

61. // Mise en place du nouvel élément.

62. Optional.ofNullable(newValue)

63. .ifPresent(value -> {

64. nameField.setText(value.getName());

65. surnameField.setText(value.getSurname());

66. });

67. });

68. // Mise en place de la grille.

69. final var personEditor = new GridPane();

70. personEditor.setHgap(6);

71. personEditor.setVgap(6);

72. // Configuration des colonnes de la grille.

73. personEditor.getColumnConstraints().setAll(

74. IntStream.range(0, 1)

75. .mapToObj(column -> new ColumnConstraints())

76. .toArray(ColumnConstraints[]::new));

77. // Configuration des lignes de la grille.

78. personEditor.getRowConstraints().setAll(

79. IntStream.range(0, 2)

80. .mapToObj(row -> new RowConstraints())

81. .toArray(RowConstraints[]::new));

82. personEditor.getChildren().setAll(

83. nameLabel, nameField,

84. surnameLabel, surnameField,

85. buttonHBox);

86. VBox.setVgrow(personEditor, Priority.NEVER);

Et enfin nous pouvons assembler les deux contrôles dans notre UI :

Code JDK10

1. // Panneau de gauche.

2. final var leftVBox = new VBox();

3. leftVBox.setSpacing(6);

4. leftVBox.setPadding(new Insets(6));

5. leftVBox.getChildren().setAll(personTableView, personEditor);

V-A-2 - Liste

La mise en place de la liste graphique est beaucoup plus rapide, nous nous contentons de créer une ListView qui a pour source de données la liste même qui est contenue dans la table.

Code JDK10

1. // Liste.

2. final var personListView = new ListView<Person>();

3. personListView.setItems(persons);

V-A-3 - Zone de message

Ici, nous allons utiliser un composant TextFlow qui va nous permettre d’écrire des messages en utilisant du texte avec des mises en forme riches.

Code JDK10

1. final var messageFlow = new TextFlow();

2. AnchorPane.setTopAnchor(messageFlow, 0d);

3. AnchorPane.setLeftAnchor(messageFlow, 0d);

4. AnchorPane.setRightAnchor(messageFlow, 0d);

5. final var messageAnchor = new AnchorPane(messageFlow);

6. final var messageScroll = new ScrollPane(messageAnchor);

V-A-4 - Scène

Et enfin nous allons monter notre UI dans la scène attachée à notre fenêtre en assemblant les trois composants :

Code JDK10

1. // UI finale.

2. final var root = new SplitPane(leftVBox, personListView, messageScroll);

3. final var scene = new Scene(root, 600, 600);

4. primaryStage.setTitle("Gestion des utilisateurs"); // NOI18N.

5. primaryStage.setScene(scene);

6. primaryStage.show();

7. Platform.runLater(() -> root.setDividerPositions(0.33d, 0.66d));

V-B - Réaction aux modifications de la liste

En l’état notre UI fonctionne avec les modifications de notre éditeur qui sont reprises dans la liste graphique puisque nous avons créé une liste de données avec un extracteur. Cependant nous n’avons pas encore de retour dans l’onglet des messages. Pour régler ces soucis, nous devons mettre en place un écouteur sur la liste :

Code JDK10

1. private int added = 0;

2. private int removed = 0;

3. private int updated = 0;

5. [...]

7. persons.addListener((ListChangeListener<Person>.Change change) -> {

8. while (change.next()) {

9. int oldAdded = added;

10. int oldRemoved = removed;

11. int oldUpdated = updated;

12. if (change.wasPermutated()) {

13. } else if (change.wasReplaced()) {

14. } else if (change.wasAdded()) {

15. System.out.println("Was added.");

16. added++;

17. } else if (change.wasRemoved()) {

18. System.out.println("Was removed.");

19. removed++;

20. } else if (change.wasUpdated()) {

21. System.out.println("Was updated.");

22. updated++;

23. }

24. // Mise à jour du message.

25. updateMessage(oldAdded, oldRemoved, oldUpdated, added, removed, updated);

26. }

27. });

V-C - Publication du message

Lorsque la méthode updateMessage() est invoquée, nous recréons le contenu du TextFlow pour signaler les événements d’ajout, de retrait ou de modification :

JDK10

1. private void updateMessage(final int oldAdded, final int oldRemoved, final int oldUpdated, final int newAdded, final int newRemoved, final int newUpdated) {

2. final var bits = messageFlow.getChildren();

3. messageFlow.getChildren().clear();

4. if (newAdded > 0 || newRemoved > 0 || newUpdated > 0) {

5. bits.add(new Text("La liste a été modifiée :")); // NOI18N.

6. if (newAdded > 0) {

7. final var text = new Text("\n\tajout(s) : " + newAdded); // NOI18N.

8. if (oldAdded != newAdded) {

9. text.setFill(Color.RED);

10. }

11. bits.add(text);

12. }

13. if (newRemoved > 0) {

14. final var text = new Text("\n\tretrait(s) : " + newRemoved); // NOI18N.

15. if (oldRemoved != newRemoved) {

16. text.setFill(Color.RED);

17. }

18. bits.add(text);

19. }

20. if (newUpdated > 0) {

21. final var text = new Text("\n\tmodification(s) : " + newUpdated); // NOI18N.

22. if (oldUpdated != newUpdated) {

23. text.setFill(Color.RED);

24. }

25. bits.add(text);

26. }

27. }

28. }

VI - Code source

Le code source de cet article est disponible sur GitHub.com à l’URL https://github.com/fabricebouye/dvp-collection-extractor-javafx. Vous trouverez à cette adresse des projets pour l’IDE JetBrain Intellij IDEA. Certains de ces projets sont disponibles au format JDK7, JDK 8 ou JDK 10.

VII - Conclusion

Dans cet article nous nous sommes penchés sur l’utilisation des extracteurs qui permettent d’ajouter un nouveau niveau de fonctionnalité aux listes observables : nous pouvons désormais être notifiés lorsque l’état d’un objet contenu dans une liste observable est modifié. Et ceci peut être fait sans devoir pour autant coder toute une tuyauterie complexe d’écouteurs sur chacune des propriétés observées.

VIII - 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 pour sa correction orthographique.

(1) Il est également possible d’utiliser la variante de la méthode de fabrique observableList() de la classe utilitaire FXCollections qui prend en paramètres une liste préexistante et un extracteur pour créer une telle liste observable sur une collection qui existe déjà.