notes·de·pit

Parfois j'apprends à pêcher à des gens qui n'aiment pas le poisson

Tuto Java, composant JTable

JTable est un composant graphique (Swing) permettant d’obtenir une vue sur un ensemble de données structurées en lignes et colonnes.

starwars-coffee.jpg

Un peu comme suit :

TableDialogEditDemo-tooltip.png



Utilisation simple, voire simpliste

La manière la plus simple et donc la moins paramétrables puisque tout le boulot est déjà fait, est d’utiliser ces constructeurs de JTable

JTable(Object[][] rowData, Object[] columnNames)
JTable(Vector rowData, Vector columnNames)

Le premier argument représente les données dans un tableau à deux dimensions et le deuxième argument les titres des colonnes.

Exemple chez Oracle, SimpleJTableDemo

String[] columnNames = {"First Name",
   "Last Name", "Sport", "# of Years", "Vegetarian"};
 
Object[][] data = {
   {"Kathy", "Smith", "Snowboarding", new Integer(5), new Boolean(false)},
   {"John", "Doe", "Rowing", new Integer(3), new Boolean(true)},
   {"Sue", "Black", "Knitting", new Integer(2), new Boolean(false)},
   {"Jane", "White", "Speed reading", new Integer(20), new Boolean(true)},
   {"Joe", "Brown", "Pool", new Integer(10), new Boolean(false)}
};
 
final JTable table = new JTable(data, columnNames);
table.setFillsViewportHeight(true);

L’instruction table.setFillsViewportHeight(true); permet de demander à la table de prendre toute la place lorsqu’elle se retrouve dans un JScrollPane par exemple.

Le principal avantage de cette méthode, c’est que c’est très simple.

Les inconvénients:

Cette utilisation simple utilise un modèle par défaut pour gérer les données et le comportement de la vue. On est donc bien dans une organisation modèle / vue.

En utilisant ce modèle par défaut:

TableColumn column = table.getColumnModel().getColumn(i);
column.setPreferredWidth(100);

Par défaut, la sélection se fait par lignes entières. On peut alors sélectionner différentes lignes de la manière habituelle (Ctrl-Clic).

table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
table.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
table.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)

Remarque: Il est possible de permettre la sélection de cellules sans pour autant imposer la sélection de lignes entières.

Ceci se fait grâce à des combinaisons des propriétés liées suivantes;

table.setRowSelectionAllowed(true);
table.setColumnSelectionAllowed(true);
table.setCellSelectionEnabled(true);

Lorsque des cellules sont sélectionnées, ce sont les coordonnées des cellules qui sont disponibles. Les méthodes suivantes retournent des int

table.getSelectedRows()
table.getSelectedColumns()

Les deux instructions qui suivent donnent en plus la ligne et la colonne de la dernière cellule sélectionnée:

table.getSelectionModel().getLeadSelectionIndex()
table.getColumnModel().getSelectionModel().getLeadSelectionIndex());

Et si j’en veux plus ?

Si on ne peut se contenter du modèle par défaut, il faudra écrire le sien.

Remarque importante puisque l’on est bien dans une optique modèle / vue, chaque fois que l’on parle de la cellule à la position row,column cela fait référence à la position de la cellule dans le modèle. Si la vue est modifiée (déplacement d’une colonne par exemple) cela n’a pas d’impact sur la position dans le modèle.

Pour permettre la transition, il existe des méthodes de conversion:

int realColumnIndex = convertColumnIndexToModel(colIndex);
int realRowIndex = convertRowIndexToModel(rowIndex);
int viewColumnIndex = convertColumnIndexToView(colIndex);
int viewRowIndex = convertRowIndexToView(rowIndex);

L’organisation des classes gravitant autour de la classe JTable est assez habituelle; une interface spécifiant le contrat, une classe abstraite faisant une partie du boulot et une classe « instanciable » qui a fait des choix par défaut et qui est utilisable en l’état.

Pour écrire son propre modèle, on peut donc:

public int getRowCount() {}
public int getColumnCount() {}
public String getColumnName(int columnIndex) {}
public Class<?> getColumnClass(int columnIndex) { }
public boolean isCellEditable(int rowIndex, int columnIndex) {}
public Object getValueAt(int rowIndex, int columnIndex) {}
public void setValueAt(
   Object aValue, int rowIndex, int columnIndex) {}
public void addTableModelListener(TableModelListener l) {}
public void removeTableModelListener(TableModelListener l) {}
public class TableModelExtends extends AbstractTableModel{
   // ajouter ici son/ses container/s
 
   public int getRowCount() {}
   public int getColumnCount() {}
   public Object getValueAt(int rowIndex, int columnIndex) {}
}

