Licence CC BY-NC-SA

Les interfaces de tableaux

Ce contenu est obsolète. Il peut contenir des informations intéressantes mais soyez prudent avec celles-ci.

Nous continuons notre progression avec un autre composant assez complexe : le tableau. Celui-ci fonctionne un peu comme le JTree vu précédemment.

Les choses se compliquent dès que l'on doit manipuler les données à l'intérieur du tableau, car Java impose de séparer strictement l'affichage et les données dans le code.

Premiers pas

Les tableaux sont des composants qui permettent d'afficher des données de façon structurée. Pour ceux qui ne savent pas ce que c'est, en voici un à la figure suivante.

Exemple de tableau

Le code source de ce programme est le suivant :

 1
 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
//CTRL + SHIFT + O pour générer les imports
public class Fenetre extends JFrame {

  public Fenetre(){
    this.setLocationRelativeTo(null);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setTitle("JTable");
    this.setSize(300, 120);

    //Les données du tableau
    Object[][] data = {
      {"Cysboy", "28 ans", "1.80 m"},
      {"BZHHydde", "28 ans", "1.80 m"},
      {"IamBow", "24 ans", "1.90 m"},
      {"FunMan", "32 ans", "1.85 m"}
    };

    //Les titres des colonnes
    String  title[] = {"Pseudo", "Age", "Taille"};
    JTable tableau = new JTable(data, title);
    //Nous ajoutons notre tableau à notre contentPane dans un scroll
    //Sinon les titres des colonnes ne s'afficheront pas !
    this.getContentPane().add(new JScrollPane(tableau));
  }   

  public static void main(String[] args){
    Fenetre fen = new Fenetre();
    fen.setVisible(true);
  }   
}

Vous instanciez un objet JTable en lui passant en paramètres les données qu'il doit utiliser.

Les titres des colonnes de votre tableau peuvent être de type String ou de type Object, tandis que les données sont obligatoirement de type Object.

Vous verrez un peu plus loin qu'il est possible de mettre plusieurs types d'éléments dans un tableau. Mais nous n'en sommes pas là : il nous faut d'abord comprendre comment fonctionne cet objet.

Les plus observateurs auront remarqué que j'ai mis le tableau dans un scroll… En fait, si vous avez essayé d'ajouter le tableau dans le contentPane sans scroll, vous avez dû constater que les titres des colonnes n'apparaissent pas. En effet, le scroll indique automatiquement au tableau l'endroit où il doit afficher ses titres ! Sans lui, vous seriez obligés de préciser où afficher l'en-tête du tableau, comme ceci :

1
2
3
4
//On indique que l'en-tête doit être au nord, donc au-dessus
this.getContentPane().add(tableau.getTableHeader(), BorderLayout.NORTH);
//Et le corps au centre !
this.getContentPane().add(tableau, BorderLayout.CENTER);

Je pense que nous avons fait le tour des préliminaires… Entrons dans le vif du sujet !

Gestion de l'affichage

Les cellules

Vos tableaux sont composés de cellules. Vous pouvez les voir facilement, elles sont encadrées de bordures noires et contiennent les données que vous avez mises dans le tableau d'Object et de String. Celles-ci peuvent être retrouvées par leurs coordonnées (x, y)x correspond au numéro de la ligne et y au numéro de la colonne ! Une cellule est donc l'intersection d'une ligne et d'une colonne.

Afin de modifier une cellule, il faut récupérer la ligne et la colonne auxquelles elle appartient. Ne vous inquiétez pas, nous allons prendre tout cela point par point. Tout d'abord, commençons par changer la taille d'une colonne et d'une ligne. Le résultat final ressemble à ce qu'on voit sur la figure suivante.

Changement de taille

Vous allez voir que le code utilisé est simple comme tout, encore fallait-il que vous sachiez quelles méthodes et quels objets utiliser… Voici le code permettant d'obtenir ce résultat :

 1
 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
//CTRL + SHIFT + O pour générer les imports
public class Fenetre extends JFrame {

  private JTable tableau;
  private JButton change = new JButton("Changer la taille");
  private JButton retablir = new JButton("Rétablir");

