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.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
public
final
class
Person {
public
Person
(
final
String name, final
String surname) {
setName
(
name);
setSurname
(
surname);
}
private
final
StringProperty name =
new
SimpleStringProperty
(
this
, "name"
); // NOI18N.
public
final
String getName
(
) {
return
name.get
(
);
}
public
final
void
setName
(
final
String value) {
name.set
(
value);
}
public
final
StringProperty nameProperty
(
) {
return
name;
}
private
final
StringProperty surname =
new
SimpleStringProperty
(
this
, "surname"
); // NOI18N.
public
final
String getSurname
(
) {
return
surname.get
(
);
}
public
final
void
setSurname
(
final
String value) {
surname.set
(
value);
}
public
final
StringProperty surnameProperty
(
) {
return
surname;
}
@Override
public
String toString
(
) {
String name =
getName
(
);
name =
(
name ==
null
) ? ""
: name.trim
(
); // NOI18N.
String surname =
getSurname
(
);
surname =
(
surname ==
null
) ? ""
: surname.trim
(
); // NOI18N.
final
StringBuilder builder =
new
StringBuilder
(
);
if
(!
name.isEmpty
(
)) {
builder.append
(
name.trim
(
));
}
if
(!
name.isEmpty
(
) &&
!
surname.isEmpty
(
)) {
builder.append
(
" "
);
}
if
(!
surname.isEmpty
(
)) {
builder.append
(
surname.trim
(
));
}
return
builder.toString
(
);
}
}
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 :
2.
3.
4.
5.
6.
// Création de la liste.
final
ObservableList<
Person>
persons =
FXCollections.observableArrayList
(
new
Person
(
"Dupond"
, "Valérie"
), // NOI18N.
new
Person
(
"Higgins"
, "Clark"
), // NOI18N.
new
Person
(
"Pantou"
, "Maurice"
), // NOI18N.
new
Person
(
"Parmentier"
, "Yvette"
)); // NOI18N.
Ou encore :
2.
3.
4.
5.
6.
7.
// Création de la liste.
final
ObservableList<
Person>
persons =
FXCollections.observableArrayList
(
);
persons.setAll
(
new
Person
(
"Dupond"
, "Valérie"
), // NOI18N.
new
Person
(
"Higgins"
, "Clark"
), // NOI18N.
new
Person
(
"Pantou"
, "Maurice"
), // NOI18N.
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
// Écouteur.
persons.addListener
(
new
ListChangeListener<
Person>(
) {
@Override
public
void
onChanged
(
ListChangeListener.Change<
? extends
Person>
change) {
// Rappel : plusieurs modifications peuvent être agrégées dans un seul événement.
while
(
change.next
(
)) {
String changeLabel =
"?"
; // NOI18N.
if
(
change.wasReplaced
(
)) {
changeLabel =
"replaced"
; // NOI18N.
}
else
if
(
change.wasAdded
(
)) {
changeLabel =
"added"
; // NOI18N.
}
else
if
(
change.wasRemoved
(
)) {
changeLabel =
"removed"
; // NOI18N.
}
else
if
(
change.wasPermutated
(
)) {
changeLabel =
"permutated"
; // NOI18N.
}
else
if
(
change.wasUpdated
(
)) {
changeLabel =
"updated"
; // NOI18N.
}
final
String pattern =
String.format
(
"Element %s was %s%n"
, "%d"
, changeLabel); // NOI18N.
if
(
change.wasAdded
(
) ||
change.wasReplaced
(
) ||
change.wasUpdated
(
)) {
// Parcours exclusif.
for
(
int
index =
change.getFrom
(
); index <
change.getTo
(
); index++
) {
System.out.printf
(
pattern, index);
}
}
else
{
// Parcours inclusif.
for
(
int
index =
change.getFrom
(
); index <=
change.getTo
(
); index++
) {
System.out.printf
(
pattern, index);
}
}
}
}
}
);
Ou encore :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
// Écouteur.
persons.addListener (
ListChangeListener.Change<
? extends
Person>
change) ->
{
// Rappel : plusieurs modifications peuvent être agrégées dans un seul événement.
while
(
change.next
(
)) {
String changeLabel =
"?"
; // NOI18N.
if
(
change.wasReplaced
(
)) {
changeLabel =
"replaced"
; // NOI18N.
}
else
if
(
change.wasAdded
(
)) {
changeLabel =
"added"
; // NOI18N.
}
else
if
(
change.wasRemoved
(
)) {
changeLabel =
"removed"
; // NOI18N.
}
else
if
(
change.wasPermutated
(
)) {
changeLabel =
"permutated"
; // NOI18N.
}
else
if
(
change.wasUpdated
(
)) {
changeLabel =
"updated"
; // NOI18N.
}
final
String pattern =
String.format
(
"Element %s was %s%n"
, "%d"
, changeLabel); // NOI18N.
if
(
change.wasAdded
(
) ||
change.wasReplaced
(
) ||
change.wasUpdated
(
)) {
// Parcours exclusif.
IntStream.range
(
change.getFrom
(
), change.getTo
(
))
.forEach
(
index ->
System.out.printf
(
pattern, index));
}
else
{
// Parcours inclusif.
IntStream.rangeClosed
(
change.getFrom
(
), change.getTo
(
))
.forEach
(
index ->
System.out.printf
(
pattern, index));
}
}
}
);
III-D. Test▲
Nous pouvons ensuite tester les différents types d’opérations :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
// Opérations.
System.out.println
(
persons);
// Retrait.
System.out.println
(
"== Remove =="
);
persons.remove
(
0
);
System.out.println
(
persons);
// Ajout.
System.out.println
(
"== Add =="
);
persons.add
(
0
, new
Person
(
"Dupond"
, "Valérie"
)); // NOI18N.
System.out.println
(
persons);
// Remplacement.
System.out.println
(
"== Replace =="
);
persons.set
(
0
, new
Person
(
"Langworth"
, "Cathy"
)); // NOI18N.
System.out.println
(
persons);
// Tri -> permutation.
System.out.println
(
"== Permute =="
);
Collections.sort
(
persons, new
Comparator<
Person>(
) {
@Override
public
int
compare
(
Person p1, Person p2) {
int
result =
p1.getName
(
).compareTo
(
p2.getName
(
));
if
(
result ==
0
) {
result =
p1.getSurname
(
).compareTo
(
p2.getSurname
(
));
}
return
result;
}
}
);
System.out.println
(
persons);
// Mise à jour.
System.out.println
(
"== Update =="
);
persons.get
(
0
).setName
(
"Smith"
); // NOI18N.
persons.get
(
0
).setSurname
(
"Anny"
); // NOI18N.
System.out.println
(
persons);
Ou :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
// Opérations.
System.out.println
(
persons);
// Retrait.
System.out.println
(
"== Remove =="
);
persons.remove
(
0
);
System.out.println
(
persons);
// Ajout.
System.out.println
(
"== Add =="
);
persons.add
(
0
, new
Person
(
"Dupond"
, "Valérie"
)); // NOI18N.
System.out.println
(
persons);
// Remplacement.
System.out.println
(
"== Replace =="
);
persons.set
(
0
, new
Person
(
"Langworth"
, "Cathy"
)); // NOI18N.
System.out.println
(
persons);
// Tri -> permutation.
System.out.println
(
"== Permute =="
);
persons.sort
((
p1, p2) ->
{
int
result =
p1.getName
(
).compareTo
(
p2.getName
(
));
if
(
result ==
0
) {
result =
p1.getSurname
(
).compareTo
(
p2.getSurname
(
));
}
return
result;
}
);
System.out.println
(
persons);
// Mise à jour.
System.out.println
(
"== Update =="
);
persons.get
(
0
).setName
(
"Smith"
); // NOI18N.
persons.get
(
0
).setSurname
(
"Anny"
); // NOI18N.
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.
2.
3.
4.
5.
6.
final
ObservableList<
Person>
persons =
FXCollections.observableArrayList
(
new
Callback<
Person, Observable[]>(
) {
@Override
public
Observable[] call
(
final
Person person) {
return
new
ObservableValue[]{
person.nameProperty
(
), person.surnameProperty
(
)}
;
}
}
);
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 !
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 :
2.
3.
persons.get
(
0
).setName
(
"Smith"
); // NOI18N.
persons.get
(
0
).setSurname
(
"Anny"
); // NOI18N.
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 :
2.
3.
persons.get
(
1
).setSurname
(
"Andy"
); // NOI18N.
persons.get
(
2
).setName
(
"Cervantes"
); // NOI18N.
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
// Colonne nom.
final
var
nameColumn =
new
TableColumn<
Person, String>(
"Nom"
); // NOI18N.
nameColumn.setCellValueFactory
(
feature ->
feature.getValue
(
).nameProperty
(
));
// Ou :
//nameColumn.setCellValueFactory(new PropertyValueFactory<>("name")); // NOI18N.
// Colonne prénom.
final
var
surnameColumn =
new
TableColumn<
Person, String>(
"Prénom"
); // NOI18N.
surnameColumn.setCellValueFactory
(
feature ->
feature.getValue
(
).surnameProperty
(
));
// Ou :
//surnameColumn.setCellValueFactory(new PropertyValueFactory<>("surname")); // NOI18N.
// Table.
final
var
personTableView =
new
TableView<
Person>(
);
personTableView.setColumnResizePolicy
(
TableView.CONSTRAINED_RESIZE_POLICY);
personTableView.getColumns
(
).addAll
(
nameColumn, surnameColumn);
personTableView.setItems
(
persons);
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
82.
83.
84.
85.
86.
final
var
persons =
personTableView.getItems
(
);
// Nom.
final
var
nameField =
new
TextField
(
);
GridPane.setConstraints
(
nameField, 1
, 0
);
GridPane.setHgrow
(
nameField, Priority.ALWAYS);
final
var
nameLabel =
new
Label
(
"Nom"
); // NOI18N.
nameLabel.setLabelFor
(
nameField);
GridPane.setConstraints
(
nameLabel, 0
, 0
);
// Prénom.
final
var
surnameField =
new
TextField
(
);
GridPane.setConstraints
(
surnameField, 1
, 1
);
GridPane.setHgrow
(
surnameField, Priority.ALWAYS);
final
var
surnameLabel =
new
Label
(
"Prénom"
); // NOI18N.
surnameLabel.setLabelFor
(
surnameField);
GridPane.setConstraints
(
surnameLabel, 0
, 1
);
// Binding booléens.
final
var
hasNoName =
nameField.textProperty
(
).isEmpty
(
);
final
var
hasNoSurame =
surnameField.textProperty
(
).isEmpty
(
);
final
var
selectionIsNull =
personTableView.getSelectionModel
(
).selectedItemProperty
(
).isNull
(
);
// Ajout d'une personne.
final
var
addButton =
new
Button
(
"Ajouter"
); // NOI18N.
addButton.disableProperty
(
).bind
(
hasNoName.or
(
hasNoSurame));
addButton.setOnAction
(
event ->
{
final
var
name =
nameField.getText
(
).trim
(
);
final
var
surname =
surnameField.getText
(
).trim
(
);
final
var
person =
new
Person
(
name, surname);
persons.add
(
person);
personTableView.getSelectionModel
(
).select
(
person);
}
);
// Mise à jour d'une personne.
final
var
updateButton =
new
Button
(
"Modifier"
); // NOI18N.
updateButton.disableProperty
(
).bind
(
selectionIsNull.or
(
hasNoName.or
(
hasNoSurame)));
updateButton.setOnAction
(
event ->
{
final
var
name =
nameField.getText
(
).trim
(
);
final
var
surname =
surnameField.getText
(
).trim
(
);
final
var
person =
personTableView.getSelectionModel
(
).getSelectedItem
(
);
person.setName
(
name);
person.setSurname
(
surname);
}
);
// Suppression d'une personne.
final
var
removeButton =
new
Button
(
"Supprimer"
); // NOI18N.
removeButton.disableProperty
(
).bind
(
selectionIsNull);
removeButton.setOnAction
(
event ->
{
final
int
index =
personTableView.getSelectionModel
(
).getSelectedIndex
(
);
persons.remove
(
index);
}
);
// Regroupement des boutons.
final
var
buttonHBox =
new
HBox
(
);
buttonHBox.setSpacing
(
6
);
buttonHBox.setAlignment
(
Pos.CENTER_RIGHT);
buttonHBox.getChildren
(
).setAll
(
addButton, updateButton, removeButton);
GridPane.setConstraints
(
buttonHBox, 0
, 3
, 2
, 1
);
// Mettre à jour l'éditeur lors d'une sélection dans la table.
personTableView.getSelectionModel
(
).selectedItemProperty
(
).addListener
((
observable, oldValue, newValue) ->
{
// Retrait de l'ancien élément.
Optional.ofNullable
(
oldValue)
.ifPresent
(
value ->
{
nameField.setText
(
null
);
surnameField.setText
(
null
);
}
);
// Mise en place du nouvel élément.
Optional.ofNullable
(
newValue)
.ifPresent
(
value ->
{
nameField.setText
(
value.getName
(
));
surnameField.setText
(
value.getSurname
(
));
}
);
}
);
// Mise en place de la grille.
final
var
personEditor =
new
GridPane
(
);
personEditor.setHgap
(
6
);
personEditor.setVgap
(
6
);
// Configuration des colonnes de la grille.
personEditor.getColumnConstraints
(
).setAll
(
IntStream.range
(
0
, 1
)
.mapToObj
(
column ->
new
ColumnConstraints
(
))
.toArray
(
ColumnConstraints[]::new
));
// Configuration des lignes de la grille.
personEditor.getRowConstraints
(
).setAll
(
IntStream.range
(
0
, 2
)
.mapToObj
(
row ->
new
RowConstraints
(
))
.toArray
(
RowConstraints[]::new
));
personEditor.getChildren
(
).setAll
(
nameLabel, nameField,
surnameLabel, surnameField,
buttonHBox);
VBox.setVgrow
(
personEditor, Priority.NEVER);
Et enfin nous pouvons assembler les deux contrôles dans notre UI :
2.
3.
4.
5.
// Panneau de gauche.
final
var
leftVBox =
new
VBox
(
);
leftVBox.setSpacing
(
6
);
leftVBox.setPadding
(
new
Insets
(
6
));
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.
2.
3.
// Liste.
final
var
personListView =
new
ListView<
Person>(
);
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.
2.
3.
4.
5.
6.
final
var
messageFlow =
new
TextFlow
(
);
AnchorPane.setTopAnchor
(
messageFlow, 0
d);
AnchorPane.setLeftAnchor
(
messageFlow, 0
d);
AnchorPane.setRightAnchor
(
messageFlow, 0
d);
final
var
messageAnchor =
new
AnchorPane
(
messageFlow);
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 :
2.
3.
4.
5.
6.
7.
// UI finale.
final
var
root =
new
SplitPane
(
leftVBox, personListView, messageScroll);
final
var
scene =
new
Scene
(
root, 600
, 600
);
primaryStage.setTitle
(
"Gestion des utilisateurs"
); // NOI18N.
primaryStage.setScene
(
scene);
primaryStage.show
(
);
Platform.runLater
((
) ->
root.setDividerPositions
(
0.33
d, 0.66
d));
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
private
int
added =
0
;
private
int
removed =
0
;
private
int
updated =
0
;
[...]
persons.addListener
((
ListChangeListener<
Person>
.Change change) ->
{
while
(
change.next
(
)) {
int
oldAdded =
added;
int
oldRemoved =
removed;
int
oldUpdated =
updated;
if
(
change.wasPermutated
(
)) {
}
else
if
(
change.wasReplaced
(
)) {
}
else
if
(
change.wasAdded
(
)) {
System.out.println
(
"Was added."
);
added++
;
}
else
if
(
change.wasRemoved
(
)) {
System.out.println
(
"Was removed."
);
removed++
;
}
else
if
(
change.wasUpdated
(
)) {
System.out.println
(
"Was updated."
);
updated++
;
}
// Mise à jour du message.
updateMessage
(
oldAdded, oldRemoved, oldUpdated, added, removed, updated);
}
}
);
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 :
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
private
void
updateMessage
(
final
int
oldAdded, final
int
oldRemoved, final
int
oldUpdated, final
int
newAdded, final
int
newRemoved, final
int
newUpdated) {
final
var
bits =
messageFlow.getChildren
(
);
messageFlow.getChildren
(
).clear
(
);
if
(
newAdded >
0
||
newRemoved >
0
||
newUpdated >
0
) {
bits.add
(
new
Text
(
"La liste a été modifiée :"
)); // NOI18N.
if
(
newAdded >
0
) {
final
var
text =
new
Text
(
"
\n\t
ajout(s) : "
+
newAdded); // NOI18N.
if
(
oldAdded !=
newAdded) {
text.setFill
(
Color.RED);
}
bits.add
(
text);
}
if
(
newRemoved >
0
) {
final
var
text =
new
Text
(
"
\n\t
retrait(s) : "
+
newRemoved); // NOI18N.
if
(
oldRemoved !=
newRemoved) {
text.setFill
(
Color.RED);
}
bits.add
(
text);
}
if
(
newUpdated >
0
) {
final
var
text =
new
Text
(
"
\n\t
modification(s) : "
+
newUpdated); // NOI18N.
if
(
oldUpdated !=
newUpdated) {
text.setFill
(
Color.RED);
}
bits.add
(
text);
}
}
}
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.