Un type pour chaque cellule

Par défaut chaque cellule est supposée être de type Object, ce qui ne m’arrange pas. Préciser le type de chaque cellule se fait facilement par l’ajout de la méthode suivante à son modèle.

public Class getColumnClass(int c) {
        return getValueAt(0, c).getClass();
}

Dès que la cellule a un type il est possible de modifier la manière dont le contenu est « rendu ». Pour ce faire il faut assigner au type un nouveau CellRenderer qui sera le même pour toutes les cellules de ce type.

Par défaut chaque type a son CellRenderer et un booléen, par exemple, sera représenté par une checkbox. Mais je peux choisir autre chose…

table.setDefaultRenderer(Object.class, new TableCellRenderer() {
   public Component getTableCellRendererComponent(
      JTable table, Object value, boolean isSelected, 
      boolean hasFocus, int row, int column) {
                setToolTipText("My tooltip, " + value);
 
                JLabel label = new JLabel(value.toString());
                Random random = new Random();
                label.setForeground(new Color(
                        random.nextInt(255),
                        random.nextInt(255),
                        random.nextInt(255)));
                return label;
            }
        });

L’appel a setToolTipText() permet de personnaliser le texte présenté lors du passage de la souris sur l’élément 2.

Des cellules non éditables (ou partiellement)

Le modèle par défaut permet l’édition des cellules. C’est la méthode isCellEditable qui en est responsable.

public boolean isCellEditable(int row, int col) {
   //Note that the data/cell address is constant,
   //no matter where the cell appears onscreen.
  return false;
}

Lorsque je change une valeur dans la vue, ce changement est répercuté dans le modèle 3. Je peux en être informé… si je décide d’écouter la vue. Pour ce faire, implémenter TableModelListener, récrire la méthode tableChanged

public void tableChanged(TableModelEvent e) {
   int row = e.getFirstRow();
   int column = e.getColumn();
   TableModel model = (TableModel) e.getSource();
   String columnName = model.getColumnName(column);
   Object data = model.getValueAt(row, column);
 
   // do something with data
   System.out.println("Value changed, type " 
      + data.getClass() + " value " + data);
}

et ne pas oublier de s’inscrire comme écouteur:

table.getModel().addTableModelListener(this);

Pour modifier une cellule de la JTable, je peux utiliser l’éditeur par défaut, en définir un (ce que nous ne ferons pas ici) ou utiliser une combo box. Pour utiliser une combo box, il suffit de passer une JCombobox au constructeur de DefaultCellEditor

TableColumn sportColumn = table.getColumnModel().getColumn(2);
...
JComboBox comboBox = new JComboBox();
comboBox.addItem("Snowboarding");
comboBox.addItem("Rowing");
comboBox.addItem("Chasing toddlers");
comboBox.addItem("Speed reading");
comboBox.addItem("Teaching high school");
comboBox.addItem("None");
sportColumn.setCellEditor(new DefaultCellEditor(comboBox));

Stocker ses données autrement

Si je ne désire pas stocker mes données dans un tableau à deux dimensions (ou un Vector), je dois choisir mon conteneur. Dès que c’est fait, l’écriture des méthodes suivantes suffit.

Object getValueAt(int row, int col) 
int getColumnCount()
int getRowCount()

Trier les colonnes

Par défaut les colonnes ne peuvent être triées. Pour l’autoriser, positionner la propriété liée AutoCreateRowSorter à vrai permet d’utiliser un tri par défaut… c’est à dire basé sur le comparator de la classe 4.

table.setAutoCreateRowSorter(true);

Pour rappel, lorsque l’on trie des colonnes, c’est bien la vue qui est triée. Le modèle sous-jacent reste inchangé. Accéder aux données implique une «translation» comme dit plus haut.

Imprimer

Pour imprimer la table, un appel à la méthode print() fait le boulot.

Points non abordés

Enjoy !


Liens et crédits

How to use Tables (Oracle). Les exemples proviennent pour la plupart du tuto de Oracle.
Crédit photo, BigRedCurlyGuy

À lire aussi


  1. Oracle semble privilégier d’hériter de AbstractTableModel. 

  2. À ce stade, ça ne fonctionne pas « comme ça » chez moi. Je corrige dès que je (ou vous) trouvez quelque chose. 

  3. Le tableau passé en paramètre est modifié. Si ma source de données est une BD, il faudra répercuter le changement. C’est sans doute l’occasion de le faire. 

  4. Si l’on ne s’est pas arrangé pour que les cellules aient un type, c’est un tri par ordre alphabétique de toString qui se fait.