  public Fenetre(){
    this.setLocationRelativeTo(null);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setTitle("JTable");
    this.setSize(300, 240);

    Object[][] data = {
      {"Cysboy", "28 ans", "1.80 m"},
      {"BZHHydde", "28 ans", "1.80 m"},
      {"IamBow", "24 ans", "1.90 m"},
      {"FunMan", "32 ans", "1.85 m"}
    };

    String  title[] = {"Pseudo", "Age", "Taille"};
    this.tableau = new JTable(data, title);

    JPanel pan = new JPanel();

    change.addActionListener(new ActionListener(){
      public void actionPerformed(ActionEvent arg0) {            
        changeSize(200, 80);
        change.setEnabled(false);
        retablir.setEnabled(true);
      }         
    });

    retablir.addActionListener(new ActionListener(){
      public void actionPerformed(ActionEvent arg0) {

        changeSize(75, 16);
        change.setEnabled(true);
        retablir.setEnabled(false);
      }         
    });

    retablir.setEnabled(false);
    pan.add(change);
    pan.add(retablir);

    this.getContentPane().add(new JScrollPane(tableau), BorderLayout.CENTER);
    this.getContentPane().add(pan, BorderLayout.SOUTH);
  }

  /**
   * Change la taille d'une ligne et d'une colonne
   * J'ai mis deux boucles afin que vous puissiez voir 
   * comment parcourir les colonnes et les lignes
   */
  public void changeSize(int width, int height){
    //Nous créons un objet TableColumn afin de travailler sur notre colonne
    TableColumn col;
    for(int i = 0; i < tableau.getColumnCount(); i++){
      if(i == 1){
        //On récupère le modèle de la colonne
        col = tableau.getColumnModel().getColumn(i);
        //On lui affecte la nouvelle valeur
        col.setPreferredWidth(width);
      }
    }            
    for(int i = 0; i < tableau.getRowCount(); i++){
      //On affecte la taille de la ligne à l'indice spécifié !
      if(i == 1)
        tableau.setRowHeight(i, height);
      }
    }

  public static void main(String[] args){
    Fenetre fen = new Fenetre();
    fen.setVisible(true);
  }   
}

Tout comme pour les tableaux vus dans la première partie de cet ouvrage, les indices des lignes et des colonnes d'un objet JTable commencent à 0 !

Vous constatez que la ligne et la colonne concernées changent bien de taille lors du clic sur les boutons. Vous venez donc de voir comment changer la taille des cellules de façon dynamique. Je dis ça parce que, au cas où vous ne l'auriez pas remarqué, vous pouvez changer la taille des colonnes manuellement. Il vous suffit de cliquer sur un séparateur de colonne, de maintenir le clic et de déplacer le séparateur, comme indiqué à la figure suivante.

Séparateurs

