Introduction▲
Les JTables sont des composants Java écrits par Sun Microsystem pour gérer les tableaux. Sun fournit des Objets par défaut permettant de les utiliser très simplement comme tableur. Mais il est également possible de mettre ce que l'on veut dans chacune des cases (même un autre JTable!) et d'aller plus loin qu'un tableur classique.
Vous trouverez plus bas du code tiré directement d'Edupassion.com, site web proposant entre autres des bulletins de notes. Parfois le code est coloré en rouge, permettant de retrouver un même objet entre différents objets.
1. Ce que l'utilisateur voit▲
1-A. L'utilisateur voit des cellules : les TableCellRenderer▲
Si ce que l'on voit est un texte ou un nombre, on aura sans doute affaire à un new DefaultTableCellRenderer() qui hérite de JLabel. Chaque case peut être différente : c'est la fonction TableCellRenderer.getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) qui definira le JComponent selon la ligne et la colonne. Si getTableCellRendererComponent renvoie un JButton, l'utilisateur pourra cliquer dessus. Si le JComponent est un JLabel, on peut lire une donnée.
Voici le code pour obtenir le JComponent de type JButton dans lequel apparaît le nom de l'élève.
public
class
RendererEleve extends
javax.swing.JButton implements
javax.swing.table.TableCellRenderer {
public
RendererEleve
(
) {
/*Constructeur vide*/
}
/** l'object value représente en principe l'élève */
public
java.awt.Component getTableCellRendererComponent
(
javax.swing.JTable table, Object value, boolean
isSelected, boolean
hasFocus, int
row, int
col)
{
String nom=((
Eleve) value).toString
(
);// Prenom + Nom de l'élève
this
.setText
(
nom);
return
this
;
}
}
// Fin de la classe
1-B. L'utilisateur lit des données dans ces cellules▲
Ces données sont stockées dans le TableModel. La fonction TableModel.getValueAt(int row, int column) renvoie l'objet métier selon la colonne et la ligne.
public
class
TableModel {
public
Object getValueAt
(
int
row, int
column) {
if
(
column==
this
.INDEX_COLONNE_LISTE_DES_ELEVES){
// Colonne des élèves
Eleve eleve =
(
Eleve)listeDesEleves.get
(
row);
return
eleve;
}
if
(
column==
this
.INDEX_COLONNE_MOYENNE){
//Colonne des moyennes
Eleve eleve =
(
Eleve)listeDesEleves.get
(
row);
return
trouveMoyenne
(
eleve);
}
}
}
L'objet renvoyé sera traité par la fonction TableCellRenderer.getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) vue auparavant (I.A).
1-C. L'utilisateur comprend que les cellules sont rangées par colonne▲
La plupart du temps, les éléments d'une colonne ont la même signification : une date, un nom, un prénom, et ici les notes. Les fonctionnalités d'une colonne sont gérées par une TableColumn.
public
class
TableColumnEleve extends
TableColumn{
/**
* Crée un style pour les TableColumn des Eleves, et spécifie le modelIndex à 0.
*/
public
TableColumnEleve
(
) {
super
(
0
); // le modelIndex est à 0, ce qui indique que les élèves seront représentés à la première colonne
setHeaderRenderer
(
new
HeaderEleve
(
));//Le component sera un HeaderEleve, qui hérite d'un JLabel
setCellRenderer
(
new
RendererEleve
(
));//Le Component est un RendererEleve qui hérite d'un JButton
}
}
// Fin de TableColumnEleve
Ces TableColumn sont ensuite réparties lors de la création du JTable
public
class
Bulletin extends
JTable {
BulletinModel model;
public
Bulletin
(
Cours cours) {
/* Création du modèle */
this
.setAutoCreateColumnsFromModel
(
false
);
BulletinModel model=
new
BulletinModel
(
cours, this
); //Le modèle dépendra des notes dans le Cours
this
.setModel
(
model);
/* Création des TableColumn */
createColumns
(
);
}
private
void
createColumns
(
) {
/* Premiere TableColumn : celle contenant la liste des Eleves */
TableColumnEleve tableColumnEleve =
new
TableColumnEleve
(
);
this
.addColumn
(
tableColumnEleve);
/* Ajout des contrôles : On a une nouvelle colonne par contrôle donné aux élèves */
for
(
int
modelIndex=
model.INDEX_PREMIER_CONTROLE;modelIndex <=
model.INDEX_DERNIER_CONTROLE;modelIndex++
){
Controle contrôle=
(
Controle) listeDesControles.get
(
modelIndex-
model.INDEX_PREMIER_CONTROLE);
TableColumnControle columnControle=
new
TableColumnControle
(
modelIndex, controle);//Contrairement à TableColumnEleve , l'index est dans le constructeur
this
.addColumn
(
columnControle);
}
//On continue avec les autres colonnes
}
}
//Fin de la classe Bulletin
1-D. La colonne comprend un entête▲
L'entête est de la classe HeaderRenderer : il s'agit encore d'une TableCellRenderer qui par défaut est grisâtre (selon le JRE utilisé). Si votre table est grande, vous la ferez scroller vers le bas, mais l'entête reste. C'est pourquoi l'entête n'est pas géré graphiquement par le container du JTable, mais par le scrollPane parent.
this
.scrollPane.getViewport
(
).add
(
this
.bulletin);//Permet au scollpane de gérer le header - bulletin derive de JTable
Chaque colonne contient un Header, qui est donc choisi dans le TableColumn
public
class
TableColumnEleve extends
TableColumn{
/**
* Crée un style pour les TableColumn des Eleves, et spécifie le modelIndex à 0.
*/
public
TableColumnEleve
(
) {
super
(
0
); // le modelIndex est à 0, ce qui indique la première colonne
setHeaderRenderer
(
new
HeaderEleve
(
));//Le component sera un HeaderEleve, qui hérite d'un JLabel disabled
setCellRenderer
(
new
RendererEleve
(
));//Le Component est un RendererEleve qui hérite d'un JButton
}
}
// Fin de TableColumnEleve
2. Ce que l'utilisateur fait▲
2-A. L'utilisateur va cliquer sur une case pour en modifier le contenu▲
Si la case est éditable, alors on rentre dans le mode d'édition de la cellule, géré par un objet de classe TableCellEditor. Il apparaît à l'écran un nouvel objet : le CellEditorComponent. Il s'agit d'un JComponent classique tels un JTextField ou un JCombo.
Style de note européenne : NoteEUEditor |
Style de note Primaire (NotePrimaireEditor) |
On obtient le CellEditorComponent d'édition dans TableCellEditor : public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column)
public
class
NoteEUEditor extends
AbstractCellEditor implements
TableCellEditor{
public
NoteEU noteEU;
JTextField fieldNoteEU=
null
; // C'est le JComponent que l'utilisateur va voir quand il entrera dans le mode Editor.
JTable table;
/** Constructeur pour NoteEUEditor
* Cet éditeur permet de modifier une note sur /20 avec un seul clic.
*/
public
NoteEUEditor
(
) {
this
.fieldNoteEU=
new
JTextField (
);
}
/**
* Cette fonction permet l'affichage sous forme de String de la note de l'élève
* L'objet value est référencé dans le TableModel.getValueAt().
*/
public
Component getTableCellEditorComponent
(
JTable table, Object value, boolean
isSelected, int
row, int
column)
{
NoteEU noteEU=(
NoteEU) value; //L'objet Value est ce qui apparait AVANT que l'on modifie la valeur - ici c'est une note sur 20
String stringNote=
((
Float)noteEU.getValue
(
)).toString
(
);//StringNote est la chaine de caractère que l'on va afficher dans le JTextField
/* Traitement conditionnel selon les spécificités métier */
if
(
noteEU.getValue
(
)<-
2000
) fieldNoteEU.setText
(
""
);//Cas pour un élève absent, représenté par la note -10002
else
labelNoteEU.setText
(
stringNote);
/* Une fois que tous les calculs sont faits, on sélectionnera les chiffres de la note */
SwingUtilities.invokeLater
(
new
Runnable
(
) {
public
void
run
(
) {
fieldNoteEU.requestFocus
(
);
fieldNoteEU.selectAll
(
);// utilisé pour que tout soit sélectionné, mais ça ne marche pas...
}
}
);
return
fieldNoteEU; //Le JTextField contient maintenant ce que l'on veut.
}
}
2-B. L'utilisateur fait une modification▲
Si on valide le changement, on renvoie un objet grâce à la fonction public Object getCellEditorValue() dans le même TableCellEditor.
public
class
NoteEUEditor extends
AbstractCellEditor implements
TableCellEditor{
(
... Voir plus haut ...)
/**
* Renvoie un objet une fois APRÈS MODIFICATION par l'utilisateur
* L'objet renvoyé est très basique et sera ensuite traité par TableModel.setValueAt().
*/
public
Object getCellEditorValue
(
)
{
String str=
labelNoteEU.getText
(
);
if
(
str.equalsIgnoreCase
(
"ABS"
)||
str.equalsIgnoreCase
(
"a"
)||
str.equalsIgnoreCase
(
"absent"
)) return
Note.FLOAT_ABSENT; //On renvoit -10002
if
(
str.equalsIgnoreCase
(
""
)) return
Note.FLOAT_NON_NOTE;
try
{
Float f=
new
Float
(
str);
if
(
f.floatValue
(
)<=
Note.FLOAT_NA.floatValue
(
)) return
Note.FLOAT_NA;
else
return
f;
}
catch
(
Exception e) {
e.printStackTrace
(
);return
Note.FLOAT_NA;}
//Si l'utilisateur a rentré n'importe quoi (ce qui arrive souvent ;) )
}
}
Arrive alors un processus invisible pour l'utilisateur grâce aux différents Events préprogrammés par nos amis de Sun Microsystem. L'objet renvoyé par AbstractCellEditor : public Object getCellEditorValue() arrive à TableModel.setValueAt(Object obj, int row, int col), ce que nous détaillerons plus bas.
Il est vivement conseillé de passer par AbstractCellEditor afin de ne pas reprogrammer des méthodes comme fireEditingStopped(). En effet l'AbstractCellEditor fera souvent la démarche logique souhaitée.
2-C. Le Model est modifié▲
Les observateurs auront noté que la fonction getCellEditorValue() ne fait pas référence à la position de la cellule dans le tableau. Cela a pour avantage de pouvoir réutiliser le TableCellEditor dans d'autres JTable (voire JTree ou Jlist avec une utilisation mineure de l'héritage). L'influence de la position dans le tableau dépend logiquement du traitement métier, et est traitée dans le TabelModel.setValueAt().
public
void
setValueAt
(
Object obj, int
row, int
col) {
if
(
col==
this
.INDEX_COLONNE_APPRECIATION) this
.setValueForAppreciation
(
obj, row);
if
(
col==
this
.INDEX_COLONNE_MOYENNE) this
.setValueForMoyenne
(
obj,row,col);
else
this
.setValueForControle
(
obj, row, col);
}
L'Editor renvoie un Objet, et en fonction de celui-ci, le TableModel modifie ses données. Les systèmes d'événement programmés par Sun mettront à jour la partie visuelle en faisant appel à TableModel.getValueAt() avec les nouvelles valeurs.
3. Ce que JTable fait… et ne fait pas▲
3-A. Une couche de verre▲
Lorsque l'on clique sur un bouton de la JTable, on ne clique pas sur le bouton, mais sur la JTable, qui transmet ensuite l'événement au bouton… si on l'a programmé. On dit parfois qu'il y a une glace de verre par-dessus la Table : il est possible de voir ce qu'il y a en dessous, mais en appuyant à un endroit, on appuie sur toute la table.
3-B. Un exemple classique : Cliquer sur un bouton du Header, ce qui nous permettra de rajouter un Contrôle à notre Bulletin▲
On va créer un MouseListener qui va écouter ce que notre souris clique.
public
class
EvtAjoutEtModifDeControle implements
MouseListener{
Bulletin bulletin;
JTableHeader header;
/**
* Creates a new instance of EvtAjoutEtModifDeControle
*/
public
EvtAjoutEtModifDeControle
(
Bulletin bulletin) {
this
.bulletin=
bulletin;
}
public
void
mouseClicked
(
MouseEvent e) {
/* Ligne clé ! c'est ici qu'on sait où on clique */
int
indexDeColonneSelected =
bulletin.convertColumnIndexToModel
(
bulletin.columnAtPoint
(
e.getPoint
(
)));
/* code une fois que l'on a su où l'on cliquait */
if
(
indexDeColonneSelected==
bulletin.getBulletinModel
(
).INDEX_COLONNE_AJOUT_DEVOIR)
MaFactory.creerControle
(
bulletin);
}
/* Les autres méthodes non prises en charge */
public
void
mousePressed
(
MouseEvent e) {
}
public
void
mouseReleased
(
MouseEvent e) {
}
public
void
mouseEntered
(
MouseEvent e) {
}
public
void
mouseExited
(
MouseEvent e) {
}
}
Et dans le constructeur de la JTable, on rajoute la ligne :
this
.getTableHeader
(
).addMouseListener
(
new
EvtAjoutEtModifDeControle
(
this
));
Pour continuer avec notre allégorie, il faut ordonner à chaque JComponent de « regarder » ce qui se passe au-dessus de la glace posée sur la Table.
3-C. Une conséquence : Utiliser autant que possible les objets et méthodes par défaut, afin de ne pas devoir reprogrammer les événements▲
J'ai donné tantôt un exemple de JComboBox fourni lorsque l'on clique sur une cellule. On voudrait qu'il se passe ceci : User choisit dans le combo un objet_choisit -> public void monJComboActionPerformed() -> public Object monEditor.getCellEditorValue() -> monModel.setValueAt (objet_choisi, row, col)
Malheureusement, une fois que l'on choisit l'élément de notre JComboBox, la fonction programmée JComboBoxActionPerformed() ne s'exécute pas : l'événement n'est pas transmis, et il faudrait reprogrammer les enchaînements des événements. C'est faisable, mais pénible, c'est pourquoi Sun Microsystem nous donne un DefaultCellEditor (JComboBox combo) - il est possible d'avoir un DefaultCellEditor basé sur d'autres JComponents.
Le DefaultCellEditor s'occupe des événements, et il vous reste à faire le code métier. Dans notre cas, la dernière ligne des bulletins est occupée par les moyennes du Controle et il n'y a donc pas de choix possible : voyons la nouvelle implémentation pour l'éditeur de notes type Ecole Primaire.
public
class
NotePrimaireEditor extends
DefaultCellEditor{
/** Creates a new instance of NoteUSEditor */
public
NotePrimaireEditor
(
JComboBox combo) {
super
(
combo);
}
public
Component getTableCellEditorComponent
(
JTable table, Object value, boolean
isSelected, int
row, int
column) {
/* Si c'est la dernière ligne, on affiche la moyenne */
if
(
row ==
((
BulletinModel)table.getModel
(
)).INDEX_DERNIERE_LIGNE){
if
(
value!=
null
)
return
new
JLabel
(
value.toString
(
));
else
return
new
JLabel
(
);
}
else
//On peut afficher le JComboBox
return
super
.getTableCellEditorComponent
(
table, value, isSelected, row, column);
}
/** La fonction est à implémenter si vous voulez renvoyer un autre objet que celui présent dans le JComboBox (par exemble à partir d'une Factory).
* Sinon, c'est le DefaultCellEditor qui renvoie l'objet contenu dans le JCombo. C'est le traitement par défaut, car c'est le plus logique.
*public Object getCellEditorValue() {}
*/
}
4. Conclusion▲
4-A. Récapitulatif▲
Les objets de La VUE UTILISATEUR définissent ce que l'utilisateur voit avant d'interagir avec le JTable. Le TableCellRenderer dessine sur l'écran selon les données du Model,les directives données par le TableColumn, puis l'index de ligne ou de colonne. |
4-B. Et quand j'aurai tout compris à ce tutoriel ?▲
Voici de quoi faire pas mal de choses avec les JTables. Cependant, les JTables étant des composants Swing, les bonnes pratiques de Swing s'appliquent aussi, notamment une bonne gestion des Threads (cf. SwingUtilities en section II-A).
- Si votre programme a des accès longs vers la base de données, vous devrez utiliser d'autres fonctions des JTables, comme EditCellAt().
- Si votre programme est complexe (volontairement ou non), vous devrez peut-être redéfinir les Events, ou réécrire les fonctions fireXXX() des objets fournis par Sun.
Mon avis est que l'utilisation des JTables est déjà assez compliquée, et si vous souhaitez que votre code soit maintenable par une autre personne, évitez au maximum de réécrire ce que Sun Microsystem a déjà fait. Concrètement, cela consiste à écrire des classes qui héritent des DefaultXXX ou AbstractXXX plutôt que d'implémenter à nouveau les Interfaces.