1. Introduction▲
Cet article est la suite de Création interface graphique avec Swing : les basesCréation interface graphique avec Swing : les bases qui vous a présenté les bases du développement d'interfaces graphiques avec Swing. Je vous conseille de lire le précédent article indiqué ci-dessus avant de lire celui-ci qui en est le prolongement.
Cet article va vous présenter la création de tableaux en Swing. Ce composant pose souvent des problèmes lorsqu'on débute avec Swing. Je vais donc tenter d'expliquer les différents concepts liés à l'utilisation de tableaux en Swing.
Dans cet article, nous verrons donc les concepts de base du composant JTable, la définition de modèle de tableaux, la modification dynamique du contenu du tableau, la façon de modifier l'affichage des différentes cellules du tableau, la modification directe du contenu du tableau et finalement le tri et le filtrage du tableau.
2. Concepts de base▲
Un JTable est donc un composant Swing permettant d'afficher un tableau formé d'un certain nombre de lignes et d'un certain nombre de colonnes. En plus des lignes de contenu, le JTable a également une ligne d'en-tête présentant un titre pour chaque colonne.
Un JTable a donc d'un côté des données et de l'autre des données d'en-tête. On peut voir les données comme un tableau à deux dimensions dans lequel chaque valeur correspond à la valeur d'une cellule du tableau. Quant aux en-têtes, on peut les voir comme un tableau de chaînes de caractères.
Le JTable utilise différents concepts de Swing :
- un modèle pour stocker les données. Un JTable utilise une classe implémentant TableModel. Nous verrons plus loin comment spécifier un modèle pour un Jtable ;
- un renderer pour le rendu des cellules. On peut spécifier un TableCellRenderer pour chaque classe de données. Nous verrons plus loin ce que ça signifie exactement ;
- un éditeur pour l'édition du contenu d'une cellule. On peut spécifier un TableCellEditor pour chaque classe de données. Encore une fois, nous approfondirons ce concept plus loin.
Tout au long de cet article, on va développer un petit programme très simple permettant de gérer une liste d'amis. Voici les caractéristiques d'un ami :
- un nom et un prénom (classe String) ;
- une couleur préférée (classe Color) ;
- un sexe (booléen homme/femme) ;
- un sport qu'on pratique avec lui (énumération Sport).
Voici notre énumération Sport :
public
enum
Sport {
TENNIS,
FOOTBALL,
NATATION,
RIEN;
}
On va donc commencer par une première version, des plus basiques, de notre application.
La façon la plus simple, mais pas la meilleure est de passer directement à JTable un tableau à deux dimensions pour les données et un tableau à une dimension pour l'en-tête de chaque colonne.
Voici donc l'implémentation la plus basique qui soit de notre programme :
public
class
JTableBasiqueAvecPanel extends
JFrame {
public
JTableBasiqueAvecPanel
(
) {
super
(
);
setTitle
(
"JTable basique dans un JPanel"
);
setDefaultCloseOperation
(
JFrame.EXIT_ON_CLOSE);
Object[][] donnees =
{
{
"Johnathan"
, "Sykes"
, Color.red, true
, Sport.TENNIS}
,
{
"Nicolas"
, "Van de Kampf"
, Color.black, true
, Sport.FOOTBALL}
,
{
"Damien"
, "Cuthbert"
, Color.cyan, true
, Sport.RIEN}
,
{
"Corinne"
, "Valance"
, Color.blue, false
, Sport.NATATION}
,
{
"Emilie"
, "Schrödinger"
, Color.magenta, false
, Sport.FOOTBALL}
,
{
"Delphine"
, "Duke"
, Color.yellow, false
, Sport.TENNIS}
,
{
"Eric"
, "Trump"
, Color.pink, true
, Sport.FOOTBALL}
,
}
;
String[] entetes =
{
"Prénom"
, "Nom"
, "Couleur favorite"
, "Homme"
, "Sport"
}
;
JTable tableau =
new
JTable
(
donnees, entetes);
getContentPane
(
).add
(
tableau.getTableHeader
(
), BorderLayout.NORTH);
getContentPane
(
).add
(
tableau, BorderLayout.CENTER);
pack
(
);
}
public
static
void
main
(
String[] args) {
new
JTableBasiqueAvecPanel
(
).setVisible
(
true
);
}
On utilise donc le constructeur JTable(Object[][] data, Object[] entetes) pour gérer nos données et nos en-têtes. Pour ajouter le JTable dans un JPanel, il faut ajouter séparément le header du tableau et le tableau en lui-même.
Cela nous donnera le résultat suivant :
Avec très peu de code, nous avons donc un tableau fonctionnel. Néanmoins, cette première implémentation souffre de plusieurs défauts :
- On ne peut pas afficher plus de lignes qu'il n'y a d'espace disponible sur la fenêtre ;
- Les données sont statiques et immuables ;
- On ne peut pas gérer la façon dont seront affichées les données ;
- Aucune distinction entre les données et la vue ;
- Les colonnes couleurs et Homme ne sont pas très esthétiques.
Le point 1 est très vite corrigible. En fait, la bonne façon d'ajouter un JTable dans un JPanel est de passer par un JScrollPane qui permettra d'afficher plus de lignes que la fenêtre ne le permet. Pour la suite de cet article, on va donc toujours utiliser un JScrollPane. Voici donc une deuxième version avec JScrollPane :
public
class
JTableBasiqueAvecScrollPane extends
JFrame {
public
JTableBasiqueAvecScrollPane
(
) {
super
(
);
setTitle
(
"JTable basique dans un JScrollPane"
);
setDefaultCloseOperation
(
JFrame.EXIT_ON_CLOSE);
Object[][] donnees =
{
{
"Johnathan"
, "Sykes"
, Color.red, true
, Sport.TENNIS}
,
{
"Nicolas"
, "Van de Kampf"
, Color.black, true
, Sport.FOOTBALL}
,
{
"Damien"
, "Cuthbert"
, Color.cyan, true
, Sport.RIEN}
,
{
"Corinne"
, "Valance"
, Color.blue, false
, Sport.NATATION}
,
{
"Emilie"
, "Schrödinger"
, Color.magenta, false
, Sport.FOOTBALL}
,
{
"Delphine"
, "Duke"
, Color.yellow, false
, Sport.TENNIS}
,
{
"Eric"
, "Trump"
, Color.pink, true
, Sport.FOOTBALL}
,
}
;
String[] entetes =
{
"Prénom"
, "Nom"
, "Couleur favorite"
, "Homme"
, "Sport"
}
;
JTable tableau =
new
JTable
(
donnees, entetes);
getContentPane
(
).add
(
new
JScrollPane
(
tableau), BorderLayout.CENTER);
pack
(
);
}
public
static
void
main
(
String[] args) {
new
JTableBasiqueAvecScrollPane
(
).setVisible
(
true
);
}
}
Cette fois, on ajoute directement le JTable dans le JScrollPane. Voici le résultat :
Cette version est déjà meilleure que la précédente, mais n'est de loin pas encore parfaite, nous allons encore l'améliorer dans les chapitres suivants.
3. Le modèle du tableau▲
Une chose indispensable à faire lorsqu'on utilise des JTable est d'utiliser un modèle de tableau pour stocker les données. Il faut donc créer une classe étendant TableModel. En pratique, on implémente rarement directement TableModel, mais on hérite plutôt d'AbstractTableModel et l'on redéfinit les méthodes nécessaires. Pour commencer, voici les méthodes qu'il faudra redéfinir pour notre modèle de données statique :
- int getRowCount() : doit retourner le nombre de lignes du tableau ;
- int getColumnCount() : doit retourner le nombre de colonnes du tableau ;
- Object getValueAt(int rowIndex, int columnIndex) : doit retourner la valeur du tableau à la colonne et la ligne spécifiées ;
- String getColumnName(int columnIndex) : doit retourner l'en-tête de la colonne spécifiée.
On va donc créer notre premier modèle en reprenant les données dans un tableau à deux dimensions pour commencer :
public
class
ModeleStatique extends
AbstractTableModel {
private
final
Object[][] donnees;
private
final
String[] entetes =
{
"Prénom"
, "Nom"
, "Couleur favorite"
, "Homme"
, "Sport"
}
;
public
ModeleStatique
(
) {
super
(
);
donnees =
new
Object[][]{
{
"Johnathan"
, "Sykes"
, Color.red, true
, Sport.TENNIS}
,
{
"Nicolas"
, "Van de Kampf"
, Color.black, true
, Sport.FOOTBALL}
,
{
"Damien"
, "Cuthbert"
, Color.cyan, true
, Sport.RIEN}
,
{
"Corinne"
, "Valance"
, Color.blue, false
, Sport.NATATION}
,
{
"Emilie"
, "Schrödinger"
, Color.magenta, false
, Sport.FOOTBALL}
,
{
"Delphine"
, "Duke"
, Color.yellow, false
, Sport.TENNIS}
,
{
"Eric"
, "Trump"
, Color.pink, true
, Sport.FOOTBALL}
,
}
;
}
public
int
getRowCount
(
) {
return
donnees.length;
}
public
int
getColumnCount
(
) {
return
entetes.length;
}
public
String getColumnName
(
int
columnIndex) {
return
entetes[columnIndex];
}
public
Object getValueAt
(
int
rowIndex, int
columnIndex) {
return
donnees[rowIndex][columnIndex];
}
}
Et on modifie cette fois notre JTable pour utiliser ce modèle :
public
class
JTableBasiqueAvecModeleStatique extends
JFrame {
public
JTableBasiqueAvecModeleStatique
(
) {
super
(
);
setTitle
(
"JTable avec modèle statique"
);
setDefaultCloseOperation
(
JFrame.EXIT_ON_CLOSE);
JTable tableau =
new
JTable
(
new
ModeleStatique
(
));
getContentPane
(
).add
(
new
JScrollPane
(
tableau), BorderLayout.CENTER);
pack
(
);
}
public
static
void
main
(
String[] args) {
new
JTableBasiqueAvecModeleStatique
(
).setVisible
(
true
);
}
}
On a donc créé une classe héritant de AbstractTableModel et redéfinissant les méthodes indispensables. Les données sont toujours stockées de la même manière, mais cette solution est plus souple et beaucoup plus propre. Si l'on regarde la JFrame, on peut voir qu'il n'y a plus aucune donnée dans cette classe, ce qui est donc beaucoup plus propre en termes de découplage. En plus de cela, on est maintenant maîtres de nos données et de la façon dont elles sont stockées. Mais au niveau de l'affichage, rien n'a changé :
On a maintenant une bonne base, mais on va encore l'améliorer. En règle générale, il est extrêmement rare de voir des données sous la forme de tableaux à deux dimensions. En général, Java étant un langage orienté objet, on manipule des objets. On va donc créer un objet Ami qui va représenter un de nos amis :
public
class
Ami {
private
String nom;
private
String prenom;
private
Color couleur;
private
boolean
homme;
private
Sport sport;
public
Ami
(
String nom, String prenom, Color couleur, boolean
homme, Sport sport) {
super
(
);
this
.nom =
nom;
this
.prenom =
prenom;
this
.couleur =
couleur;
this
.homme =
homme;
this
.sport =
sport;
}
public
String getNom
(
) {
return
nom;
}
public
void
setNom
(
String nom) {
this
.nom =
nom;
}
public
String getPrenom
(
) {
return
prenom;
}
public
void
setPrenom
(
String prenom) {
this
.prenom =
prenom;
}
public
Color getCouleur
(
) {
return
couleur;
}
public
void
setCouleur
(
Color couleur) {
this
.couleur =
couleur;
}
public
boolean
isHomme
(
) {
return
homme;
}
public
void
setHomme
(
boolean
homme) {
this
.homme =
homme;
}
public
Sport getSport
(
) {
return
sport;
}
public
void
setSport
(
Sport sport) {
this
.sport =
sport;
}
}
Une simple classe de données toute bête. Et on va prendre en compte ceci dans notre modèle :
public
class
ModeleStatiqueObjet extends
AbstractTableModel {
private
final
Ami[] amis;
private
final
String[] entetes =
{
"Prénom"
, "Nom"
, "Couleur favorite"
, "Homme"
, "Sport"
}
;
public
ModeleStatiqueObjet
(
) {
super
(
);
amis =
new
Ami[]{
new
Ami
(
"Johnathan"
, "Sykes"
, Color.red, true
, Sport.TENNIS),
new
Ami
(
"Nicolas"
, "Van de Kampf"
, Color.black, true
, Sport.FOOTBALL),
new
Ami
(
"Damien"
, "Cuthbert"
, Color.cyan, true
, Sport.RIEN),
new
Ami
(
"Corinne"
, "Valance"
, Color.blue, false
, Sport.NATATION),
new
Ami
(
"Emilie"
, "Schrödinger"
, Color.magenta, false
, Sport.FOOTBALL),
new
Ami
(
"Delphine"
, "Duke"
, Color.yellow, false
, Sport.TENNIS),
new
Ami
(
"Eric"
, "Trump"
, Color.pink, true
, Sport.FOOTBALL)
}
;
}
public
int
getRowCount
(
) {
return
amis.length;
}
public
int
getColumnCount
(
) {
return
entetes.length;
}
public
String getColumnName
(
int
columnIndex) {
return
entetes[columnIndex];
}
public
Object getValueAt
(
int
rowIndex, int
columnIndex) {
switch
(
columnIndex){
case
0
:
return
amis[rowIndex].getPrenom
(
);
case
1
:
return
amis[rowIndex].getNom
(
);
case
2
:
return
amis[rowIndex].getCouleur
(
);
case
3
:
return
amis[rowIndex].isHomme
(
);
case
4
:
return
amis[rowIndex].getSport
(
);
default
:
return
null
; //Ne devrait jamais arriver
}
}
}
Cette fois, le code commence à devenir intéressant. C'est là qu'on commence à comprendre l'utilité d'utiliser un modèle et non pas directement le constructeur de JTable. Si maintenant par exemple, on veut inverser deux colonnes et mettre « prénom » après « nom », il suffit d'inverser les deux colonnes dans la liste des colonnes et d'inverser les deux return de la méthode getValueAt() alors que ceci aurait été beaucoup plus difficile et contraignant avec un tableau d'objets.
Pour ce qui est de l'affichage, il suffit d'utiliser le nouveau modèle au lieu de l'ancien :
public
class
JTableBasiqueAvecModeleStatiqueObjet extends
JFrame {
public
JTableBasiqueAvecModeleStatiqueObjet
(
) {
super
(
);
setTitle
(
"JTable avec modèle statique et des objets"
);
setDefaultCloseOperation
(
JFrame.EXIT_ON_CLOSE);
JTable tableau =
new
JTable
(
new
ModeleStatiqueObjet
(
));
getContentPane
(
).add
(
new
JScrollPane
(
tableau), BorderLayout.CENTER);
pack
(
);
}
public
static
void
main
(
String[] args) {
new
JTableBasiqueAvecModeleStatiqueObjet
(
).setVisible
(
true
);
}
}
Rien ne change au niveau du rendu. Dans le chapitre suivant, on va rendre dynamique notre modèle en permettant l'ajout et le retrait d'ami.
4. Ajouter/Supprimer des lignes▲
On va maintenant passer à quelque chose qui va rendre notre opération un peu plus intéressante et surtout rendre notre modèle indispensable. C'est-à -dire donner à l'utilisateur la possibilité de modifier le contenu du tableau. Pour le moment, on va se contenter d'ajouter et de supprimer des lignes. On verra au chapitre 6 comment modifier des valeurs dans le tableau.
La première chose à faire est donc de rendre notre modèle dynamique. Pour cela, on va donc ajouter des méthodes addAmi et removeAmi. Pour avertir le JTable qu'il y a eu des modifications sur le modèle, il faut appeler les méthodes fireXXX qui sont définies dans AbstractTableModel. En plus de cela, il faut bien évidemment utiliser une structure de données qui soit dynamique. Le tableau n'est pas du tout adapté. On va donc utiliser cette fois une ArrayList. Voici donc ce que pourrait donner notre modèle dynamique :
public
class
ModeleDynamiqueObjet extends
AbstractTableModel {
private
final
List<
Ami>
amis =
new
ArrayList<
Ami>(
);
private
final
String[] entetes =
{
"Prénom"
, "Nom"
, "Couleur favorite"
, "Homme"
, "Sport"
}
;
public
ModeleDynamiqueObjet
(
) {
super
(
);
amis.add
(
new
Ami
(
"Johnathan"
, "Sykes"
, Color.red, true
, Sport.TENNIS));
amis.add
(
new
Ami
(
"Nicolas"
, "Van de Kampf"
, Color.black, true
, Sport.FOOTBALL));
amis.add
(
new
Ami
(
"Damien"
, "Cuthbert"
, Color.cyan, true
, Sport.RIEN));
amis.add
(
new
Ami
(
"Corinne"
, "Valance"
, Color.blue, false
, Sport.NATATION));
amis.add
(
new
Ami
(
"Emilie"
, "Schrödinger"
, Color.magenta, false
, Sport.FOOTBALL));
amis.add
(
new
Ami
(
"Delphine"
, "Duke"
, Color.yellow, false
, Sport.TENNIS));
amis.add
(
new
Ami
(
"Eric"
, "Trump"
, Color.pink, true
, Sport.FOOTBALL));
}
public
int
getRowCount
(
) {
return
amis.size
(
);
}
public
int
getColumnCount
(
) {
return
entetes.length;
}
public
String getColumnName
(
int
columnIndex) {
return
entetes[columnIndex];
}
public
Object getValueAt
(
int
rowIndex, int
columnIndex) {
switch
(
columnIndex){
case
0
:
return
amis.get
(
rowIndex).getPrenom
(
);
case
1
:
return
amis.get
(
rowIndex).getNom
(
);
case
2
:
return
amis.get
(
rowIndex).getCouleur
(
);
case
3
:
return
amis.get
(
rowIndex).isHomme
(
);
case
4
:
return
amis.get
(
rowIndex).getSport
(
);
default
:
return
null
; //Ne devrait jamais arriver
}
}
public
void
addAmi
(
Ami ami) {
amis.add
(
ami);
fireTableRowsInserted
(
amis.size
(
) -
1
, amis.size
(
) -
1
);
}
public
void
removeAmi
(
int
rowIndex) {
amis.remove
(
rowIndex);
fireTableRowsDeleted
(
rowIndex, rowIndex);
}
}
Rien de bien compliqué donc. Pour la méthode add(), on ajoute le nouvel Ami dans la liste ensuite de quoi on prévient la JTable qu'un nouvel élément a été inséré. Pour la méthode remove() le principe est le même, on commence par supprimer l'élément de la liste et enfin on prévient le tableau qu'un élément a été supprimé. On va ajouter deux actions dans notre interface graphique. La première va ajouter un ami (pour simplifier, cela va toujours rajouter le même objet, alors qu'en réalité, il faudrait proposer à l'utilisateur de configurer le nouvel ami) et la seconde va supprimer le ou les éléments sélectionnés. Voici ce que ça va nous donner :
public
class
JTableBasiqueAvecModeleDynamiqueObjet extends
JFrame {
private
ModeleDynamiqueObjet modele =
new
ModeleDynamiqueObjet
(
);
private
JTable tableau;
public
JTableBasiqueAvecModeleDynamiqueObjet
(
) {
super
(
);
setTitle
(
"JTable avec modèle dynamique"
);
setDefaultCloseOperation
(
JFrame.EXIT_ON_CLOSE);
tableau =
new
JTable
(
modele);
getContentPane
(
).add
(
new
JScrollPane
(
tableau), BorderLayout.CENTER);
JPanel boutons =
new
JPanel
(
);
boutons.add
(
new
JButton
(
new
AddAction
(
)));
boutons.add
(
new
JButton
(
new
RemoveAction
(
)));
getContentPane
(
).add
(
boutons, BorderLayout.SOUTH);
pack
(
);
}
public
static
void
main
(
String[] args) {
new
JTableBasiqueAvecModeleDynamiqueObjet
(
).setVisible
(
true
);
}
private
class
AddAction extends
AbstractAction {
private
AddAction
(
) {
super
(
"Ajouter"
);
}
public
void
actionPerformed
(
ActionEvent e) {
modele.addAmi
(
new
Ami
(
"Megan"
, "Sami"
, Color.green, false
, Sport.NATATION));
}
}
private
class
RemoveAction extends
AbstractAction {
private
RemoveAction
(
) {
super
(
"Supprimmer"
);
}
public
void
actionPerformed
(
ActionEvent e) {
int
[] selection =
tableau.getSelectedRows
(
);
for
(
int
i =
selection.length -
1
; i >=
0
; i--
){
modele.removeAmi
(
selection[i]);
}
}
}
}
L'action pour ajouter un ami ne fait rien de bien spécial et est plutôt triviale. Par contre, il y a quelques petites choses à dire sur l'action de suppression. Tout d'abord, il faut savoir qu'une JTable peut fonctionner selon plusieurs modes de sélection qui sont configurables via la méthode setSelectionMode. Le paramètre mode est une des valeurs suivantes venant de ListSelectionModel :
- SINGLE_SELECTION : permet de sélectionner une seule ligne ;
- SINGLE_INTERVAL_SELECTION : permet de sélectionner un intervalle de ligne ;
- MULTIPLE_INTERVAL_SELECTION : permet de sélectionner de multiples intervalles. C'est la valeur par défaut.
Il faut donc comprendre que le tableau de lignes renvoyé par la méthode getSelectedRows() peut retourner plusieurs intervalles. Les résultats sont retournés dans l'ordre ascendant. Il nous faut donc les supprimer depuis la fin, sinon on fausserait les résultats.
Cela va nous donner l'affichage suivant :
Comme vous pouvez le constater, on vient de construire un tableau tout à fait dynamique sans grands problèmes. Dans le prochain chapitre, on va maintenant résoudre le problème des colonnes Couleur et Homme qui ne sont pas très pratiques en l'état.
5. L'affichage des cellules▲
On va maintenant passer à la personnalisation de l'affichage des différentes cellules. Voici ce qu'on va effectuer comme changements :
- afficher la couleur au lieu du toString() de Color ;
- afficher l'image du sexe au lieu d'un booléen ;
- afficher le nom d'un ami en gras.
Pour cela, il va falloir commencer par spécifier dans le modèle à quelle classe correspond chacune colonne. On ne peut configurer des renderers que par colonne. À la suite de quoi, on configurera les renderers pour chaque classe de colonne au niveau de la JTable. Voici donc la première chose à faire. Il suffit de redéfinir la méthode getColumnClass() dans notre modèle :
@Override
public
Class getColumnClass
(
int
columnIndex){
switch
(
columnIndex){
case
2
:
return
Color.class
;
case
3
:
return
Boolean.class
;
default
:
return
Object.class
;
}
}
À noter que ceci n'est pas vraiment indispensable, car cela est automatiquement fait par AbstractTableModel. Mais je trouve personnellement cela plus clair ainsi.
On va maintenant créer nos renderers. Un renderer est simplement une classe implémentant TableCellRenderer qui est une interface ne contenant qu'une seule méthode retournant un composant Swing. En pratique, on hérite généralement de DefaultCellRenderer qui représente un JLabel comme renderer.
Il faut éviter dans la mesure du possible de renvoyer un nouvel objet dans un renderer si on a beaucoup d'éléments dans notre JTable. Cela voudrait dire qu'il faudrait créer un objet pour chaque ligne et à chaque fois qu'on redessine la JTable, ce qui peut dégrader très fortement les performances. C'est pourquoi on garde un même objet qu'on modifie pour chaque cellule.
On va donc créer notre premier renderer qui va simplement modifier le background du JLabel avec la couleur favorite de l'élément courant :
public
class
ColorCellRenderer extends
DefaultTableCellRenderer {
@Override
public
Component getTableCellRendererComponent
(
JTable table, Object value, boolean
isSelected, boolean
hasFocus, int
row, int
column) {
super
.getTableCellRendererComponent
(
table, value, isSelected, hasFocus, row, column);
Color color =
(
Color) value;
setText
(
""
);
setBackground
(
color);
return
this
;
}
}
C'est donc extrêmement simple d'implémenter ce renderer. Il suffit de récupérer la couleur de l'ami et de la mettre en background de notre JLabel. On peut donc passer au suivant. Cette fois, on va afficher une image pour le sexe de la personne (il faut aussi penser à modifier l'en-tête de la colonne pour mettre Sexe au lieu d'homme).
public
class
SexeCellRenderer extends
DefaultTableCellRenderer {
private
Icon manImage;
private
Icon womanImage;
public
SexeCellRenderer
(
) {
super
(
);
manImage =
new
ImageIcon
(
"man.png"
);
womanImage =
new
ImageIcon
(
"woman.png"
);
}
@Override
public
Component getTableCellRendererComponent
(
JTable table, Object value, boolean
isSelected, boolean
hasFocus, int
row, int
column) {
super
.getTableCellRendererComponent
(
table, value, isSelected, hasFocus, row, column);
Boolean homme =
(
Boolean)value;
setText
(
""
);
if
(
homme){
setIcon
(
manImage);
}
else
{
setIcon
(
womanImage);
}
return
this
;
}
}
On commence donc par charger les images dans le constructeur ensuite de quoi dans la méthode de rendu, en fonction du sexe de la personne, on affiche la bonne image. On peut donc passer au dernier renderer :
public class BoldCellRenderer extends DefaultTableCellRenderer {
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
setFont(getFont().deriveFont(Font.BOLD));
return this;
}
}
Rien à dire de ce côté, c'est plus que trivial. On va maintenant configurer le tout dans notre tableau :
tableau.setDefaultRenderer
(
Boolean.class
, new
SexeCellRenderer
(
));
tableau.setDefaultRenderer
(
Color.class
, new
ColorCellRenderer
(
));
tableau.getColumnModel
(
).getColumn
(
1
).setCellRenderer
(
new
BoldCellRenderer
(
));
Pour les deux premiers, on peut directement les lier à une classe de colonne, mais pour le renderer qui met en gras le texte, on ne peut pas le lier à String puisqu'on veut seulement le mettre sur une colonne et non les deux. Ceci va nous donner l'affichage suivant :
Cette fois on a déjà quelque chose de beaucoup plus intéressant visuellement. Vous pouvez faire des renderers beaucoup plus évolués avec d'autres composants qu'un JLabel comme des JPanel ou même pourquoi pas une JTable.
Dans le prochain chapitre, on va permettre la modification directe des valeurs de la JTable.
6. Permettre de modifier les cellules▲
On va maintenant rendre notre tableau éditable pour que l'utilisateur puisse modifier un ami. Pour cela, il va falloir commencer par rendre notre modèle éditable. Pour cela, il faut implémenter la méthode isCellEditable(int row, int column) qui va indiquer quelles sont les cellules éditables. Dans notre tableau, toutes les cellules seront éditables. Ensuite, il faut prendre en compte les modifications. Pour cela, il faut implémenter la méthode setValueAt(Object value, int column, int row) qui est automatiquement appelée lorsque l'utilisateur valide sa modification. En plus de cela, on va également modifier notre méthode getColumnClass pour ajouter la classe Sport pour la colonne 4 puisqu'on voudra également le modifier. Voici ce que vont donner ces méthodes dans notre modèle éditable :
@Override
public
boolean
isCellEditable
(
int
rowIndex, int
columnIndex) {
return
true
; //Toutes les cellules éditables
}
@Override
public
void
setValueAt
(
Object aValue, int
rowIndex, int
columnIndex) {
if
(
aValue !=
null
){
Ami ami =
amis.get
(
rowIndex);
switch
(
columnIndex){
case
0
:
ami.setPrenom
((
String)aValue);
break
;
case
1
:
ami.setNom
((
String)aValue);
break
;
case
2
:
ami.setCouleur
((
Color)aValue);
break
;
case
3
:
ami.setHomme
((
Boolean)aValue);
break
;
case
4
:
ami.setSport
((
Sport)aValue);
break
;
}
}
}
@Override
public
Class getColumnClass
(
int
columnIndex){
switch
(
columnIndex){
case
2
:
return
Color.class
;
case
3
:
return
Boolean.class
;
case
4
:
return
Sport.class
;
default
:
return
Object.class
;
}
}
La première méthode retourne true car toutes les méthodes sont éditables. La deuxième récupère l'Ami modifié et en fonction de la colonne modifie la bonne propriété de l'Ami.
Maintenant notre modèle est modifiable, mais cela ne va pas fonctionner tout seul, car JTable ne sait pas comment éditer des couleurs ou des « Sport » par défaut. Il va donc falloir utiliser un nouveau concept, celui des TableCellEditor. Un editor est en fait simplement un objet permettant de gérer l'édition d'une cellule, un peu sur le même principe que le renderer. Par défaut, JTable gère déjà l'édition de tous les champs configurés en tant que Object sous forme d'un JTextField ainsi que les Boolean sous forme de cases à cocher. Donc, dans notre cas, cela marchera directement pour le nom et le prénom de l'ami, mais ne marchera pas pour le reste. On ne peut pas éditer une couleur ou une énumération sous forme de texte. Pour ce qui est du sexe, ça va fonctionner, mais pas complètement à cause de notre renderer qui n'est pas cohérent avec l'editor. Il nous faudra donc créer 3 editors.
On va commencer par le plus simple, pour l'énumération. On va utiliser une simple liste déroulante. Dans ce cas, c'est plus que simple, car il existe la classe DefaultCellEditor qui a un constructeur prenant un JComboBox, on va donc en profiter :
public
class
SportCellEditor extends
DefaultCellEditor {
public
SportCellEditor
(
) {
super
(
new
JComboBox
(
Sport.values
(
)));
}
}
On peut donc voir que créer un éditeur pour un type énuméré est extrêmement simple.
On va maintenant créer un éditeur pour notre couleur. On pourrait utiliser un champ texte avec la valeur hexadécimale de la couleur ou encore trois champs texte avec chacun une composante RGB, mais ce ne serait pas très pratique alors qu'on a un composant de choix de couleur dans Swing, le JColorChooser. Par contre, on ne peut pas l'utiliser comme editor. Il faut en fait utiliser un bouton comme editor qui va ouvrir le JColorChooser. Cela va nous montrer comment faire un editor évolué et nous montrer les différents concepts inhérents à ces editors. Voici donc un editor permettant d'utiliser le JColorChooser de Swing :
public
class
ColorCellEditor extends
AbstractCellEditor implements
TableCellEditor, ActionListener {
private
Color couleur;
private
JButton bouton;
private
JColorChooser colorChooser;
private
JDialog dialog;
public
ColorCellEditor
(
) {
super
(
);
bouton =
new
JButton
(
);
bouton.setActionCommand
(
"change"
);
bouton.addActionListener
(
this
);
bouton.setBorderPainted
(
false
);
colorChooser =
new
JColorChooser
(
);
dialog =
JColorChooser.createDialog
(
bouton, "Pick a Color"
, true
, colorChooser, this
, null
);
}
@Override
public
void
actionPerformed
(
ActionEvent e) {
if
(
"change"
.equals
(
e.getActionCommand
(
))) {
bouton.setBackground
(
couleur);
colorChooser.setColor
(
couleur);
dialog.setVisible
(
true
);
fireEditingStopped
(
);
}
else
{
couleur =
colorChooser.getColor
(
);
}
}
@Override
public
Object getCellEditorValue
(
) {
return
couleur;
}
@Override
public
Component getTableCellEditorComponent
(
JTable table, Object value, boolean
isSelected, int
row, int
column) {
couleur =
(
Color)value;
return
bouton;
}
}
Cette fois, on peut tout de suite voir que ça se complique un peu. Le TableCellEditor en lui-même est un JButton. La méthode getTableCellEditorComponent retourne donc le composant faisant l'édition. La méthode getCellEditorValue retourne la valeur entrée dans l'editor, dans notre, cas, il s'agit de la couleur modifiée (ou laissée telle quelle) du JColorChooser. On appelle la méthode fireEditingStopped() pour avertir le JTable qu'on a terminé l'édition et qu'il faut afficher à nouveau le renderer.
On va s'occuper du dernier de nos editors, celui pour le sexe. Encore une fois, on aurait plusieurs solutions. On pourrait afficher une liste déroulante avec les deux choix, des boutons radio, une case à cocher ou même un JTextField pourquoi pas. Dans notre cas, on va faire très simple, un simple bouton qui change la valeur à chaque clic :
public
class
SexeCellEditor extends
AbstractCellEditor implements
TableCellEditor, ActionListener {
private
boolean
sexe;
private
JButton bouton;
public
SexeCellEditor
(
) {
super
(
);
bouton =
new
JButton
(
);
bouton.addActionListener
(
this
);
bouton.setBorderPainted
(
false
);
}
@Override
public
void
actionPerformed
(
ActionEvent e) {
sexe ^=
true
;
fireEditingStopped
(
);
}
@Override
public
Object getCellEditorValue
(
) {
return
sexe;
}
@Override
public
Component getTableCellEditorComponent
(
JTable table, Object value, boolean
isSelected, int
row, int
column) {
sexe =
(
Boolean)value;
return
bouton;
}
}
On garde le même principe que pour le choix des couleurs sauf que cette fois c'est encore plus simple, il suffit de faire une inversion du booléen et de le retourner.
Enfin, on configure notre JTable pour qu'elle prenne en compte nos renderers :
tableau.setDefaultEditor
(
Sport.class
, new
SportCellEditor
(
));
tableau.setDefaultEditor
(
Color.class
, new
ColorCellEditor
(
));
tableau.setDefaultEditor
(
Boolean.class
, new
SexeCellEditor
(
));
Le principe est le même que pour les renderers, on a un editor par classe de colonne. Voilà ce que donne l'édition d'une couleur :
Voilà , on a maintenant un tableau complètement éditable et fonctionnel. Dans les prochains chapitres, on va fignoler le tout en permettant de trier des colonnes et de filtrer le contenu du tableau.
7. Trier le contenu▲
On va maintenant rendre notre tableau triable par colonne. Cela permet à un utiliser de trier le contenu du tableau en fonction de la colonne en appuyant sur le titre d'une colonne. Cela est fait au moyen d'un objet RowSorter. Le JTable possède une méthode permettant d'activer un sorter par défaut :
tableau.setAutoCreateRowSorter
(
true
);
Cela va trier les colonnes de classe String de façon alphabétique en fonction de la Locale courante (langue et pays), les colonnes d'une classe implémentant l'interface Comparable en fonction de leur comparaison et les autres colonnes de façon alphabétique sur leur valeur toString().
Dans la plupart des cas, c'est suffisant, mais on peut tout de même personnaliser le sorter. On peut évidemment créer notre propre RowSorter, mais il est plus pratique d'utiliser la classe TableRowSorter et de la personnaliser pour effectuer des changements plutôt que de redéfinir une nouvelle implémentation ce qui peut s'avérer assez lourd. Voici une façon de faire permettant de personnaliser le sorter :
TableRowSorter<
TableModel>
sorter =
new
TableRowSorter<
TableModel>(
tableau.getModel
(
));
tableau.setRowSorter
(
sorter);
Une première chose qu'on peut faire est de spécifier une colonne comme non triable via la méthode setSortable :
sorter.setSortable
(
2
, false
);
Ce code spécifie que la colonne « Couleur » n'est pas triable. Ensuite, on peut également indiquer au sorter s'il faut retrier le tableau après une mise à jour des données dans le tableau :
sorter.setSortsOnUpdates
(
true
);
Ce code indique au sorter qu'il faut retrier le tableau après chaque modification des données. Une autre chose intéressante est qu'on peut spécifier notre propre comparateur pour une colonne. Dans notre cas, c'est ce qu'il faut pour la colonne Couleur, car Color n'implémentant pas Comparable et est donc trié via sa valeur de toString() ce qui n'est pas très intéressant. On va donc trier cette colonne en fonction du niveau de bleu dans la couleur. On commence donc par créer un Comparator de Color :
public class ColorComparator implements Comparator<Color> {
@Override
public int compare(Color c1, Color c2) {
return new Integer(c1.getBlue()).compareTo(c2.getBlue());
}
}
Ensuite de quoi, on spécifie que la colonne 2 doit utiliser ce nouveau comparator :
sorter.setComparator
(
2
, new
ColorComparator
(
));
Voilà ce que ça donnerait avec un tri sur la colonne Couleur :
Il est donc très facile de trier un tableau.
Maintenant, lorsqu'une JTable est triable, cela va poser un problème pour la suppression de lignes. Vous pouvez essayer avec le code courant si vous triez puis tentez de supprimer des lignes, vous allez voir que les lignes supprimées ne sont pas les bonnes. À quoi est-ce dû ?
Tout simplement parce que les index retournés par les méthodes pour récupérer la sélection (getSelectedRows() par exemple) retournent l'index visuel. Dans le cas d'une JTable non triable, cet index correspond également à l'index du modèle, mais ce n'est plus le cas avec un tableau trié. On peut néanmoins très facilement résoudre ce problème en utilisant la méthode convertRowIndexToModel de la classe RowSorter. On va donc recoder la méthode RemoveAction avec cette nouvelle méthode :
private
class
RemoveAction extends
AbstractAction {
private
RemoveAction
(
) {
super
(
"Supprimmer"
);
}
public
void
actionPerformed
(
ActionEvent e) {
int
[] selection =
tableau.getSelectedRows
(
);
int
[] modelIndexes =
new
int
[selection.length];
for
(
int
i =
0
; i <
selection.length; i++
){
modelIndexes[i] =
tableau.getRowSorter
(
).convertRowIndexToModel
(
selection[i]);
}
Arrays.sort
(
modelIndexes);
for
(
int
i =
modelIndexes.length -
1
; i >=
0
; i--
){
modele.removeAmi
(
modelIndexes[i]);
}
}
}
On commence donc par récupérer les index au niveau de la vue, puis on les convertit au niveau modèle. Ensuite de quoi, il faut les trier pour supprimer les éléments depuis la fin. Vous verrez que cette fois, la suppression d'éléments fonctionne parfaitement.
Au prochain (et dernier) chapitre, on va étendre ce comportement en permettant de filtrer le contenu du tableau.
8. Filtrer le contenu▲
En plus d'effectuer un tri sur les cellules, la classe RowSorter permet également de filtrer le contenu du tableau. On peut utiliser pour cela la méthode setRowFilter() qui prend en paramètre un objet RowFilter. RowFilter possède plusieurs méthodes statiques permettant de créer des filtres. Notamment des méthodes permettant d'effectuer des opérations « and » ou « or » sur des filtres. En plus de cela, on a également une méthode statique permettant d'effectuer un filtrage par regex sur une des colonnes.
On va ajouter un bouton permettant d'effectuer un filtre sur les colonnes « nom » et « prénom » :
private
class
FilterAction extends
AbstractAction {
private
FilterAction
(
) {
super
(
"Filtrer"
);
}
public
void
actionPerformed
(
ActionEvent e) {
String regex =
JOptionPane.showInputDialog
(
"Regex de filtre : "
);
sorter.setRowFilter
(
RowFilter.regexFilter
(
regex, 0
, 1
));
}
}
Il est donc extrêmement facile d'effectuer un filtre simple sur une ou plusieurs colonnes. Voici le résultat pour un filtre avec « mp » :
Comme vous pouvez le constater, il est facile de filtrer le contenu avec les méthodes statiques de RowFilter. Pour plus de souplesse, on peut également étendre la classe RowFilter qui possède une seule méthode abstraite, include(Entry entry), qui permet d'indiquer si une ligne doit être incluse dans le tableau.
9. Conclusion▲
Voilà , nous avons maintenant traité tous les différents aspects de la création et la manipulation de tableaux (JTable) avec Swing. J'espère que ce tutoriel vous permettra de maîtriser ce composant qui n'est, une fois les différents concepts compris, pas si compliqué à utiliser.
N'hésitez pas à commenter cet article sur le sujet lié sur le forum : 40 commentaires .
Voici une archive ZIP contenant l'intégralité des sources de cet article : Fichiers sources de cet articleFichiers sources de cet article.
9-1. Remerciements▲
Un grand merci à jacques_jeanjacques_jean et Wachter pour leurs corrections orthographiques.