Par contre, cette instruction a dû vous sembler étrange : tableau.getColumnModel().getColumn(i);. En fait, vous devez savoir que c'est un objet qui fait le lien entre votre tableau et vos données. Celui-ci est ce qu'on appelle un modèle de tableau (ça vous rappelle les modèles d'arbres, non ?). L'objet en question s'appelle JTableModel et vous allez voir qu'il permet de faire des choses très intéressantes ! C'est lui qui stocke vos données… Toutes vos données !

Tous les types héritant de la classe Object sont acceptés.

Essayez ce morceau de code :

 1
 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
//CTRL + SHIFT + O pour générer les imports
public class Fenetre extends JFrame {

  private JTable tableau;
  private JButton change = new JButton("Changer la taille");

  public Fenetre(){
    this.setLocationRelativeTo(null);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setTitle("JTable");
    this.setSize(600, 140);

    Object[][] data = {   
      {"Cysboy", new JButton("6boy"), new Double(1.80), new Boolean(true)},
      {"BZHHydde", new JButton("BZH"), new Double(1.78), new Boolean(false)},
      {"IamBow", new JButton("BoW"), new Double(1.90), new Boolean(false)},
      {"FunMan", new JButton("Year"), new Double(1.85), new Boolean(true)}
    };

    String  title[] = {"Pseudo", "Age", "Taille", "OK ?"};
    this.tableau = new JTable(data, title);
    this.getContentPane().add(new JScrollPane(tableau), BorderLayout.CENTER);      
  }

  public static void main(String[] args){
    Fenetre fen = new Fenetre();
    fen.setVisible(true);
  }
}

Vous devriez obtenir un résultat similaire à celui présenté à la figure suivante.

Différents objets dans un tableau

Pour être le plus flexible possible, on doit créer son propre modèle qui va stocker les données du tableau. Il vous suffit de créer une classe héritant de AbstractTableModel qui — vous l'avez sûrement deviné — est une classe abstraite…

Voici donc un code pour étayer mes dires :

 1
 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
//CTRL + SHIFT + O pour générer les imports
public class Fenetre extends JFrame {

  private JTable tableau;
  private JButton change = new JButton("Changer la taille");

  public Fenetre(){
    this.setLocationRelativeTo(null);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setTitle("JTable");
    this.setSize(600, 140);

    Object[][] data = {   
      {"Cysboy", new JButton("6boy"), new Double(1.80), new Boolean(true)},
      {"BZHHydde", new JButton("BZH"), new Double(1.78), new Boolean(false)},
      {"IamBow", new JButton("BoW"), new Double(1.90), new Boolean(false)},
      {"FunMan", new JButton("Year"), new Double(1.85), new Boolean(true)}
    };

    String  title[] = {"Pseudo", "Age", "Taille", "OK ?"};

    ZModel model = new ZModel(data, title);
    System.out.println("Nombre de colonne : " + model.getColumnCount());
    System.out.println("Nombre de ligne : " + model.getRowCount());
    this.tableau = new JTable(model);
    this.getContentPane().add(new JScrollPane(tableau), BorderLayout.CENTER);      
  }

  //Classe modèle personnalisée
  class ZModel extends AbstractTableModel{
    private Object[][] data;
    private String[] title;

    //Constructeur
    public ZModel(Object[][] data, String[] title){
      this.data = data;
      this.title = title;
    }

    //Retourne le nombre de colonnes
    public int getColumnCount() {
      return this.title.length;
    }

    //Retourne le nombre de lignes
    public int getRowCount() {
      return this.data.length;
    }

    //Retourne la valeur à l'emplacement spécifié
    public Object getValueAt(int row, int col) {
      return this.data[row][col];
    }            
  }

  public static void main(String[] args){
    Fenetre fen = new Fenetre();
    fen.setVisible(true);
  }
}

Le résultat se trouve à la figure suivante.

Utilisation d'un modèle de tableau

Bon… Vous ne voyez plus les titres des colonnes. Ceci est dû au fait que vous n'avez redéfini que les méthodes abstraites de la classe AbstractTableModel. Si nous voulons voir nos titres de colonnes apparaître, il faut redéfinir la méthode getColumnName(int col). Elle devrait ressembler à ceci :

1
2
3
4
5
6
/**
* Retourne le titre de la colonne à l'indice spécifié
*/
public String getColumnName(int col) {
  return this.title[col];
}

Exécutez à nouveau votre code, après avoir rajouté cette méthode dans votre objet ZModel : vous devriez avoir le même rendu que la figure suivante.

Affichage des titres de colonnes

Regardez la figure suivante pour comprendre l'intérêt de gérer sa propre classe de modèle.

Affichage de checkbox

Vous avez vu ? Les booléens se sont transformés en cases à cocher ! Les booléens valant « vrai » sont devenus des cases cochées et les booléens valant « faux » sont maintenant des cases non cochées ! Pour obtenir ça, j'ai redéfini une méthode dans mon modèle et le reste est automatique. Cette méthode permet de retourner la classe du type de valeur d'un modèle et de transformer vos booléens en cases à cocher… Au moment où notre objet crée le rendu des cellules, il invoque cette méthode et s'en sert pour créer certaines choses, comme ce que vous venez de voir.

Pour obtenir ce rendu, il vous suffit de redéfinir la méthode getColumnClass(int col). Cette méthode retourne un objet Class. Je vous laisse réfléchir un peu… Pour savoir comment faire, c'est juste en dessous :

1
2
3
4
5
6
7
//Retourne la classe de la donnée de la colonne
public Class getColumnClass(int col){
  //On retourne le type de la cellule à la colonne demandée
  //On se moque de la ligne puisque les types de données sont les mêmes quelle que soit la ligne
  //On choisit donc la première ligne
  return this.data[0][col].getClass();
}

Je ne sais pas si vous avez remarqué, mais vos cellules ne sont plus éditables ! Je vous avais prévenus que ce composant était pénible… En fait, vous devez aussi informer votre modèle qu'il faut avertir l'objet JTable que certaines cellules peuvent être éditées et d'autres pas (le bouton, par exemple). Pour ce faire, il faut redéfinir la méthode isCellEditable(int row, int col) qui, dans la classe mère, retourne systématiquement false

Ajoutez donc ce morceau de code dans votre modèle pour renvoyer true :

1
2
3
4
//Retourne vrai si la cellule est éditable : celle-ci sera donc éditable
public boolean isCellEditable(int row, int col){
  return true; 
}

Vos cellules sont à nouveau éditables. Cependant, vous n'avez pas précisé au modèle que la cellule contenant le bouton n'est pas censée être éditable… Comment régler ce problème ? Grâce à la méthode getClass() de tout objet Java ! Vous pouvez déterminer de quelle classe est issu votre objet grâce au mot-clé instanceof. Regardez comment on procède :

1
2
3
4
5
6
7
8
//Retourne vrai si la cellule est éditable : celle-ci sera donc éditable
public boolean isCellEditable(int row, int col){
  //On appelle la méthode getValueAt qui retourne la valeur d'une cellule
  //Et on effectue un traitement spécifique si c'est un JButton
  if(getValueAt(0, col) instanceof JButton)
    return false;
  return true; 
}

Victoire ! Les cellules sont à nouveau éditables, sauf le JButton. D'ailleurs, je suppose que certains d'entre vous attendent toujours de le voir apparaître… Pour cela, nous n'allons pas utiliser un modèle de tableau, mais un objet qui s'occupe de gérer le contenu et la façon dont celui-ci est affiché.

Les modèles font un pont entre ce qu'affiche JTable et les actions de l'utilisateur. Pour modifier l'affichage des cellules, nous devrons utiliser DefaultCellRenderer.

Contrôlez l'affichage

Vous devez bien distinguer un TableModel d'un DefaultTableCellRenderer. Le premier fait le lien entre les données et le tableau tandis que le second s'occupe de gérer l'affichage dans les cellules !

Le but du jeu est de définir une nouvelle façon de dessiner les composants dans les cellules. En définitive, nous n'allons pas vraiment faire cela, mais dire à notre tableau que la valeur contenue dans une cellule donnée est un composant (bouton ou autre). Il suffit de créer une classe héritant de DefaultTableCellRenderer et de redéfinir la méthode public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column).

Il y en a, des paramètres ! Mais, dans le cas qui nous intéresse, nous n'avons besoin que d'un seul d'entre eux : value. Remarquez que cette méthode retourne un objet Component. Nous allons seulement spécifier le type d'objet dont il s'agit suivant le cas.

Regardez notre classe héritée :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//CTRL + SHIFT + O pour générer les imports
public class TableComponent extends DefaultTableCellRenderer {

  public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
    //Si la valeur de la cellule est un JButton, on transtype cette valeur
    if (value instanceof JButton)
      return (JButton) value;
    else
      return this;
  }
}

