Un peu comme suit;

TableDialogEditDemo-tooltip.png


  • Utilisation simple, voire simpliste
  • Et si j'en veux plus ?
    • Un type pour chaque cellule
    • Des cellules non éditables (ou partiellement)
    • Stocker ses données autrement
    • Trier les colonnes
    • Imprimer
  • Points non abordés

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:

  • toutes les cellules sont éditables. Dans l'exemple, le tableau passé en paramètre est modifié lorsque l'on édite une cellule;
  • les données sont des Objects, qui sont présentés comme des String;
    • remplacer la valeur de l'entier 5 de Kathy Smith dans l'exemple par la valeur "foo", ne pose aucun problème (ce n'est peut-être pas ce que l'on cherche).
    • les booléen sont également convertis en String alors que l'on pourrait s'attendre à ce qu'ils soient représentés par une checkbox par exemple.
  • les données doivent se trouver dans un tableau ou dans un vecteur. Ceci ne permet pas de placer les données ailleurs, dans une base de données ou structurées différemment.

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:

  • il est possible de changer la taille d'une colonne. La table essayera de maintenir les proportions lors d'un redimensionnement;
TableColumn column = table.getColumnModel().getColumn(i);
column.setPreferredWidth(100);
  • lorsque l'utilisateur fait une sélection, il est possible de la récupérer.

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:

  • implémenter l'interface TableModel
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) {}
  • hériter directement de AbstractTableModel et écrire les méthodes abstraites
public class TableModelExtends extends AbstractTableModel{
   // ajouter ici son/ses container/s
 
   public int getRowCount() {}
   public int getColumnCount() {}
   public Object getValueAt(int rowIndex, int columnIndex) {}
}
  • hériter de DefaultTableModel qui hérite de AbstractTableModel qui implémente TableModel … et ne récrire que ce dont on a besoin[1]

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

  • Dans la suite du tri des données, la possibilité de les filtrer est également donnée.
  • Comment définir son propre éditeur de cellules
  • La table est capable d'écouter les modifications des données

Enjoy !


Liens et crédits

À lire aussi

Notes

[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