Une fois cette classe créée, il suffit de signaler à notre tableau qu'il doit utiliser ce rendu de cellules grâce à l'instruction this.tableau.setDefaultRenderer(JButton.class, new TableComponent());. Le premier paramètre permet de dire à notre tableau de faire attention à ce type d'objet et enfin, le second lui dit d'utiliser ce modèle de cellules.

Cela nous donne la figure suivante.

Affichage des boutons

Voilà notre bouton en chair et en os ! Je me doute bien que les plus taquins d'entre vous ont dû essayer de mettre plus d'un type de composant dans le tableau… Et ils se retrouvent le bec dans l'eau car il ne prend en compte que les boutons pour le moment.

En fait, une fois que vous avez défini une classe héritée afin de gérer le rendu de vos cellules, vous avez fait le plus gros du travail… Tenez, si, par exemple, nous voulons mettre ce genre de données dans notre tableau :

1
2
3
4
5
6
Object[][] data = {
  {"Cysboy", new JButton("6boy"), new JComboBox(new String[]{"toto", "titi", "tata"}), new Boolean(true)},
  {"BZHHydde", new JButton("BZH"), new JComboBox(new String[]{"toto", "titi", "tata"}), new Boolean(false)},
  {"IamBow", new JButton("BoW"), new JComboBox(new String[]{"toto", "titi", "tata"}), new Boolean(false)},
  {"FunMan", new JButton("Year"), new JComboBox(new String[]{"toto", "titi", "tata"}), new Boolean(true)}
};

… et si nous conservons l'objet de rendu de cellules tel qu'il est actuellement, nous obtiendrons la figure suivante.

Problème d'affichage d'une combo

Les boutons s'affichent toujours, mais pas les combos ! Je sais que certains d'entre vous ont presque trouvé la solution. Vous n'auriez pas ajouté ce qui suit dans votre objet de rendu de cellule ?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//CTRL + SHIFT + O pour générer les imports
public class TableComponent extends DefaultTableCellRenderer {

  public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
    if (value instanceof JButton)
      return (JButton) value;
    //Lignes ajoutées
    else if(value instanceof JComboBox)
      return (JComboBox) value;
    else
      return this;
  }
}

Ceux qui ont fait cela ont trouvé la première partie de la solution ! Vous avez bien spécifié à votre objet de retourner une valeur castée en cas de rencontre avec une combo. Seulement, et j'en suis quasiment sûr, vous avez dû oublier de dire à votre tableau de faire attention aux boutons et aux combos ! Rappelez-vous cette instruction :

1
this.tableau.setDefaultRenderer(JButton.class, new TableComponent());

Votre tableau ne fait attention qu'aux boutons ! Pour corriger le tir, il faut simplement changer JButton.class en JComponent.class. Après avoir fait ces deux modifications, vous devriez parvenir à la figure suivante.

Combos et boutons dans un tableau

Maintenant, vous devriez avoir saisi la manière d'utiliser les modèles de tableaux et les rendus de cellules… Cependant, vous aurez constaté que vos composants sont inertes ! C'est dû au fait que vous allez devoir gérer vous-mêmes la façon dont réagissent les cellules. Cependant, avant d'aborder ce point, nous allons nous pencher sur une autre façon d'afficher correctement des composants dans un JTable. Nous pouvons laisser de côté notre classe servant de modèle et nous concentrer sur les composants.

Commençons par revenir à un code plus sobre :

 1
 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
//CTRL + SHIFT + O pour générer les imports
public class Fenetre extends JFrame {

  private JTable tableau;
  private JButton change = new JButton("Changer la taille");

  public Fenetre(){
    this.setLocationRelativeTo(null);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setTitle("JTable");
    this.setSize(600, 180);

    Object[][] data = {   
      {"Cysboy", "6boy", "Combo", new Boolean(true)},
      {"BZHHydde", "BZH", "Combo", new Boolean(false)},
      {"IamBow", "BoW", "Combo", new Boolean(false)},
      {"FunMan", "Year", "Combo", new Boolean(true)}
    };

    String  title[] = {"Pseudo", "Age", "Taille", "OK ?"};

    this.tableau = new JTable(data, title);
    this.tableau.setRowHeight(30);
    this.getContentPane().add(new JScrollPane(tableau), BorderLayout.CENTER);

  }
  public static void main(String[] args){
    Fenetre fen = new Fenetre();
    fen.setVisible(true);
  }
}

De là, nous allons créer une classe qui affichera un bouton dans les cellules de la seconde colonne et une combo dans les cellules de la troisième colonne… Le principe est simple : créer une classe qui héritera de la classe JButton et qui implémentera l'interface TableCellRenderer. Nous allons ensuite dire à notre tableau qu'il doit utiliser utiliser ce type de rendu pour la seconde colonne.

Voici notre classe ButtonRenderer :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//CTRL + SHIFT + O pour générer les imports
public class ButtonRenderer extends JButton implements TableCellRenderer{

  public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean isFocus, int row, int col) {
    //On écrit dans le bouton ce que contient la cellule
    setText((value != null) ? value.toString() : "");
    //On retourne notre bouton
    return this;
  }
}

Il nous suffit maintenant de mettre à jour le tableau grâce à l'instruction this.tableau.getColumn("Age").setCellRenderer(new ButtonRenderer()); : on récupère la colonne grâce à son titre, puis on applique le rendu ! Résultat en figure suivante.

Objet de rendu de bouton

Votre bouton est de nouveau éditable, mais ce problème sera réglé par la suite… Pour le rendu de la cellule numéro 3, je vous laisse un peu chercher, ce n'est pas très difficile maintenant que vous avez appris cette méthode.

Voici le code ; la figure suivante vous montre le résultat.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//CTRL + SHIFT + O pour générer les imports
public class ComboRenderer extends JComboBox implements TableCellRenderer {

  public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean isFocus, int row, int col) {
    this.addItem("Très bien");
    this.addItem("Bien");
    this.addItem("Mal");
    return this;
  }
}

Rendu d'une combo

Interaction avec l'objet JTable

Dernière ligne droite avant la fin du chapitre… Nous commencerons par le plus difficile et terminerons par le plus simple ! Je vous le donne en mille : le composant le plus difficile à utiliser dans un tableau, entre un bouton et une combo c'est… le bouton !

Eh oui, vous verrez que la combo est gérée presque automatiquement, alors qu'il vous faudra dire aux boutons ce qu'ils devront faire… Pour arriver à cela, nous allons créer une classe qui permettra à notre tableau d'effectuer des actions spécifiques grâce aux boutons.

 1
 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
//CTRL + SHIFT + O pour générer les imports
public class ButtonEditor extends DefaultCellEditor {

  protected JButton button;
  private boolean   isPushed;
  private ButtonListener bListener = new ButtonListener();

  //Constructeur avec une CheckBox
  public ButtonEditor(JCheckBox checkBox) {
    //Par défaut, ce type d'objet travaille avec un JCheckBox
    super(checkBox);
    //On crée à nouveau un bouton
    button = new JButton();
    button.setOpaque(true);
    //On lui attribue un listener
    button.addActionListener(bListener);
  }

  public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { 
    //On précise le numéro de ligne à notre listener
    bListener.setRow(row);
    //Idem pour le numéro de colonne
    bListener.setColumn(column);
    //On passe aussi le tableau en paramètre pour des actions potentielles
    bListener.setTable(table);

    //On réaffecte le libellé au bouton
    button.setText( (value == null) ? "" : value.toString() );
    //On renvoie le bouton
    return button;
  }

  //Notre listener pour le bouton
  class ButtonListener implements ActionListener{        
    private int column, row;
    private JTable table;
    private int nbre = 0;

    public void setColumn(int col){this.column = col;}
    public void setRow(int row){this.row = row;}
    public void setTable(JTable table){this.table = table;}

    public void actionPerformed(ActionEvent event) {
      //On affiche un message, mais vous pourriez effectuer les traitements que vous voulez
      System.out.println("coucou du bouton : " + ((JButton)event.getSource()).getText());
      //On affecte un nouveau libellé à une autre cellule de la ligne
      table.setValueAt("New Value " + (++nbre), this.row, (this.column -1));
    }
  }     
}

Ce code n'est pas très difficile à comprendre… Vous commencez à avoir l'habitude de manipuler ce genre d'objet. Maintenant, afin d'utiliser cet objet avec notre tableau, nous allons lui indiquer l'existence de cet éditeur dans la colonne correspondante avec cette instruction :

1
this.tableau.getColumn("Age").setCellEditor(new ButtonEditor(new JCheckBox()));

Le rendu, visible à la figure suivante, est très probant.

Bouton actif

Vous pouvez voir que lorsque vous cliquez sur un bouton, la valeur dans la cellule située juste à gauche est modifiée. L'utilisation est donc très simple. Imaginez par conséquent que la gestion des combos est encore plus aisée !

Un peu plus tôt, je vous ai fait développer une classe permettant d'afficher la combo normalement. Cependant, il y a beaucoup plus facile… Vous avez pu voir que la classe DefaultCellEditor pouvait prendre un objet en paramètre : dans l'exemple du JButton, il utilisait un JCheckBox. Vous devez savoir que cet objet accepte d'autres types de paramètres :

  • un JComboBox ;
  • un JTextField.

Nous pouvons donc utiliser l'objet DefaultCellEditor directement en lui passant une combo en paramètre… Nous allons aussi enlever l'objet permettant d'afficher correctement la combo afin que vous puissiez juger de l'efficacité de cette méthode.

Voici le nouveau code du constructeur de notre fenêtre :

 1
 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
//CTRL + SHIFT + O pour générer les imports
public class Fenetre extends JFrame {

  private JTable tableau;
  private JButton change = new JButton("Changer la taille");
  //Contenu de notre combo
  private String[] comboData = {"Très bien", "Bien", "Mal"};

  public Fenetre(){
    this.setLocationRelativeTo(null);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setTitle("JTable");
    this.setSize(600, 180);
    //Données de notre tableau
    Object[][] data = {   
      {"Cysboy", "6boy", comboData[0], new Boolean(true)},
      {"BZHHydde", "BZH", comboData[0], new Boolean(false)},
      {"IamBow", "BoW", comboData[0], new Boolean(false)},
      {"FunMan", "Year", comboData[0], new Boolean(true)}
    };
    //Titre du tableau
    String  title[] = {"Pseudo", "Age", "Taille", "OK ?"};
    //Combo à utiliser
    JComboBox combo = new JComboBox(comboData);

    this.tableau = new JTable(data, title);      
    this.tableau.setRowHeight(30);
    this.tableau.getColumn("Age").setCellRenderer(new ButtonRenderer());
    this.tableau.getColumn("Age").setCellEditor(new ButtonEditor(new JCheckBox()));
    //On définit l'éditeur par défaut pour la cellule en lui spécifiant quel type d'affichage prendre en compte
    this.tableau.getColumn("Taille").setCellEditor(new DefaultCellEditor(combo));
    this.getContentPane().add(new JScrollPane(tableau), BorderLayout.CENTER);
  }

  public static void main(String[] args){
    Fenetre fen = new Fenetre();
    fen.setVisible(true);
  }
}

C'est d'une simplicité enfantine ! Le résultat est, en plus, très convaincant, comme le montre la figure suivante.

DefaultCellEditor et combo

Votre cellule se « transforme » en combo lorsque vous cliquez dessus ! En fait, lorsque le tableau sent une action sur cette cellule, il utilise l'éditeur que vous avez spécifié pour celle-ci.

Si vous préférez que la combo soit affichée directement même sans clic de souris, il vous suffit de laisser l'objet gérant l'affichage et le tour est joué. De même, pour le bouton, si vous enlevez l'objet de rendu du tableau, celui-ci s'affiche comme un bouton lors du clic sur la cellule !

Il ne nous reste plus qu'à voir comment rajouter des informations dans notre tableau, et le tour est joué.

Certains d'entre vous l'auront remarqué, les boutons ont un drôle de comportement. Cela est dû au fait que vous avez affecté des comportements spéciaux à votre tableau… Il faut donc définir un modèle à utiliser afin de bien définir tous les points comme l'affichage, la mise à jour, etc.

Nous allons donc utiliser un modèle de tableau personnalisé où les actions seront définies par nos soins. Voici la classe Fenetre modifiée en conséquence :

 1
 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
87
88
89
90
91
//CTRL + SHIFT + O pour générer les imports
public class Fenetre extends JFrame {

  private JTable tableau;
  private JButton change = new JButton("Changer la taille");
  //Contenu de notre combo
  private String[] comboData = {"Très bien", "Bien", "Mal"};

  public Fenetre(){
    this.setLocationRelativeTo(null);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.setTitle("JTable");
    this.setSize(600, 180);
    //Données de notre tableau
    Object[][] data = {   
      {"Cysboy", "6boy", comboData[0], new Boolean(true)},
      {"BZHHydde", "BZH", comboData[0], new Boolean(false)},
      {"IamBow", "BoW", comboData[0], new Boolean(false)},
      {"FunMan", "Year", comboData[0], new Boolean(true)}
    };
    String  title[] = {"Pseudo", "Age", "Taille", "OK ?"};
    JComboBox combo = new JComboBox(comboData);

    //Nous devons utiliser un modèle d'affichage spécifique pour pallier les bugs d'affichage !
    ZModel zModel = new ZModel(data, title);

    this.tableau = new JTable(zModel);
    this.tableau.setRowHeight(30);
    this.tableau.getColumn("Age").setCellRenderer(new ButtonRenderer());
    this.tableau.getColumn("Age").setCellEditor(new ButtonEditor(new JCheckBox()));
    //On définit l'éditeur par défaut pour la cellule
    //En lui spécifiant quel type d'affichage prendre en compte
    this.tableau.getColumn("Taille").setCellEditor(new  DefaultCellEditor(combo));
    this.getContentPane().add(new JScrollPane(tableau), BorderLayout.CENTER);
  }

  class ZModel extends AbstractTableModel{
    private Object[][] data;
    private String[] title;

    //Constructeur
    public ZModel(Object[][] data, String[] title){
      this.data = data;
      this.title = title;
    }

    //Retourne le titre de la colonne à l'indice spécifié
    public String getColumnName(int col) {
      return this.title[col];
    }

    //Retourne le nombre de colonnes
    public int getColumnCount() {
      return this.title.length;
    }

    //Retourne le nombre de lignes
    public int getRowCount() {
      return this.data.length;
    }

    //Retourne la valeur à l'emplacement spécifié
    public Object getValueAt(int row, int col) {
      return this.data[row][col];
    }

    //Définit la valeur à l'emplacement spécifié
    public void setValueAt(Object value, int row, int col) {
      //On interdit la modification sur certaines colonnes !
      if(!this.getColumnName(col).equals("Age") && !this.getColumnName(col).equals("Suppression"))
        this.data[row][col] = value;
    }

    //Retourne la classe de la donnée de la colonne
    public Class getColumnClass(int col){
      //On retourne le type de la cellule à la colonne demandée
      //On se moque de la ligne puisque les types de données sont les mêmes quelle que soit la ligne
      //On choisit donc la première ligne
      return this.data[0][col].getClass();
    }

    public boolean isCellEditable(int row, int col){
      return true;
    }
  }

  public static void main(String[] args){
    Fenetre fen = new Fenetre();
    fen.setVisible(true);
  }
}

Vous aurez remarqué que nous construisons notre tableau par le biais de notre modèle, ce qui implique que nous devrons également passer par le modèle pour modifier le tableau ! Conséquence directe : il va falloir modifier un peu notre classe ButtonEditor.

Voici la classe ButtonEditor utilisant le modèle de tableau pour gérer la modification des valeurs :

 1
 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
//CTRL + SHIFT + O pour générer les imports
public class ButtonEditor extends DefaultCellEditor {

  protected JButton button;
  private boolean   isPushed;
  private ButtonListener bListener = new ButtonListener();

  public ButtonEditor(JCheckBox checkBox) {
    super(checkBox);
    button = new JButton();
    button.setOpaque(true);
    button.addActionListener(bListener);
  }

  public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) { 
    //On affecte le numéro de ligne au listener
    bListener.setRow(row);
    //Idem pour le numéro de colonne
    bListener.setColumn(column);
    //On passe aussi le tableau en paramètre pour des actions potentielles
    bListener.setTable(table);

    //On réaffecte le libellé au bouton
    button.setText( (value == null) ? "" : value.toString() );
    //On renvoie le bouton
    return button;
  }

  //Notre listener pour le bouton
  class ButtonListener implements ActionListener{

    private int column, row;
    private JTable table;
    private int nbre = 0;
    private JButton button;

    public void setColumn(int col){this.column = col;}
    public void setRow(int row){this.row = row;}
    public void setTable(JTable table){this.table = table;}

    public JButton getButton(){return this.button;}

    public void actionPerformed(ActionEvent event) {
      System.out.println("coucou du bouton : " + ((JButton)event.getSource()).getText());
      //On affecte un nouveau libellé à une celulle de la ligne
      ((AbstractTableModel)table.getModel()).setValueAt("New Value " + (++nbre), this.row, (this.column -1));   
      //Permet de dire à notre tableau qu'une valeur a changé à l'emplacement déterminé par les valeurs passées en paramètres
      ((AbstractTableModel)table.getModel()).fireTableCellUpdated(this.row, this.column - 1);
      this.button = ((JButton)event.getSource());
    }
  }
}

Voilà : désormais, le bug d'affichage devrait avoir disparu ! Je vous propose donc de continuer sur notre lancée.

Ajouter des lignes et des colonnes

Je vais profiter de ce point pour vous montrer une autre façon d'initialiser un tableau :

1
2
//data et title sont toujours nos tableaux d'objets !
JTable tableau = new JTable(new DefaultTableModel(data, title));

L'intérêt ? C'est très simple : l'ajout et la suppression dynamiques d'entrées dans un tableau se font via un modèle de rendu, vous vous en doutiez. Cependant, avec cette notation, vous économisez une ligne de code et vous avez la possibilité d'affecter diverses tâches à votre modèle de rendu, comme, par exemple, formater les données…

Dans un premier temps, ajoutons et retirons des lignes à notre tableau. Nous garderons le même code que précédemment avec deux ou trois ajouts :

  • le bouton pour ajouter une ligne ;
  • le bouton pour effacer une ligne.

Le modèle par défaut défini lors de la création du tableau nous donne accès à deux méthodes fort utiles :

  • addRow(Object[] lineData) : ajoute une ligne au modèle et met automatiquement à jour le tableau ;
  • removeRow(int row) : efface une ligne du modèle et met automatiquement à jour le tableau.

Avant de pouvoir utiliser ce modèle, nous allons devoir le récupérer. En fait, c'est notre tableau qui va nous le fournir en invoquant la méthode getModel() qui retourne un objet TableModel. Attention, un cast sera nécessaire afin de pouvoir utiliser l'objet récupéré ! Par exemple : ((ZModel)table.getModel()).removeRow().

Au final, la figure suivante nous montre ce que nous obtiendrons.

Ajout et suppression de lignes

Vous constatez que j'ai ajouté un bouton « Ajouter une ligne » ainsi qu'un bouton « Supprimer la ligne » ; mis à part ça, l'IHM n'a pas changé.

Essayez de développer ces nouvelles fonctionnalités. Pour télécharger le code complet du chapitre, c'est par ici que ça se passe.


  • Pour utiliser le composant « tableau », vous devrez utiliser l'objet JTable.
  • Celui-ci prend en paramètres un tableau d'objets à deux dimensions (un tableau de données) correspondant aux données à afficher, et un tableau de chaînes de caractères qui, lui, affichera les titres des colonnes.
  • Afin de gérer vous-mêmes le contenu du tableau, vous pouvez utiliser un modèle de données (TableModel).
  • Pour ajouter ou retirer des lignes à un tableau, il faut passer par un modèle de données. Ainsi, l'affichage est mis à jour automatiquement.
  • Il en va de même pour l'ajout et la suppression de colonnes.
  • La gestion de l'affichage brut (hors édition) des cellules peut se gérer colonne par colonne à l'aide d'une classe dérivant de TableCellRenderer.
  • La gestion de l'affichage brut lors de l'édition d'une cellule se gère colonne par colonne avec une classe dérivant de DefaultCellEditor.