Les arbres et les tableaux

Chapitres traités   

Nous avons décrit pratiquement l'ensemble des composants swing les plus utilisés pour concevoir des interfaces homme machine relativement sophistiqués. Il nous reste à voir deux composants qui peuvent également s'avérer très utiles dans certaines situations.

Le premier composant représenté par la classe JTree permet de visualiser une structure hiérarchique sous forme d'arbre. Naturellement, nous connaissons bien l'arbre proposé par les explorateurs qui visualise le système de fichier en séparant bien les répertoires, les sous-répertoires et les fichiers. Il existe un grand nombre de structures en arbres dans la vie de tous les jours, et nous verrons au cours de cette étude comment les mettre en oeuvre.

De même, il existe dans swing un composant, JTable, très élaboré, qui affiche une grille bidimensionnelle d'objets. Là aussi, les tableaux sont très courant dans les interfaces utilisateur. De par leur nature, les tableaux sont compliqués, mais, peut-être plus que d'autres classes de swing, le composant JTable prend en charge la plus grosse partie de cette complexité. Vous pourrez produire des tableaux parfaitement fonctionnels avec un comportement très riche, en écrivant uniquement quelques lignes de code.

Choix du chapitre Les arbres - JTree

Tous les utilisateurs d'ordinateurs possédant un système de fichiers hiérarchiques ont déjà rencontré des arbres. En tant que programmeurs, il nous faut souvent afficher des structures hiérarchiques. La classe JTree prend en charge l'organisation des arbres et le traitement des requêtes de l'utilisateur visant à ajouter et à supprimer des noeuds.

Terminologie

Avant de poursuivre, je pense qu'il est souhaitable de se mettre d'accord sur quelques éléments de terminologie :

  1. Un arbre est composé de noeuds.
  2. Un noeud peut soit être une feuille, soit posséder des noeuds enfants.
  3. Chaque noeud, à l'exception du noeud de départ (la racine), possède un seul parent.
  4. Un arbre possède un seul noeud de départ.
  5. Des arbres peuvent être assemblés dans un groupe, chaque arbre possédant sa propre racine. Ce type de groupe est appelé une forêt.

Modèle MVC

JTree est l'un des composants les plus élaborés. Les arborescences conviennent parfaitement à la représentation hiérarchique d'informations, comme le contenu d'un disque dur ou l'organigramme d'une entreprise. Comme la plupart des autres composants swing, le modèle de données est distinct de la représentation visuelle, et le composant JTree se doit de respecter cette architecture Modèle-Vue-Contrôleur.

Ainsi, un modèle de données hiérarchiques doit être fourni à l'arbre qui affiche alors ces données pour vous. Cela signifie que vous pouvez par exemple mettre à jour le modèle de données et être certain que le composant visuel sera correctement actualisé.

JTree est très puissant et complexe. En fait, il est si compliqué que les classes qui gèrent JTree possèdent leur propre paquetage, javax.swing.tree. Néanmoins, si vous acceptez les options par défaut presque partout, JTree s'avère très simple à utiliser.

Création d'un arbre

Pour construire un JTree, et en respectant ce que nous venons d'évoquer, vous devez spécifier le modèle d'arbre dans le construsteur :

TreeModel modèle = ...
JTree arbre = new JTree(modèle);

Il existe également des constructeurs qui permettent de créer des arbres à partir d'un ensemble d'éléments :

  1. JTree(Object[] noeuds)
  2. JTree(Vector<?> noeuds)
  3. JTree(Hashtable<?, ?> noeuds) : les valeurs sont transformées en noeuds.

Ces constructeurs ne sont pas très utiles. Ils permettent surtout de générer des forêts d'arbres, chaque arbre possédant un seul noeud. Le troisième constructeur semble particulièrement inutile puisque les noeuds sont organisés selon l'ordre aléatoire fourni par les codes de hachage des clés.

Les noeuds et les modèles

Du coup, la question qui se pose, c'est comment obtenir un modèle d'arbre ? Vous pouvez construire votre modèle en créant une classe qui implémente l'interface TreeModel. Nous verrons plus loin dans cette étude comment procéder. Le plus simple, consiste à prendre le modèle prédéfini par défaut dans la bibliothèque swing, justement nommé DefaultTreeModel :

TreeNode racine = ... ;
TreeModel
modèle = new DefaultTreeModel(racine);
JTree arbre = new JTree(modèle);

Le modèle de données d'une arborescence est constitué de noeuds interconnectés. Un noeud possède un nom, en principe un parent et un certain nombre d'enfants (éventuellement aucun). Dans swing, un noeud est représenté par l'interface TreeNode. Les noeuds modifiables sont représentés cette fois-ci par l'interface MutableTreeNode qui hérite en fait de TreeNode. Là aussi, nous pouvons créer des classes de noeud qui implémentent ces interfaces, toutefois il existe une implémentaiton concrète de l'interface MutableTreeNode qui se nomme DefaultMutableTreeNode.

TreeNode racine = new DefaultMutableTreeNode("Noeud racine");
TreeModel
modèle = new DefaultTreeModel(racine);
JTree arbre = new JTree(modèle);

Structure d'un noeud d'arbre mutable

Un noeud d'arbre mutable par défaut renferme un objet, et plus précisément un objet de l'utilisateur. L'arbre peut transformer les objets de l'utilisateur contenus dans chaque noeud. A moins que vous ne spécifiiez une méthode de transformation, l'arbre se contente d'afficher une chaîne résultant de la méthode toString().

Dans l'exemple précédent, nous nous servons de chaînes comme objets de l'utilisateur. En pratique, vous remplirez probablement des arbres avec des objets d'utilisateur plus importants. Par exemple, pour afficher un arbre de répertoire, il convient de le remplir avec des objets File.

Vous pouvez spécifier le type des objets d'utilisateur dans le constructeur, mais vous pouvez également le définir par la suite grâce à la méthode setUserObject() :
DefaultMutableTreeNode noeud = new DefaultMutableTreeNode("Un noeud");
noeud.setUserObject("Un autre noeud"); 

Mise en place de la hiérarchie

Ensuite, il faut établir les relations hiérarchies entre les parents et les enfants pour chaque noeud.

  1. Commencez par le noeud racine, et utilisez la méthode add() pour ajouter des enfants :
    DefaultMutableTreeNode racine = new DefaultMutableTreeNode("Images");
    DefaultMutableTreeNode jpeg = new DefaultMutableTreeNode("JPEG");
    DefaultMutableTreeNode gif = new DefaultMutableTreeNode("GIF");
    DefaultMutableTreeNode png = new DefaultMutableTreeNode("PNG");
    racine.add(jpeg);
    racine.add(gif);
    racine.add(png);
  2. Vous devez relier tous les noeuds de cette manière. Construisez ensuite un DefaultTreeModel avec le noeud racine. Pour terminer, construisez un JTree avec le modèle de l'arbre :
    TreeModel modèle = new DefaultTreeModel(racine); 
    JTree arbre = new JTree(modèle);
  3. Plus simplement, il suffit de passer le noeud racine au constructeur JTree(). L'arbre construit alors automatiquement un modèle d'arbre par défaut :
    JTree arbre = new JTree(racine);

Première mise en application

Afin d'illustrer notre première approche sur la gestion d'une structure arborescente, je vous propose de mettre en oeuvre une petite application qui permet de visualiser une image à partir d'un répertoire prédéterminé. Le choix de l'image s'effectue à partir d'un arbre situé sur la partie gauche. Cet arbre recense les images par type d'extension : JPEG, GIF et PNG.

codage correspondant
package arbres;

import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import javax.imageio.ImageIO;
import javax.swing.event.*;
import javax.swing.tree.DefaultMutableTreeNode;

public class Arbres extends JFrame implements TreeSelectionListener {
   private JTree arbre;
   private Vue vue = new Vue();
   private String répertoire = "C:/Photos/";
   
   public Arbres() {
      super("Images");
      construireArbre();
      add(new JScrollPane(arbre), BorderLayout.WEST);
      add(vue);
      setSize(540, 300);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }
   
   public static void main(String[] args) { new Arbres(); }

   private void construireArbre() {
      File fichiers = new File(répertoire);
      DefaultMutableTreeNode racine = new DefaultMutableTreeNode("Images");
      DefaultMutableTreeNode jpeg = new DefaultMutableTreeNode("JPEG");
      DefaultMutableTreeNode gif = new DefaultMutableTreeNode("GIF");
      DefaultMutableTreeNode png = new DefaultMutableTreeNode("PNG");
      racine.add(jpeg);
      racine.add(gif);
      racine.add(png);
      for (String nom : fichiers.list()) {
         if (nom.endsWith(".gif")) gif.add(new DefaultMutableTreeNode(nom));
         else if (nom.endsWith(".jpeg") || nom.endsWith(".jpg")) jpeg.add(new DefaultMutableTreeNode(nom));
         else if (nom.endsWith(".png")) png.add(new DefaultMutableTreeNode(nom));
      }
      arbre = new JTree(racine);
      arbre.setPreferredSize(new Dimension(180, 1000));
      arbre.addTreeSelectionListener(this);
   }

   private class Vue extends JComponent {
      private BufferedImage photo;
      private double ratio;

      @Override
      protected void paintComponent(Graphics g) {
         if (photo!=null)  g.drawImage(photo, 0, 0, getWidth(), (int)(getWidth()/ratio), null);
      }
      
      public void setPhoto(File fichier) {
         try {
            photo = ImageIO.read(fichier);
            ratio = (double)photo.getWidth() / photo.getHeight();
            repaint();           
         } 
         catch (IOException ex) { setTitle("Impossible de lire le fichier");}     
      }
   }

   public void valueChanged(TreeSelectionEvent e) {
      if (arbre.getSelectionPath()!=null) {
         String nom = arbre.getSelectionPath().getLastPathComponent().toString();
         vue.setPhoto(new File("C:/Photos/"+nom));
      }
   }
}

Lorsque vous exécutez ce programme, seuls le noeud racine (Images) et ses enfants sont visibles (JPEG, GIF et PNG). Cliquez sur les poignées pour ouvrir les arbres de niveau inférieur. Le segment dépassant des poignées se trouve sur la droite lorsque le sous-répertoire est caché, et il pointe vers le bas lorsque le sous-répertoire est affiché.

Il semble que ces segments représentent des poignées de porte. Il faut appuyer sur la poignée pour ouvrir le sous-répertoire.
§

Changer l'apparence de l'arbre

   
Par défaut l'arbre affiche des lignes entre les parents et les enfants, ce qui permet de bien vérifier rapidement l'appartenance des différents éléments (le style par défaut est Angled).

Il est possible d'enlever ces lignes de liaisons. Utilisez pour cela la méthode putClientProperty() de la classe JTree. Positionnez alors la propriété JTree.lineStyle à None :

arbre.putClientProperty("JTree.lineStyle", "None");

A l'inverse, pour vous assurez que les lignes sont bien affichées, utilisez :
arbre.putClientProperty("JTree.lineStyle", "Angled");

Un autre style appelé Horizontal permet d'afficher l'arbre avec des lignes horizontales séparant uniquement les enfants du noeud racine.

arbre.putClientProperty("JTree.lineStyle", "Horizontal");

 

Par défaut, il n'existe aucune poignée pour cacher la racine d'un arbre. Si vous le désirez, vous pouvez en ajouter une avec la méthode setShowsRootHandles() :

arbre.setShowsRootHandles(true);

Inversement, la racine peut être entièrement cachée. Cela peut être utile si vous souhaitez afficher une forêt, c'est-à-dire un ensemble d'arbres possédant chacun leur propre racine. Vous devez cependant regrouper tous les arbres de la forêt avec une seule racine commune. Vous devez alors cacher cette racine au moyen de la méthode setRootVisible() :

arbre.setRootVisible(false);

 

Il est possible de combiner ces deux dernières méthodes afin que tous les arbres de la forêt disposent de poignées pour faciliter le développement des feuilles :

arbre.setShowsRootHandles(true);
arbre.setRootVisible(false);

 

Passons maintenant de la racine aux feuilles de l'arbre. Notez que les feuilles possèdent une icône différente de celle des autres noeuds. Lorsque l'arbre est affiché, chaque noeud est représenté par une icône. Il existe en fait trois sortes d'icônes :

  1. Les icônes de feuilles.
  2. Les icônes de noeuds intermédiaires ouverts.
  3. Les icônes de noeuds intermédiaires fermés.

Pour des raisons de simplicité, nous appelerons les deux dernières icônes, des icônes de répertoire.
§

L'afficheur de noeud doit savoir quelle icône utiliser pour chaque noeud. Par défaut, cette décision est prise de la façon suivante : si la méthode isLeaf() d'un noeud renvoie true, l'icône de feuille est utilisée. Sinon, une icône de répertoire est utilisée.

La méthode isLeaf() de la classe DefaultMutableTreeNode renvoie true si le noeud ne possède aucun enfant. Par conséquent, les noeuds possédant des enfants sont associés à des icônes de répertoire, et les noeuds sans enfant sont associés à des icônes de feuille.

Parfois, cette technique n'est pas toujours appropriée. Supposons que nous ajoutions un noeud "Autre" dans notre exemple de visualisation d'images, qui permet de recenser les autres formats d'images, mais que le répertoire en question ne possède pas d'éléments. Il convient cependant d'éviter d'affecter une icône de feuille à ce noeud puisque seuls les fichiers images correspondent à des feuilles.

La classe JTree ne possède aucune information lui permettant de déterminer si un noeud doit être considéré comme une feuille ou comme un répertoire. Elle le demande donc au modèle de l'arbre. Si un noeud sans enfant n'est pas toujours interprété au plan conceptuel comme une feuille, vous pouvez demander au modèle d'utiliser différents critères pour vérifier qu'un noeud est bien une feuille, en interrogeant la propriété AllowsChildren d'un noeud. Régler cette propriété au moyen de la méthode setAllowsChildren() :

  1. Pour les noeuds correspondant à des feuilles qui ne devraient donc pas avoir d'enfant :

    noeud.setAllowsChildren(false);

  2. Pour les noeuds qui possèdent des enfants :

    noeud.setAllowsChildren(true);

Il est possible également d'utiliser le constructeur de la classe DefaultMutableTreeNode qui dispose de deux paramètres, le deuxième attend une valeur booléenne spécifiant si le noeud va posséder des enfants ou pas :
  1. Pour les noeuds correspondant à des feuilles qui ne devraient donc pas avoir d'enfant :

    DefaultMutableTreeNode noeud = new DefaultMutableTreeNode("Noeud", false);

  2. Pour les noeuds qui possèdent des enfants :

    DefaultMutableTreeNode noeud = new DefaultMutableTreeNode("Noeud", true);

Ensuite, il faut indiquer au modèle qu'il doit examiner la propriété AllowsChildren d'un noeud pour savoir s'il doit être affiché avec une icône de feuille ou non. La méthode setAskAllowsChildren() de la classe DefaultTreeModel permet de définir ce comportement :
TreeModel modèle = new DefaultTreeModel(racine); 
modèle.setAskAllowsChildren(true);
Ici aussi, vous pouvez également prévoir cette fonctionnalité directement à partir du constructeur du modèle, en proposant la valeur true sur le deuxième argument :
TreeModel modèle = new DefaultTreeModel(racine, true);

A partir de ces critères de décision, les noeuds susceptibles d'avoir des enfants sont associés à des icônes de répertoire, et les autres à des icônes de feuilles.

Sinon, si vous construisez un arbre en fournissant un noeud racine (sans modèle), vous pouvez spécifier ce comportement également directement dans le constructeur de la classe JTree :
JTree arbre = new JTree(racine, true);

modification du source correspondant à l'apparence ci-contre
private void construireArbre() {
   File fichiers = new File(répertoire);
   DefaultMutableTreeNode racine = new DefaultMutableTreeNode("Images");
   DefaultMutableTreeNode jpeg = new DefaultMutableTreeNode("JPEG", true);
   DefaultMutableTreeNode gif = new DefaultMutableTreeNode("GIF", true);
   DefaultMutableTreeNode png = new DefaultMutableTreeNode("PNG", true);
   DefaultMutableTreeNode autre = new DefaultMutableTreeNode("Autre", true);
   racine.add(jpeg);      
   racine.add(gif);
   racine.add(png);
   racine.add(autre);
   for (String nom : fichiers.list()) {
      if (nom.endsWith(".gif")) gif.add(new DefaultMutableTreeNode(nom, false));
      else if (nom.endsWith(".jpeg") || nom.endsWith(".jpg")) jpeg.add(new DefaultMutableTreeNode(nom, false));
      else if (nom.endsWith(".png")) png.add(new DefaultMutableTreeNode(nom, false));
      else autre.add(new DefaultMutableTreeNode(nom, false));
   } 
   arbre = new JTree(racine, true);
   arbre.setPreferredSize(new Dimension(180, 1000));
   arbre.addTreeSelectionListener(this);
}     

 

Choix du chapitre Identifier les noeuds d'un arbre

Une fois que l'arbre est constitué, notamment de façon automatique, il peut être intéressant de retrouver certains noeuds suivant les critères désirés. La classe DefaultMutableTreeNode dispose d'un grand nombre de méthodes qui vons nous aider pour résoudre ce problème. Deux démarches sont utilisées :

  1. La première consiste à énumérer l'ensemble des noeuds, quelque soit la profondeur, à partir d'un noeud précis.
  2. La deuxième nous permet de récupérer un noeud en particulier suivant sa qualité : une feuille, un enfant, un parent, etc.

Enumération des noeuds

Il arrive parfois que vous deviez trouver un noeud dans un arbre, en partant de la racine et en passant en revue tous les enfants jusqu'à ce que vous ayez trouvé le noeud. La classe DefaultMutableTreeNode possède plusieurs méthodes pratiques pour parcourir les noeuds d'un arbre.

Les méthodes breadthFirstEnumeration() et depthFirstEnumeration() renvoient des objets d'énumération dont la méthode nextElement() parcourt tous les enfants du noeud courant, en utilisant soit une approche horizontale, soit une approche verticale.

  1. L'approche horizontale est la plus simple à visualiser. L'arbre est parcouru par niveaux, en commençant par la racine, suivie de tous ses enfants, puis de tous ses petits-enfants, etc.


  2. Pour visualiser une approche verticale, imaginez qu'un rat soit emprisonné dans un labyrinthe en forme d'arbre. Il descend l'arbre jusqu'à ce qu'il trouve une feuille, puis il remonte d'un niveau et parcourt la prochaine branche, etc.

Cette dernière approche est aussi appelée une traversée postérieure en informatique, parce que la recherche commence par les enfants avant d'arriver aux parents. La méthode postOrderTraversal() est donc équivalente à la méthode depthFirstTraversal(). Pour que la bibliothèque soit complète, il existe aussi une méthode preOrderTraversal() qui propose également une recherche verticale qui passe en revue les parents avant les enfants.

Voici un exemple typique d'utilisation :

Enumeration recherche = racine.breadthFirstEnumeration();
while (recherche.hasMoreElements()) {
   DefaultMutableTreeNode noeud = (DefaultMutableTreeNode) recherche.nextElement();
   ...
}
Il existe une méthode pathFromAncestorEnumeration() qui trouve un chemin entre un ancêtre et un noeud spécifié, puis parcourt tous les noeuds se trouvant sur ce chemin. Cette méthode est assez simple, en fait elle se contente d'appeler la méthode getParent() jusqu'à ce que l'ancêtre spécifié soit trouvé, puis elle affiche ensuite en sens inverse le chemin parcouru.
Enumeration recherche = feuille.breadthFirstEnumeration(racine);

Pour terminer la méthode children() renvoit une énumération des enfants (immédiats : premier niveau sans les petits enfants) d'un noeud :

Enumeration enfants = racine.children();

Recherche d'un noeud en particulier

Il existe ensuite des méthodes de la classe DefaultMutableTreeNode que nous allons recenser, qui vont nous permettre de naviguer dans l'arborescence à la recherche d'un noeud en particulier :

Mise en pratique de ces différentes recherches

Nous allons reprendre l'application précédente sur laquelle nous allons faire quelque petites modifications. Nous allons en effet restructurer l'arborescence de l'arbre des fichiers. Le noeud racine s'appelle cette fois-ci "Fichiers". Ce noeud racine comporte deux autres noeuds : le premier intitulé "Images" et le second "Autre". Cette fois-ci, la mise en place des feuilles de l'arbre, correspondant aux fichiers présents dans le répertoire choisi, s'effectue automatiquement suivant le nom des noeuds fils proposés à partir du noeud "Images". S'il reste des fichiers, ceux-ci sont automatiquement placés dans le noeud "Autre".

codage correspondant
package arbres;

import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.*;
import javax.imageio.ImageIO;
import javax.swing.event.*;
import javax.swing.tree.*;

public class Arbres extends JFrame implements TreeSelectionListener {
   private JTree arbre;
   private DefaultMutableTreeNode racine;
   private DefaultMutableTreeNode images;
   private DefaultMutableTreeNode autre;
   private Vue vue = new Vue();
   private String répertoire = "C:/Photos/";

   public Arbres() {
      super("Images");
      construireArbre();
      add(new JScrollPane(arbre), BorderLayout.WEST);
      add(vue);
      setSize(540, 300);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }
   
   public static void main(String[] args) { new Arbres(); }

   private void construireArbre() { 
      racine = new DefaultMutableTreeNode("Fichiers", true);     
      images = new DefaultMutableTreeNode("Images", true);
      DefaultMutableTreeNode jpeg = new DefaultMutableTreeNode("JPG", true);  
      DefaultMutableTreeNode gif = new DefaultMutableTreeNode("GIF", true);
      DefaultMutableTreeNode png = new DefaultMutableTreeNode("PNG", true);
      autre = new DefaultMutableTreeNode("Autre", true);
      racine.add(images);      
      racine.add(autre);
      images.add(jpeg);
      images.add(gif);
      images.add(png);
      arbre = new JTree(racine, true);
      arbre.setPreferredSize(new Dimension(180, 1000));
      arbre.addTreeSelectionListener(this);
      ajouterFichiers();
   }
   
   private void ajouterFichiers() {
      File fichiers = new File(répertoire);
      ArrayList<String> liste = new ArrayList<String>();
      for (String nom : fichiers.list()) liste.add(nom);      
      Enumeration recherche = images.children();
      while (recherche.hasMoreElements()) {
         DefaultMutableTreeNode noeud = (DefaultMutableTreeNode) recherche.nextElement();            
         for (String nom : liste) {
            String extension = nom.split("\\.")[1];
            if (extension.equalsIgnoreCase(noeud.toString())) 
               noeud.add(new DefaultMutableTreeNode(nom, false));
         }   
      }             
      for (DefaultMutableTreeNode noeud = images.getFirstLeaf(); noeud !=null; noeud = noeud.getNextLeaf()) 
         liste.remove(noeud.toString());
      for (String nom : liste) autre.add(new DefaultMutableTreeNode(nom, false));
   }
	
   private class Vue extends JComponent {
      private BufferedImage photo;
      private double ratio;

      @Override
      protected void paintComponent(Graphics g) {
         if (photo!=null)  g.drawImage(photo, 0, 0, getWidth(), (int)(getWidth()/ratio), null);
      }
      
      public void setPhoto(File fichier) {
         try {
            photo = ImageIO.read(fichier);
            ratio = (double)photo.getWidth() / photo.getHeight();
            repaint();           
         } 
         catch (IOException ex) { setTitle("Impossible de lire le fichier");}     
      }
   }

   public void valueChanged(TreeSelectionEvent e) {
      if (arbre.getSelectionPath()!=null) {
         String nom = arbre.getSelectionPath().getLastPathComponent().toString();
         vue.setPhoto(new File("C:/Photos/"+nom));
      }
   }
}

 

Choix du chapitre Modifier des arbres et leur structure

Dans ce chapitre, nous allons apprendre à modifier un arbre en "temps réel". Il est par exemple possible d'ajouter un nouveau noeud par rapport à un autre noeud de référence. Ce nouveau noeud peut être considéré comme un fils du noeud de référence ou comme un frère (sibling). Le noeud de référence peut aussi être supprimé à tout moment.

Pour implémenter ce comportement, vous devrez identifier le noeud sélectionné. La classe JTree possède une technique étonnante pour identifier les noeuds d'un arbre. Elle ne gère pas les noeuds de l'arbre, mais les chemins des objets, appelés chemins de l'arbre. Un chemin d'arbre commence à la racine et correspond à une séquence de noeuds enfant.

Vous vous demandez peut-être pourquoi la classe JTree a besoin du chemin complet. Ne peut-elle pas se contenter de récupérer un TreeNode et d'appeler en boucle sa méthode getParent() ? En fait, la classe JTree ne connaît pas du tout l'interface TreeNode. Cette interface n'est en effet jamais utilisée par l'interface TreeModel. Elle ne sert qu'à l'implémentation de DefaultTreeModel. Vous pouvez posséder d'autres modèles d'arbres dans lesquels les noeuds n'implémentent pas du tout l'interface TreeNode. Si vous avez recours à un modèle d'arbre qui gère d'autres types d'objets, ces derniers peuvent ne pas avoir de méthodes getParent() et getChild(). Ils doivent cependant posséder des connexions entre eux. Cette tâche revient au modèle d'arbre. La classe JTree elle-même n'a aucune idée de la nature de leurs connexions. Pour cette raison, la classe JTree doit toujours travailler avec des chemins complets.

Retrouver la sélection

La classe TreePath gère une séquence de références d'Object (et pas de TreeNode). Un certain nombre de méthode de JTree renvoient des objets TreePath. Lorsque vous possédez un chemin d'arbre, il vous suffit en général de connaître le noeud final, que vous pouvez récupérer grâce à la méthode getLastPathComponent(). Par exemple, pour trouver quel noeud est couramment sélectionné dans un arbre, vous pouvez utiliser la méthode getSelectionPath() de la classe JTree. Vous obtiendrez en retour un objet TreePath, d'où vous déduirez le noeud sélectionné :

TreePath chemin = arbre.getSelectionPath();
DefaultMutableTreeNode noeud = (DefaultMutableTreeNode) chemin.getLastPathComponent();
En fait, comme cette requête est très fréquente, il existe une méthode pratique qui vous fournit immédiatement le noeud sélectionné :
DefaultMutableTreeNode noeud = (DefaultMutableTreeNode) arbre.getLastSelectedPathComponent();

Cette méthode n'est pas appelée getSelectedNode() parce que l'arbre ne sait pas qu'il renferme des noeuds. Seul le modèle d'arbre gère les chemins des objets.

Les chemins d'arbre sont l'une des deux techniques utilisées par la classe JTree pour décrire les noeuds. Il existe d'autres méthodes JTree qui acceptent ou renvoient un indice entier, une position de ligne. Une position de ligne est simplement un numéro de ligne (commençant par 0) correspondant au noeud spécifié dans l'arbre affiché. Seuls les noeuds visibles posèdent un numéro de ligne, et le numéro de ligne d'un noeud change si les noeuds qui le précèdent sont cachés, affichés ou modifiés. C'est pourquoi, il vaut mieux éviter de travailler avec des positions de ligne. Toutes les méthodes de JTree qui se servent de lignes possèdent un équivalent utilisant des chemins d'arbre.

Modifications sur un noeud

Une fois que vous avez trouvé le noeud sélectionné, vous pouvez le modifier. Cependant, ne vous contentez pas d'ajouter des enfants à un noeud :

noeud.add(nouveauNoeud); // non
Si vous modifiez la structure des noeuds, vous modifiez le modèle, mais l'affichage associé n'est pas mis à jour. Vous pouvez envoyer une notification par vous-même, mais si vous utilisez la méthode insertNodeInto() de la classe DefaultTreeModel, vous refaites le travail de la classe du modèle. Par exemple, l'appel suivant ajoute un noeud et le déclare comme étant le dernier noeud du noeud sélectionné, et met à jour l'affichage de l'arbre :
modèle.inserNodeInto(nouveauNoeud, noeud, noeud.getChildCount());

L'appel similaire à removeNodeFromParent() supprime un noeud et met à jour l'affichage :

modèle.removeNodeFromParent(noeud);
Si vous conservez la structure des noeuds, mais que vous modiffiez un objet utilisateur, vous devrez appeler la méthode nodeChanged() :
modèle.nodeChanged(noeudChangé);

La classe DefaultTreeModel possède une méthode reload() qui recharge le modèle entier. Cependant, évitez d'appeler cette méthode uniquement pour mettre à jour votre arbre lorsque vous avez apporté des modifications. Lorsqu'un arbre est généré à nouveau, tous les noeuds situés après les enfants de la racine sont cachés. Cela peut être extrêmement déconcertant pour vos utilisateurs, s'ils doivent ouvrir à nouveau leur arbre après chaque modification.

La classe DefaultTreeModel possède également une méthode reload() qui permet de ne recharger que les descendants d'un noeud particulier spécifié en argument de la méthode.

Pour apporter des modifications sur les noeuds d'un arbre, vous remarquez que vous êtes obligé de passer systématiquement par le modèle de l'arbre. Soit, vous construisez ce modèle dès le départ, au moyen de la classe DefaultTreeModel que vous passez ensuite en argument du constructeur de l'arbre JTree. Ou bien, vous le récupérez à partir de l'arbre au moyen de la méthode getModel() de la calsse JTree.

Gestion de l'affichage et construction de chemins d'arbre

Lorsque l'affichage est mis à jour à cause d'une modification de la structure des noeuds, les enfants ajoutés ne sont pas automatiquement affichés. En particulier, si l'un des utilisateurs ajoutait un nouvel enfant à un noeud dont les enfants sont actuellement cachés, le nouvel enfant le serait aussi. Cela ne fournit à l'utilisateur aucune information sur le fonctionnement de la commande qu'il vient d'effectuer. Dans ce cas, il convient d'ouvrir tous les noeuds parent pour que le noeud qui vient d'être ajouté soit visible. Vous pouvez vous servir de la méthode makeVisible() de la classe JTree dans ce but. La méthode makeVisible() attend un chemin d'arbre pointant sur le noeud qu'elle doit rendre visible.

Par conséquent, vous serez amené à construire un chemin d'arbre à partir de la racine et allant jusqu'au nouveau noeud. Pour obtenir un chemin d'arbre, il faut commencer par appeler la méthode getPathToRoot() de la classe DefaultTreeModel. Elle renvoie un tableau TreeNode[] contenant tous les noeuds situés entre un noeud et la racine. Vous pouvez alors passer ce tableau à un constructeur TreePath :
TreeNode[] noeuds = modèle.getPathToRoot(nouveauNoeud);
TreePath chemin = new TreePath(noeuds);
arbre.makeVisible(chemin);

Il est assez étrange que la classe DefaultTreeModel fasse semblant d'ignorer la classe TreePath, même si son travail est de communiquer avec un JTree. La classe JTree se sert beaucoup de chemins d'arbre, alors qu'elle n'utilise jamais de tableaux d'objets de noeuds.

Mais supposons maintenant que votre arbre fasse partie d'un panneau d'affichage déroulant. Après l'expansion des noeuds de l'arbre, le nouveau noeud risque une nouvelle fois de ne pas être visible parce qu'il peut se trouver en dehors de la zone visible du panneau. Pour résoudre ce problème, appelez la méthode scrollPathToVisible() au lieu d'appeler la méthode makeVisible(). Cet appel ouvre tous les noeuds du chemin et demande au panneau déroulant de se positionner sur le noeud situé à la fin du chemin :

arbre.scrollPathToVisible(chemin);

Edition d'un noeud

Par défaut, les noeuds d'un arbre peuvent être modifiés. Cependant si vous validez la méthode setEditable(), l'utilisateur peut modifier un noeud en double-cliquant simplement dessus, en modifiant la chaîne, puis en appuyant sur la touche "Entrée" :

arbre.setEditable(true);

Le système invoque alors l'éditeur de cellule par défaut, qui est implémenté par la classe DefaultCellEditor. Il est possible d'installer d'autres éditeurs de cellules, mais je préfére reporter notre étude sur les éditeurs de cellules à la section concernant les tableaux, avec lesquels les éditeurs de cellules sont plus couramment utilisés.

Mise en oeuvre sur l'application précédente

A titre d'exemple, je vous propose de reprendre l'application précédente et de faire en sorte de pouvoir rajouter ou supprimer des noeuds dans l'arborescence des fichiers. Au départ, seul le noeud "JPG" existe dans le répertoire "Images". Il est possible d'intégrer un nouveau noeud seulement si c'est un fils du noeud "Images". Il existe deux possibilités pour cela, soit à partir du noeud "Image" lui-même, soit à partir d'un fils déjà créé, comme le noeud "JPG". Dans ce cas là, nous rajoutons un noeud frère, comme cela est visualisé dans la capture ci-dessous. Les fichiers se déplacent alors en conséquence suivant les extensions proposées. A tout moment, il est également possible de supprimer un noeud particulier de l'arborescence.

codage correspondant
package arbres;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.*;
import java.util.ArrayList;
import javax.imageio.ImageIO;
import javax.swing.event.*;
import javax.swing.tree.*;

public class Arbres extends JFrame implements TreeSelectionListener {
   private JTree arbre;
   private DefaultTreeModel modèle;
   private DefaultMutableTreeNode racine;
   private DefaultMutableTreeNode images;
   private DefaultMutableTreeNode autre;
   private Vue vue = new Vue();
   private String répertoire = "C:/Photos/";
   private JToolBar barre = new JToolBar();
   private JTextField saisie = new JTextField("Nouveau répertoire");
   
   public Arbres() {
      super("Images");
      construireArbre();
      barre.add(new AbstractAction("Ajouter Frère") {
         public void actionPerformed(ActionEvent e) {
            DefaultMutableTreeNode sélection = (DefaultMutableTreeNode) arbre.getLastSelectedPathComponent();
            if (images.isNodeChild(sélection)) {
               System.out.println("Noeud enfant");
               DefaultMutableTreeNode noeud = new DefaultMutableTreeNode(saisie.getText(), true);
               modèle.insertNodeInto(noeud, images, 0);
               DefaultMutableTreeNode recherche = autre.getFirstLeaf();
               while (recherche!=null) {
                  DefaultMutableTreeNode suivant = recherche.getNextLeaf();
                  String extension = recherche.toString().split("\\.")[1];
                  if (extension.equalsIgnoreCase(noeud.toString())) 
                     modèle.insertNodeInto(recherche, noeud, 0);
                  recherche = suivant;            
               }  
               modèle.reload(autre);          
            }                            
         }         
      });
      barre.add(new AbstractAction("Ajouter Fils") {
         public void actionPerformed(ActionEvent e) {
            DefaultMutableTreeNode sélection = (DefaultMutableTreeNode) arbre.getLastSelectedPathComponent();
            if (sélection.equals(images)) {
               DefaultMutableTreeNode noeud = new DefaultMutableTreeNode(saisie.getText(), true);
               modèle.insertNodeInto(noeud, images, 0);
               DefaultMutableTreeNode recherche = autre.getFirstLeaf();
               while (recherche!=null) {
                  DefaultMutableTreeNode suivant = recherche.getNextLeaf();
                  String extension = recherche.toString().split("\\.")[1];
                  if (extension.equalsIgnoreCase(noeud.toString())) 
                     modèle.insertNodeInto(recherche, noeud, 0);
                  recherche = suivant;            
               }  
               modèle.reload(autre);          
            }                
         }
      });
      barre.add(new AbstractAction("Supprimer") {
         public void actionPerformed(ActionEvent e) {
            DefaultMutableTreeNode sélection = (DefaultMutableTreeNode) arbre.getLastSelectedPathComponent();
            DefaultMutableTreeNode noeud = sélection.getFirstLeaf();
            while (noeud!=null) {
               DefaultMutableTreeNode suivant = noeud.getNextLeaf();
               modèle.insertNodeInto(noeud, autre, 0);
               noeud = suivant;            
            }
            modèle.removeNodeFromParent(sélection);            
         }
      });
      barre.add(saisie);
      add(barre, BorderLayout.NORTH);
      add(new JScrollPane(arbre), BorderLayout.WEST);
      add(vue);
      setSize(540, 330);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }
   
   public static void main(String[] args) { new Arbres(); }

   private void construireArbre() { 
      racine = new DefaultMutableTreeNode("Fichiers", true);     
      images = new DefaultMutableTreeNode("Images", true);
      DefaultMutableTreeNode jpeg = new DefaultMutableTreeNode("JPG", true);  
      autre = new DefaultMutableTreeNode("Autre", true);
      racine.add(images);      
      racine.add(autre);
      images.add(jpeg);
      modèle = new DefaultTreeModel(racine, true);
      arbre = new JTree(modèle);
      arbre.setPreferredSize(new Dimension(180, 1000));
      arbre.addTreeSelectionListener(this);
      arbre.setEditable(true);
      ajouterFichiers();
   }
   
   private void ajouterFichiers() {
      File fichiers = new File(répertoire);
      ArrayList<String> liste = new ArrayList<String>();
      for (String nom : fichiers.list()) liste.add(nom);      
      Enumeration recherche = images.children();
      while (recherche.hasMoreElements()) {
         DefaultMutableTreeNode noeud = (DefaultMutableTreeNode) recherche.nextElement();            
         for (String nom : liste) {
            String extension = nom.split("\\.")[1];
            if (extension.equalsIgnoreCase(noeud.toString())) 
               noeud.add(new DefaultMutableTreeNode(nom, false));
         }   
      }       
      
      for (DefaultMutableTreeNode noeud = images.getFirstLeaf(); noeud !=null; noeud = noeud.getNextLeaf()) 
         liste.remove(noeud.toString());
      for (String nom : liste) autre.add(new DefaultMutableTreeNode(nom, false));
   }

   private class Vue extends JComponent {
      private BufferedImage photo;
      private double ratio;

      @Override
      protected void paintComponent(Graphics g) {
         if (photo!=null)  g.drawImage(photo, 0, 0, getWidth(), (int)(getWidth()/ratio), null);
      }
      
      public void setPhoto(File fichier) {
         try {
            photo = ImageIO.read(fichier);
            ratio = (double)photo.getWidth() / photo.getHeight();
            repaint();           
         } 
         catch (IOException ex) { setTitle("Impossible de lire le fichier");}     
      }
   }

   public void valueChanged(TreeSelectionEvent e) {
      if (arbre.getSelectionPath()!=null) {
         String nom = arbre.getSelectionPath().getLastPathComponent().toString();
         vue.setPhoto(new File("C:/Photos/"+nom));
      }
   }
}

 

Choix du chapitre Affichage des noeuds personnalisés

Dans vos applications, vous serez souvent amené à modifier la manière dont un composant d'un arbre représente les noeuds. La modification la plus courante est naturellement la possibilité de choisir plusieurs icônes pour les noeuds et pour les feuilles. Les autres changements peuvent être en rapport avec la police utilisée ou l'affichage d'images sur chaque noeuds.

Toutes ces modifications sont possibles si vous prenez la peine d'installer un nouvel afficheur de cellules d'arbre dans votre arbre. Par défaut, la classe JTree se sert d'objets de type DefaultTreeCellRenderer pour afficher chaque noeud. La classe DefaultTreeCellRenderer étend la classe JLabel. Une étiquette contient l'icône d'un noeud et le nom de ce noeud.

L'afficheur de cellules n'affiche pas les poignées permettant de savoir si un noeud est ouvert ou fermé. Ces poignées font partie de l'aspect général de l'arbre, et il est recommandé de ne pas les changer.

Personnalisation

Vous pouvez personnaliser l'affichage de trois manières différentes :

  1. Vous pouvez modifier les icônes, la police et la couleur de fond utilisées par un objet DefaultTreeCellRenderer déjà présent dans l'arbre. Dans ce cas là, ces paramètres sont utilisés pour tous les noeuds d'un arbre.
  2. Vous pouvez installer un nouvel afficheur qui étend la classe DefaultTreeCellRenderer et modifier les icônes, les polices et la couleur de fond de chaque noeud.
  3. Vous pouvez installer un afficheur qui implémente l'interface TreeCellRenderer, pour afficher une nouvelle image pour chaque noeud.

Personnaliser les icônes des noeuds (même apparence pour tous les noeuds)

Si vous désirez modifier les icônes des répertoires (ouvert ou fermé) ainsi que celle des feuilles tout en gardant la même apparence sur l'ensemble de l'arbre, il suffit :

  1. soit de récupérer l'objet DefaultTreeCellRenderer de l'arbre à partir de la méthode getCellRenderer() de la classe JTree,
  2. soit de créer un nouvel objet DefaultTreeCellRenderer et de le proposer ensuite à l'arbre au travers de la méthode setCellRenderer().

Quelque soit la solution retenue, la classe DefaultTreeCellRenderer possède, en plus de celles récupérées par héritage issue de la classe JLabel, des méthodes spécifiques à la gestion d'affichage des noeuds :

Méthodes spécifiques à la classe DefaultTreeCellRenderer
Color getBackgroundNonSelectionColor()
void setBackgroundNonSelectionColor(Color nouvelleCouleur)
Retourne ou spécifie la couleur de fond du noeud lorsque ce dernier n'est pas sélectionné.
Color getBackgroundSelectionColor()
void setBackgroundSelectionColor(Color nouvelleCouleur)
Retourne ou spécifie la couleur de fond du noeud lorsque ce dernier est sélectionné.
Color getBorderSelectionColor()
void setBorderSelectionColor(Color nouvelleCouleur)
Retourne ou spécifie la couleur de bordure du noeud lorsque ce dernier est sélectionné.
Icon getClosedIcon()
void setClosedIcon(Icon nouvelleIcône)
Retourne ou spécifie l'icône qui représente un noeud fermé (répertoire fermé). Ce noeud n'est pas une feuille et possède des enfants.
Icon getDefaultClosedIcon()
Retourne l'icône par défaut représentant un noeud fermé. Ce noeud n'est pas une feuille.
Icon getDefaultLeafIcon()
Retourne l'icône par défaut représentant une feuille.
Icon getDefaultOpenIcon()
Retourne l'icône par défaut représenant un noeud ouvert (répertoire ouvert). Ce noeud n'est pas une feuille et possède des enfants.
Icon getLeafIcon()
void setLeafIcon(Icon nouvelleIcône)
Retourne ou spécifie l'icône qui représente une feuille.
Icon getOpenIcon()
void setOpenIcon(Icon nouvelleIcône)
Retourne ou spécifie l'icône qui représente un noeud ouvert (répertoire ouvert). Ce noeud n'est pas une feuille et possède des enfants.
Color getTextNonSelectionColor()
void setTextNonSelectionColor(Color nouvelleCouleur)
Retourne ou spécifie la couleur du libellé du noeud lorsque ce dernier n'est pas sélectionné.
Color getTextSelectionColor()
void setTextSelectionColor(Color nouvelleCouleur)
Retourne ou spécifie la couleur du libellé du noeud lorsque ce dernier est sélectionné.
Component getTreeCellRendererComponent(JTree arbre, Object valeur, boolean sélection, boolean ouvert, boolean feuille, int ligne, boolean focus)
Méthode à redéfinir lorsque nous souhaitons proposer un rendu personnalisé pour chaque noeud. Cette méthode est déclarée dans l'interface TreeCellRenderer.
Exemple de mise en oeuvre en récupérant l'objet DefaultTreeCellRenderer de l'arbre JTree


   private void construireArbre() { 
      racine = new DefaultMutableTreeNode("Fichiers", true);     
      images = new DefaultMutableTreeNode("Images", true);
      DefaultMutableTreeNode jpeg = new DefaultMutableTreeNode("JPG", true);  
      DefaultMutableTreeNode gif = new DefaultMutableTreeNode("GIF", true);
      DefaultMutableTreeNode png = new DefaultMutableTreeNode("PNG", true);
      autre = new DefaultMutableTreeNode("Autre", true);
      racine.add(images);      
      racine.add(autre);
      images.add(jpeg);
      images.add(gif);
      images.add(png);
      arbre = new JTree(racine, true);
      arbre.setPreferredSize(new Dimension(200, 1000));
      arbre.addTreeSelectionListener(this);
      arbre.setShowsRootHandles(true);
      arbre.setRootVisible(false);
      DefaultTreeCellRenderer rendu = (DefaultTreeCellRenderer) arbre.getCellRenderer();
      rendu.setLeafIcon(new ImageIcon("feuille.gif"));
      rendu.setClosedIcon(new ImageIcon("répertoireFermé.gif"));
      rendu.setOpenIcon(new ImageIcon("répertoireOuvert.gif"));
      ajouterFichiers();
   }
Autre exemple en créant un nouvel objet DefaultTreeCellRenderer qui est ensuite proposé à l'arbre JTree


   private void construireArbre() { 
      racine = new DefaultMutableTreeNode("Fichiers", true);     
      images = new DefaultMutableTreeNode("Images", true);
      DefaultMutableTreeNode jpeg = new DefaultMutableTreeNode("JPG", true);  
      DefaultMutableTreeNode gif = new DefaultMutableTreeNode("GIF", true);
      DefaultMutableTreeNode png = new DefaultMutableTreeNode("PNG", true);
      autre = new DefaultMutableTreeNode("Autre", true);
      racine.add(images);      
      racine.add(autre);
      images.add(jpeg);
      images.add(gif);
      images.add(png);
      arbre = new JTree(racine, true);
      arbre.setPreferredSize(new Dimension(200, 1000));
      arbre.addTreeSelectionListener(this);
      arbre.setShowsRootHandles(true);
      arbre.setRootVisible(false);
      DefaultTreeCellRenderer rendu = new DefaultTreeCellRenderer();
      rendu.setLeafIcon(new ImageIcon("feuille.gif"));
      rendu.setClosedIcon(new ImageIcon("répertoireFermé.gif"));
      rendu.setOpenIcon(new ImageIcon("répertoireOuvert.gif"));
      arbre.setCellRenderer(rendu);
      ajouterFichiers();
   }

Avec la première méthode, la première fois que l'arbre s'affiche, la dimension des icônes est celle prévue par le système par défaut. Nous avons donc un petit aléa qui est vite résorbé dès que nous ouvrons un répertoire quelconque. Toutefois, pour éviter cet aléa, il est préférable d'utiliser la deuxième méthode.

Il n'est généralement pas souhaitable de modifier la police ou la couleur de fond d'un arbre entier, parce que cette tâche revient plutôt au look-and-feel choisi.
§

Personnaliser l'apparence de chaque noeud

Pour modifier l'apparence de certains noeuds, vous devez installer un afficheur de cellules d'arbre. Cet afficheur de cellule doit impérativement implémenter l'interface TreeCellRenderer qui possède une seule méthode getTreeCellRendererComponent(). Nous avons déjà rencontré cette méthode dans la classe DefaultTreeCellRenderer, et c'est normal puisque cette dernière implémente justement cette interface.

Pour personnaliser l'apparence de chaque noeud en particulier, il suffit finalement de créer un nouvel afficheur de cellule qui hérite de la classe DefaultTreeCellRenderer et nous devons redéfinir la méthode getTreeCellRendererComponent() pour que cette dernière soit adaptée au rendu personnalisé.

Interface TreeCellRenderer
Component getTreeCellRendererComponent(JTree arbre, Object valeur, boolean sélection, boolean ouvert, boolean feuille, int ligne, boolean focus)
Méthode à redéfinir lorsque nous souhaitons proposer un rendu personnalisé pour chaque noeud.

Attention : le paramètre valeur de la méthode getTreeCellRendererComponent() est l'objet noeud, et non l'objet de l'utilisateur ! Rappelez-vous que l'objet de l'utilisateur est une caractéristique de DefaultMutableTreeNode, et qu'un JTree peut contenir des noeuds de n'importe quel type. Si votre arbre se sert de noeuds DefaultMutableTreeNode, vous devez traiter l'objet de l'utilisateur dans une seconde étape. Pour récupérer l'objet utilisateur, passez par la méthode getUserObject() de la classe DefaultMutableTreeNode.

Attention : DefaultTreeCellRenderer se sert d'un seul objet d'étiquette pour tous les noeuds, et il ne modifie le texte de l'étiquette que d'un seul noeud. Si, par exemple, vous souhaitez modifier la police d'un noeud particulier, vous devez lui redonner sa valeur par défaut lorsque la méthode est appelée à nouveau. Autrement, tous les noeuds suivant seront affichés avec la nouvelle police.

 

Afficher une petite vignette sur la feuille correspondant à la photo à visualiser

A titre d'exemple, je vous propose de reprendre l'application précédente et de personnaliser l'icône de chaque feuille. Il est effectivement plus judicieux de proposer une petite vignette correspondant à la photo à visualiser.

package arbres;

import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.*;
import javax.imageio.ImageIO;
import javax.swing.event.*;
import javax.swing.tree.*;

public class Arbres extends JFrame implements TreeSelectionListener {
   private JTree arbre;
   private DefaultMutableTreeNode racine;
   private DefaultMutableTreeNode images;
   private DefaultMutableTreeNode autre;
   private Vue vue = new Vue();
   private String répertoire = "C:/Photos/";

   public Arbres() {
      super("Images");
      construireArbre();
      add(new JScrollPane(arbre), BorderLayout.WEST);
      add(vue);
      setSize(540, 300);
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }
   
   public static void main(String[] args) { new Arbres(); }

   private void construireArbre() { 
      racine = new DefaultMutableTreeNode("Fichiers", true);     
      images = new DefaultMutableTreeNode("Images", true);
      DefaultMutableTreeNode jpeg = new DefaultMutableTreeNode("JPG", true);  
      DefaultMutableTreeNode gif = new DefaultMutableTreeNode("GIF", true);
      DefaultMutableTreeNode png = new DefaultMutableTreeNode("PNG", true);
      autre = new DefaultMutableTreeNode("Autre", true);
      racine.add(images);      
      racine.add(autre);
      images.add(jpeg);
      images.add(gif);
      images.add(png);
      arbre = new JTree(racine, true);
      arbre.setPreferredSize(new Dimension(200, 1000));
      arbre.addTreeSelectionListener(this);
      arbre.setShowsRootHandles(true);
      arbre.setRootVisible(false);
      arbre.setCellRenderer(new RenduArbre());
      ajouterFichiers();
   }
   
   private void ajouterFichiers() {
      File fichiers = new File(répertoire);      
      ArrayList<Vignette> vignettes = new ArrayList<Vignette>();
      for (File fichier : fichiers.listFiles()) vignettes.add(new Vignette(fichier));
      Enumeration recherche = images.children();
      while (recherche.hasMoreElements()) {
         DefaultMutableTreeNode noeud = (DefaultMutableTreeNode) recherche.nextElement();            
         for (Vignette vignette : vignettes)
           if (vignette.mêmeExtension(noeud)) 
               noeud.add(new DefaultMutableTreeNode(vignette, false));    
      }        
      for (DefaultMutableTreeNode noeud = images.getFirstLeaf(); noeud !=null; noeud = noeud.getNextLeaf()) 
         vignettes.remove(noeud.getUserObject());
      for (Vignette vignette : vignettes) autre.add(new DefaultMutableTreeNode(vignette, false));
   }
   
   public void valueChanged(TreeSelectionEvent e) {
      if (arbre.getSelectionPath()!=null) {
         DefaultMutableTreeNode noeud = (DefaultMutableTreeNode) arbre.getLastSelectedPathComponent();
         Vignette vignette = (Vignette) noeud.getUserObject();
         vue.setPhoto(vignette.getFichier());
      }
   }
   
   private class Vue extends JComponent {
      private Image photo;
      private double ratio;

      @Override
      protected void paintComponent(Graphics g) {
         if (photo!=null)  g.drawImage(photo, 0, 0, getWidth(), (int)(getWidth()/ratio), null);
      }
      
      public void setPhoto(File fichier) {
         photo = new ImageIcon(fichier.getPath()).getImage();
         ratio = (double)photo.getWidth(null) / photo.getHeight(null);
         repaint();               
      }
   }

   private class RenduArbre extends DefaultTreeCellRenderer {
      public RenduArbre() {
         setClosedIcon(new ImageIcon("répertoireFermé.gif"));
         setOpenIcon(new ImageIcon("répertoireOuvert.gif")); 
         setBorder(BorderFactory.createEmptyBorder(1, 0, 1, 0));
      }
      
      @Override
      public Component getTreeCellRendererComponent(JTree arbre, Object n, boolean sélection, 
                                                                                         boolean ouvert, boolean feuille, int ligne, boolean focus) {
         super.getTreeCellRendererComponent(arbre, n, sélection, ouvert, feuille, ligne, focus);
         DefaultMutableTreeNode noeud = (DefaultMutableTreeNode) n;
         if (feuille) {
            Vignette vignette = (Vignette) noeud.getUserObject();
            setIcon(vignette.getIcon());           
         }
         return this;
      }     
   }
   
   private class Vignette extends JLabel {
      private final int largeur = 50;
      private File fichier;
      private String libellé;
      private String extension;
      
      public Vignette(File fichier) {
         this.fichier = fichier;
         String[] découpage = fichier.getName().split("\\.");
         libellé = découpage[0];
         extension = découpage[1];
         Image image = new ImageIcon(fichier.getPath()).getImage().getScaledInstance(largeur, -1, Image.SCALE_DEFAULT);
         setIcon(new ImageIcon(image));            
      }

      boolean mêmeExtension(DefaultMutableTreeNode noeud) {
         return extension.equalsIgnoreCase(noeud.toString());
      }

      public File getFichier() { return fichier; }
      
      @Override
      public String toString() { return libellé; }      
   }  
}

 

Choix du chapitre Ecouter les événements des arbres

Le plus fréquemment, et c'est justement le cas dans notre application, un composant arbre est couplé à un autre composant. Lorsque l'utilisateur sélectionne des noeuds de l'arbre, certaines informations s'affichent dans une autre fenêtre. Dans notre application, lorsque l'utilisateur sélectionne une feuille, la photo correspondante s'affiche sur la partie centrale de la fenêtre.

Une arborescence déclenche plusieurs sortent d'événements qui sont gérés par des interfaces écouteurs d'événement distincts. Nous pouvons déterminer :
  1. Le moment où les noeuds ont été ouverts et refermés, implémenté par l'interface TreeExpansionListener.
  2. Celui où ils sont sur le point d'être ouverts ou refermé (suite à un clic de l'utilisateur), implémenté par l'interface TreeWillExpandListener.
  3. Et celui où les sélections ont cours, implémenté par l'interface TreeSelectionListener.
écouteur TreeExpansionListener
void treeCollapsed(TreeExpansionEvent événement)
Appelé lorsque un noeud se referme.
void treeExpanded(TreeExpansionEvent événement)
Appelé lorsque noeud s'ouvre.
écouteur TreeWillExpandListener
void treeWillCollapse(TreeExpansionEvent événement)
Appelé lorsque un noeud est sur le point de se refermer.
void treeWillExpande(TreeExpansionEvent événement)
Appelé lorsque noeud est sur le point de s'ouvrir.
écouteur TreeSelectionListener
void valueChanged(TreeSelectionEvent événement)
Appelé lorsque l'utilisateur sélection ou désélectionne des noeuds de l'arbre.

Réagir à la sélection d'un noeud

Bien entendu, comme pour notre application, le cas le plus fréquent est la prise en compte d'une sélection, qui permet de réagir en conséquence en lançant la visualisation de l'image souhaitée. Pour obtenir ce comportement, il suffit donc d'installer un écouteur de sélection de l'arbre. Cet écouteur doit implémenter l'interface TreeSelectionListener, une interface possédant une seule méthode :

void valueChanged(TreeSelectionEvent événement);

Cette méthode est appelée lorsque l'utilisateur sélectionne ou désélectionne des noeuds de l'arbre. L'écouteur est ajouté à l'arbre de manière classique :

arbre.addTreeSelectionListener(écouteur);

Choisir le mode de sélection

Vous pouvez autoriser l'utilisateur à sélectionner un seul noeud, une zone continue de noeuds ou un ensemble arbitraire et potentiellement discontinu de noeuds. La classe JTree se sert de l'interface TreeSelectionModel pour gérer la sélection des noeuds. Vous devrez retrouver le modèle, au moyen de la méthode getSelectionModel() de la classe JTree, pour définir ensuite l'état de sélection, à l'aide de la méthode setSelectionMode() de l'interface TreeSelectionModel, avec l'une des valeurs suivantes :

  1. TreeSelectionModel.SINGLE_TREE_SELECTION,
  2. TreeSelectionModel.CONTIGUOUS_TREE_SELECTION,
  3. TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION.
Le mode de sélection discontinu est le mode par défaut. Pour permettre uniquement la sélection d'un seul noeud :

int mode = TreeSelectionModel.SINGLE_TREE_SELECTION;
arbre.getSelectionModel().setSelectionMode(écouteur);

Lorsque vous aurez défini un mode de sélection, vous n'aurez plus à vous préocuper du modèle de sélection de l'arbre.

Récupération de la sélection courante

Pour récupérer la sélection courante, il faut interroger l'arbre avec la méthode getSelectionPaths() :

TreePath[] sélections = arbre.getSelectionPaths();
Si vous limitez l'utilisateur à une seule sélection, vous pouvez avoir recours à la méthode pratique getSelectionPath(), qui renvoie le premier chemin sélectionné, ou null si aucun chemin n'a été sélectionné. Vous obtiendrez en retour un objet TreePath, d'où vous en déduirez le noeud sélectionné. Effectivement, lorsque vous possédez un chemin d'arbre, il vous suffit en général de connaître le noeud final, que vous pouvez récupérer grâce à la méthode getLastPathComponent()
TreePath chemin = arbre.getSelectionPath();
DefaultMutableTreeNode noeud = (DefaultMutableTreeNode) chemin.getLastPathComponent();
En fait, comme cette requête est très fréquente, il existe une méthode pratique qui vous fournit immédiatement le noeud sélectionné :
DefaultMutableTreeNode noeud = (DefaultMutableTreeNode) arbre.getLastSelectedPathComponent();

Cette méthode n'est pas appelée getSelectedNode() parce que l'arbre ne sait pas qu'il renferme des noeuds. Seul le modèle d'arbre gère les chemins des objets.
§

Attention : la classe TreeSelectionEvent possède une méthode getPaths() qui renvoie un tableau d'objets TreePath, mais ce tableau décrit les modifications de la sélection, et non la sélection courante.

 

Mise en oeuvre de la gestion de sélection unique d'une feuille de l'arbre
...
public class Arbres extends JFrame implements TreeSelectionListener {
...
   private void construireArbre() { 
...
      arbre = new JTree(racine, true);
      arbre.setPreferredSize(new Dimension(200, 1000));
      arbre.addTreeSelectionListener(this);
      arbre.setShowsRootHandles(true);
      arbre.setRootVisible(false);
      arbre.setCellRenderer(new RenduArbre());
      arbre.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
      ajouterFichiers();
   }
... 
   public void valueChanged(TreeSelectionEvent e) {
      if (arbre.getSelectionPath()!=null) {
         DefaultMutableTreeNode noeud = (DefaultMutableTreeNode) arbre.getLastSelectedPathComponent();
         Vignette vignette = (Vignette) noeud.getUserObject();
         vue.setPhoto(vignette.getFichier());
      }
   }
...
}

 

Choix du chapitre Les tableaux

Les tableaux présentent les informations sous forme de lignes et de colonnes ordonnées. Ils conviennent particulièrement à la représentation de schémas financiers ou de données de base de données relationnelle. Comme les arborescences, les tableaux de Swing sont extrêmement puissants et personnalisables. Si on se cantonne à leurs options par défaut, ils s'avèrent eu outre très simples à utiliser.

Les tableaux sont représentés par le composant JTable qui affiche une grille bidirectionnelle d'objets. Naturellement, les tableaux sont très courants dans les interfaces utilisateurs. De par leur nature, les tableaux sont compliqués, mais, peut-être plus que pour d'autres classes de Swing, le composant JTable prend en charge la plus grosse partie de cette complexité. Vous pourrez ainsi produire des tableaux parfaitement fonstionnels avec un comportement riche, en écrivant uniquement quelques lignes de code. Mais vous pouvez bien sûr écrire un code plus complet et personnaliser l'affichage et le comportement de vos applications.

Gestion de la table par modèles

Le composant JTable comporte un certain nombre de modèles internes qui permettent de respecter l'architecture MVC :

  1. Comme pour les composants d'un arbre, un JTable n'enregistrement pas ses propres données, mais il les obtient à partir d'un modèle de tableau issu de l'interface TableModel. Tous les élements important de cette classe JTable sont construits dans le paquetage javax.swing.table comme l'est cette interface TableModel. Vous pouvez construire un tableau à partir d'un tableau bidimensionnel d'objet et emballer automatiquement ce tableau dans un modèle par défaut (classe DefaultTableModel) en faisant juste appel au constructeur de JTable.
  2. De même, il est possible de prévoir un modèle spécifique au nom donné par chacune des colonnes du tableau, qui se présente généralement sous forme grisé et en haut du tableau. Pour cela, vous devez implémenter l'interface TableColumnModel. Toutefois, là aussi, il existe une classe DefaultTableColumnModel qui implémente cette interface et qui est directement intégré, sauf avis contraire, dans le composant JTable.
  3. Enfin, il existe un modèle prévu pous les sélections des éléments, issu de l'interface ListSelectionModel, dont la classe JTable possède également un modèle par défaut : DefaultListSelectionModel.

Phase de construction

Plusiseurs constructeurs sont aménagés pour résoudre les différentes possibilités de création de tableaux :

  1. JTable() : Construit un tableau vierge avec le modèle de table par défaut, le modèle de colonne par défaut et le modèle de sélection par défaut.
  2. JTable(int nombreLignes, int nombreColonnes) : Construit un tableau en spécifiant le nombre de lignes et de colonnes avec des cellules vierges avec les modèles par défaut.
  3. JTable(Object[][] cellules, Object[] nomColonnes) : Construit un tableau avec la valeur de chacune des cellules d'une part, et le nom donné à chacune des colonnes d'autre part, en prenant les modèles par défaut.
  4. JTable(TableModel modèleTable) : Construit un tableau en proposant un nouveau modèle de table, mais en gardant le modèle de colonne par déafut et le modèle de sélection par défaut.
  5. JTable(TableModel modèleTable, TableColumnModel modèleColonne) : Construit un tableau en proposant un nouveau modèle de table et un nouveau modèle de colonne tout en gardant le modèle de sélection par défaut.
  6. JTable(TableModel modèleTable, TableColumnModel modèleColonne, ListSelectionModel modèleSélection) : Construit un tableau en spécifiant les trois modèles.
  7. JTable(Vector cellules, Vector nomsColonne) : Construit un tableau à partir d'un verteur de vecteur pour le contenu des cellules et à partir d'un vecteur pour spécifier le nom des colonnes.

Mise en oeuvre d'un tableau simple

La figure ci-dessous montre un tableau typique, qui décrit les propriétés des planètes du système solaire. Une planète est considérée comme gazeuse si elle est composée principalement d'hydrogène et d'hélium.

codage correspondant

package tables;

import javax.swing.*;
import java.awt.*;

public class Tables extends JFrame {
   private Object[][] cellules = {
      {"Mercure", 2440.0, 0, false, Color.yellow},
      {"Vénus", 6052.0, 0, false, Color.yellow},
      {"Terre", 6378.0, 1, false, Color.blue},
      {"Mars", 3397.0, 2, false, Color.red},
      {"Jupiter", 71492.0, 16, true, Color.orange},
      {"Saturne", 60268.0, 18, true, Color.orange},
      {"Uranus", 25559.0, 17, true, Color.blue},
      {"Neptune", 24766.0, 8, true, Color.blue},
      {"Pluton", 1137.0, 1, false, Color.black}
   };
   private String[] nomColonnes = {"Planète", "Rayon", "Satellites", "Gazeuse", "Couleur"};

   public Tables() {
      super("Planètes");
      add(new JScrollPane(new JTable(cellules, nomColonnes)));
      pack();
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }
   
   public static void main(String[] args) { new Tables(); }
}
  1. Comme vous pouvez le voir dans le code, les données de ce tableau sont enregistrées dans un tableau bidimensionnel de valeurs Object.
  2. Nous profitons ici de l'autoboxing. Effectivement, les deuxièmes troisièmes et quatrièmes colonnes sont automatiquement converties en objet de type Double, Integer et Boolean.
  3. Sinon, le tableau se contente d'invoquer la méthode toString() de chaque objet pour l'afficher. C'est ce qui se passe pour l'affichage de la classe Color.
  4. Le nom des colonnes sont fournies dans des chaînes séparées : String[] nomColonnes = {"Planète", "Rayon", "Satellites", "Gazeuse", "Couleur"};

Comportement minimal d'un tableau

Le tableau résultant possède déjà un comportement étonnament riche. Pour n'avoir saisie que très peu de code, nous obtenons énormément de fonctionnalités intéressantes prêtes à l'emploi.

  1. En-têtes des colonnes : Le composant JTable place automatiquement des en-têtes de colonnes sous une forme différente des cellules. Il apparaît clairement qu'ils ne font pas partie de la zone de données de la table.

  2. Dépassement des cellules : Lorsque la donnée d'une cellule est trop longue, elle est automatiquement tronquée et présentée an pointillé (...). C'est le cas des cellules de couleur de la colonne de droite :


  3. Le nom des colonnes toujours visible : Recadrez le tableau verticalement jusqu'à ce que l'ascenceur vertical apparaisse et déplacez cet ascenceur. Les noms des colonnes restent constamment visibles.


  4. Sélection de ligne : Nous pouvons cliquer sur une cellule quelconque pour sélectionner la ligne entière. Cette fonction est contrôlable : nous pouvons choisir des cellules individuelles, des lignes ou des colonnes entières ou une combinaison des deux. Pour configurer la fonction sélection du composant JTable, nous devons faire appel aux méthodes setCellSelectionEnabled(), setColumnSelectionAllowed() et setRowSelectionAllowed().


  5. Edition de cellule : En double-cliquant sur une cellule, nous ouvrons une vue de modification ; elle propose alors un petit curseur de texte. Nous pouvons saisir ainsi directement dans la cellule le nouvelle valeur souhaitée.

  6. Dimensionnement de colonne : Lorsque nous déplaçons le curseur de la souris entre deux colonnes, nous obtenons un petit curseur en forme de flèches opposées. Cliquez et glissez pour modifier la largeur de la colonne. Suivant la configuration de JTable, la largeur des autres colonnes est également succeptible de changer. La fonction de redimensionnement est contrôlée par la méthode setAutoResizedMode().


  7. Réordonnancement des colonnes : En cliquant et glissant sur un en-tête de colonne, nous pouvons déplacer la colonne entière à un autre endroit de la table. Effectivement, cliquez sur l'un des noms de colonnes et déplacez-le à droite ou à gauche. La colonne entière se détache. Vous pouvez donc l'amener à un nouvel emplacement. Cela modifie uniquement l'affichage des colonnes. Le modèle des données n'est pas affecté.

  8. Imprimer un tableau : Depuis la version 5 de la JDK, vous envoyer tout le contenu d'un tableau directement à l'imprimante au moyen de la méthode print() de la classe JTable. Une boîte de dialogue d'impression s'affiche afin que vous choisissiez la bonne imprimante.
Méthodes intéressantes de la classe JTable
void addColumn(TableColumn colonne)
void addColumnSelectionInterval(int de, int à)
void addRowSelectionInterval(int de, int à)
Ajout de lignes et de colonnes.
void clearSelection()
Désactive la sélection.
boolean editCellAt(int ligne, int colonne)
boolean editCellAt(int ligne, int colonne, EventObject événement)
Gestion de l'édition par programme.
int getColumnCount()
String getColumnName(int colonne)
boolean getColumnSelectionAllowed()
int getEditingColumn()
int getSelectedColumn()
int getSelectedColumnCount()
int[] getSelectedColumns()
void setColumnSelectionInterval(int de, int à)
void setEditingColumn(int colonne)
Gestion des colonnes.
int getRowCount()
int getRowHeight()
int getRowHeight(int ligne)
int getRowMargin()
boolean getRowSelectionAllowed()
int getEditingRow()
int getSelectedRow()
int getSelectedRowCount()
int[] getSelectedRows()
void setEditingRow(int ligne)
void setRowSelectionAllowed(boolean validation)
void setRowSelectionInterval(int de, int à)
Gestion des lignes.
TableModel getModel()
TableColumnModel getColumnModel()
ListSelectionModel getSelectionModel()
void setModel(TableModel modèle)
void setColumnModel(TableColumnModel modèle)
void setSelectionModel(ListSelectionModel modèle)
Gestion des trois modèles par défaut de la table.
Object getValueAt(int ligne, int colonne)
void setValueAt(Object valeur, int ligne, int colonne)
Gestion des valeurs de cellules.
boolean isCellEditable(int ligne, int colonne)
boolean isCellSelected(int ligne, int colonne)
boolean isColumnSelected(int colonne)
boolean isEditing()
boolean isRowSelected(int ligne)
Quelques tests.
void moveColumn(int colonne, int colonneCible)
Déplace la colonne vers la nouvelle position.
boolean print()
boolean print(JTable.PrintMode mode)
boolean print(JTable.PrintMode mode, MessageFormat en-tête, MessageFormat pied)
boolean print(JTable.PrintMode mode, MessageFormat en-tête, MessageFormat pied, boolean boîteImpression, PrintRequestAttributeSet attr, boolean interactive)
boolean print(JTable.PrintMode mode, MessageFormat en-tête, MessageFormat pied, boolean boîteImpression, PrintRequestAttributeSet attr, boolean interactive, PrintService service)
Imprimer.
void removeColumn(TableColumn colonne)
void removeColumnSelectionInterval(int de, int à)
void removeEditor()
void removeRowSelectionInterval(int de, int à)
Suppressions.
void selectAll()
Sélectionner toutes les lignes.
void setAutoResizeMode(int mode)
Choisi le mode d'affichage automatique. Il existe 5 modes de réaffichage automatique :
AUTO_RESIZE_OFF,
AUTO_RESIZE_NEXT_COLUMN,
AUTO_RESIZE_SUBSEQUENT_COLUMNS,
AUTO_RESIZE_LAST_COLUMN,
AUTO_RESIZE_ALL_COLUMNS
.
void setCellEditor(TableCellEditor éditeur)
void setDefaultEditor(Class<?> classColonne, TableCellEditor éditeur)
void setDefaultRenderer(Class<?> classColonne, TableCellRenderer rendu)
Proposer une gestion personnalisée de la table, respectivement l'éditeur et le rendu de la cellule.
void setCellSelectionEnabled(boolean validation)
void setColumnSelectionAllowed(boolean validation)
void setColumnSelectionAllowed(boolean validation)
void setShowGrid(boolean grille)
void setShowHorizontalLines(boolean horizontal)
void setShowVerticalLines(boolean vertical)
Validations.
void setGridColor(Color couleur)
void setSelectionBackground(Color fond)
void setSelectionForeground(Color texte)
Gestion des couleurs.
void setIntercellSpacing(Dimension intervalle)
void setPreferredScrollableViewportSize(Dimension dimension)
void setRowHeight(int hauteur)
void setRowHeight(int ligne, int hauteur)
void setRowMargin(int marge)
Réglages des différentes dimensions.
void sorterChanged(RowSorterEvent événement)
void tableChanged(TableModelEvent événement)
void valueChanged(ListSelectionEvent événement)
void editingCanceled(ChangeEvent événement)
void editingStopped(ChangeEvent événement)
Notifications.

Utiliser le modèle de table par défaut

Il est possible de travailler avec le modèle par défaut du tableau (DefaultTableModel) afin d'introduire le contenu de chacune des cellules ultérieurement (ainsi que le nom des colonnes), c'est-à-dire après la création de la table. Pour utiliser toutes les compétences de ce modèle, faites appel à la méthode getModel() du composant JTable.

Méthodes spécifiques à la classe DefaultTableModel
DefaultTableModel()
DefaultTableModel(int nombreLignes, int nombreColonnes).
DefaultTableModel(Object[][] données, Object[] nomColonnes)
DefaultTableModel(Object[] nomColonnes, int nombreLignes)
DefaultTableModel(Vector nomColonnes, int nombreLignes)
DefaultTableModel(Vector données, Vector nomColonnes)
Ensemble de constructeurs relativement similaires à la classe JTable.
void addColumn(Object nomColonnes)
void addColumn(Object nomColonnes, Object[] valeursDeLaColonne)
void addColumn(Object nomColonnes, Vector valeursDeLaColonne)
Ajoute une nouvelle colonne au modèle avec éventuellement des valeurs.
void addRow(Object[] valeursDeLaNouvelleLigne)
void addRow(Vector valeursDeLaNouvelleLigne)
Ajoute une nouvelle ligne avec l'ensemble des valeurs proposées en argument.
int getRowCount()
int
getColumnCount()
Retourne le nombre de lignes et de colonnes.
String getColumnName(int numéroColonne)
Retourne le nom de la colonne suivant la position spécifiée en argument.
Vector getDataVector()
Retourne le vecteur de vecteur de l'ensemble des valeurs pour chacune des cellules.
Object getValueAt(int ligne, int colonne) .
Retourne la valeur de la cellule se situant aux coordonnées du tableau spécifiées en argument de la méthode.
void insertRow(int ligne, Object[] donnéesDeLaLigne)
void insertRow(int ligne, Vector donnéesDeLaLigne)
Insère une nouvelle ligne dans le modèle avec l'ensemble des valeurs spécifiées.
boolean isCellEditable(int ligne, int colonne)
Contrôle si la cellule spécifiée est éditable ou pas.
void moveRow(int début, int fin, int nouvellePosition)
Un ensemble de lignes peuvent être déplacées dans le modèle.
void removeRow(int ligne)
Supprime la ligne du modèle
void setColumnCount(int nombreColonnes)
Prévoit un certaine nombre de colonnes au modèle.
void setColumnIdentifiers(Object[] nouveauxNomsDeColonne)
void setColumnIdentifiers(Vector nouveauxNomsDeColonne)
Spécifie le nom de chaque colonne.
void setDataVector(Object[][] données, Object[] nomColonnes)
void setDataVector(Vector données, Vector nomColonnes)
Spécifie la valeur de chacune des cellules ainsi que le nom de chaque colonne.
void setRowCount(int nombreLignes)
Prévoit un certain nombre de lignes.
void setValueAt(Object uneValeur, int ligne, int colonne)
Propose une nouvelle valeur à une cellule.

A titre d'exemple, je vous propose de réaliser un tableau dynamique qui recense l'ensemble des fichiers stockés dans le répertoire "C:\Photos\". Dans ce tableau apparaît repectivement, le nom du fichier, son extension, son nombre d'octets, savoir s'il s'agit d'une image avec les dimensions de cette image.

codage correspondant
package tables;

import java.awt.Image;
import java.io.File;
import javax.swing.*;
import javax.swing.table.*;

public class Tables extends JFrame {
   private String[] colonnes = {"Nom fichier", "Extensions", "Octets", "Image ?", "Largeur", "Hauteur"};
   private JTable table = new JTable();

   public Tables() {
      super("Liste des fichiers");
      construireTableau();
      add(new JScrollPane(table));
      pack();
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }
   
   private void construireTableau() {
      File[] fichiers = new File("C:/Photos/").listFiles();
      DefaultTableModel modèle = (DefaultTableModel) table.getModel();
      modèle.setColumnIdentifiers(colonnes);
      for (File fichier : fichiers) {
         String[] découpage = fichier.getName().split("\\.");
         String libellé = découpage[0];
         String extension = découpage[1].toUpperCase();
         long taille = fichier.length();
         Image image = new ImageIcon(fichier.getPath()).getImage();  
         boolean isImage = image.getWidth(null) != -1;
         modèle.addRow(new Object[]{libellé, extension, taille, isImage, image.getWidth(null), image.getHeight(null)});
      }
   }
   
   public static void main(String[] args) { new Tables(); }
}

 

Choix du chapitre Modèles de tableaux

JTable est un composant très puissant. Il propose gracieusement de nombreuses fonctions. Cependant, la configuration par défaut n'est généralement pas conseillé. La stratégie suivie ne correspond pas vraiment à ce que nous désirons faire dans ces exemples. En particulier, nous souhaitons des entrées en lecture seule : elle ne doivent pas être modifiables. De même, nous aimerions que les entrées de la colonne "Image ?" soient des cases à cocher et non des termes anglais. Enfin, l'idéal serait de pouvoir visualiser les fichiers qui sont des images.

La classe abstraite AbstractTableModel

Pour obtenir une plus grande souplesse de JTable, il vaut mieux implémenter votre propre modèle de tableau au lieu de placer toutes vos données dans un tableau bidimensionnel pour les afficher sous forme de tableau. Il suffit donc de personnaliser vos données en écrivant votre modèle de table en implémentant l'interface TableModel. Par chance, Swing nous facilite la tâche en proposant la classe abstraite AbstractTableModel faisant le gros du travail. Dans notre modèle personnalisé, il suffit ainsi de créer une classe qui hérite de cette classe AbstractTableModel et de redéfinir les trois méthodes abstraites suivantes :

  1. int getRowCount() : cette méthode renvoie le nombre de ligne de la table.
  2. int getColumnCount() : cette méthode renvoie le nombre de colonne de la table.
  3. Object getValueAt(int ligne, int colonne) : cette méthode renvoie la valeur de la cellule désignée.

Il existe plusieurs manières d'implémenter la méthode getValueAt(). Vous pouvez simplement calculer la réponse, ou chercher la valeur dans une base de données, ou encore dans une autre source de données.

Lorsque JTable a besoin de valeurs de données, il appelle la méthode getValueAt() du modèle de la table. Pour connaître la taille globale de la table, il appelle les méthodes getRowCount() et getColumnCount() de ce modèle de table.

Nom des colonnes

Si vous ne définissez pas de noms de colonnes, la méthode getColumnName() du modèle AbstractTableModel choisi comme nom A, B, C, etc. Pour modifier le nom des colonnes, il suffit de surcharger la méthode getColumnName().

Gestion de l'affichage suivant le type de colonne

Par défaut, AbstractTableModel rend toutes les cellules non modifiables, ce qui correspond à ce que nous voulions. Aucune modification n'est nécessaire à ce sujet. Pour proposer une visualisation personnalisée suivant le type à représenter, le tableau doit posséder plus d'informations sur les types des colonnes. Pour cela, vous devez nous redéfinir la méthode getColumnClass() de votre modèle de tableau personnalisé (issu de la classe AbstractTableModel), qui renvoie la classe qui décrit le type de la colonne. La classe JTable choisira alors un afficheur approprié pour cette classe :

  1. Icon : génère automatiquement une image.
  2. Boolean : génère automatiquement des cellules de type case à cocher.
  3. Numériques : génère automatiquement une valeur numérique de type entier, de type réel, etc.
  4. Object : génère une chaîne de caractères en faisant appel à la méthode toString().

Pour les autres types, vous pouvez fournir vos propres afficheurs de cellule. Les afficheurs de cellules de tableau sont comparables aux afficheurs de cellules d'arbre. Ce sujet sera traité dans le chapitre suivant.

Exemple d'application de modèle personnalisé

A titre d'exemple, je vous propose de reprendre l'application précédente et de créer un nouveau modèle personnalisé qui permettra en autre de prévoir des cases à cocher et d'afficher des vignettes pour les fichiers image.

codage correspondant
package tables;

import java.awt.Image;
import java.io.File;
import java.text.DecimalFormat;
import javax.swing.*;
import javax.swing.table.*;

public class Tables extends JFrame {
   private ModèleTableau modèle = new ModèleTableau();
   private JTable tableau = new JTable(modèle);

   public Tables() {
      super("Liste des fichiers");
      tableau.setRowHeight(70);
      add(new JScrollPane(tableau));
      pack();
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }
   
   public static void main(String[] args) { new Tables(); }
   
   private class ModèleTableau extends AbstractTableModel {
      private String[] colonnes = {"Nom fichier", "Extension", "Poids", "Image ?", "Dimension", "Vue"};
      private File[] fichiers = new File("C:/Photos/").listFiles();
      private Object[] lignes = new Object[fichiers.length];

      public ModèleTableau() {       
         for (int i=0; i<fichiers.length; i++) {
            File fichier = fichiers[i];
            String[] découpage = fichier.getName().split("\\.");
            String libellé = découpage[0];
            String extension = découpage[1].toUpperCase();
            long taille = fichier.length();
            Image image = new ImageIcon(fichier.getPath()).getImage();  
            Icon icône = new ImageIcon(image.getScaledInstance(-1, 70, Image.SCALE_DEFAULT));
            boolean isImage = image.getWidth(null) != -1;
            String dimension = isImage ? image.getWidth(null)+" x "+image.getHeight(null) : "Aucune";
            lignes[i] = new Object[]{libellé, extension, taille, isImage, dimension, icône};
         }
      }

      public int getRowCount() {
         return fichiers.length;
      }

      public int getColumnCount() {
         return colonnes.length;
      }

      public Object getValueAt(int ligne, int colonne) {
         Object[] fichier = (Object[]) lignes[ligne];
         return fichier[colonne];
      }

      @Override
      public Class<?> getColumnClass(int colonne) {
         Object[] première = (Object[]) lignes[0];
         return première[colonne].getClass();
      }

      @Override
      public String getColumnName(int colonne) {
         return colonnes[colonne];
      }
   }
}

 

Choix du chapitre Rendu de cellule personnalisé

La classe JTable est déjà capable de maîtriser un certain nombre d'affichages prédéfinis, comme les cases à cocher et les icônes. Toutefois, il peut être intéressant d'aller plus loin dans cette démarche et de prévoir, par exemple, un formatage particulier pour les valeurs numériques.

Pour les types autre que les classes Icon et Boolean, vous devez fournir votre propre afficheur de cellules. Les afficheurs de cellules de tableau sont comparables aux afficheurs de cellules d'arbre, que nous avons déjà abordés. Ils doivent implémenter l'interface TableCellRenderer, qui comprend une seule méthode getTableCellRendererComponent() que vous devez donc redéfinir pour adapter l'affichage à votre convenance.

Interface TableCellRenderer
Component getTableCellRendererComponent(JTable table, Object valeur, boolean sélectionné, boolean focus, int ligne, int colonne)
Méthode à redéfinir lorsque nous souhaitons proposer un rendu personnalisé pour chaque noeud.

Cette méthode est appelée lorsque la table doit afficher une cellule. Vous devez renvoyer un composant dont la méthode paint() est invoqué pour dessiner la cellule.

Une fois que vous avez créer notre afficheur de cellule en créant une classe qui implémente l'interface TableCellRenderer, vous devrez demander au tableau d'utiliser cet afficheur pour tous les objets qui correspond à la classe que vous souhaitez prendre en compte. La méthode setDefaultRenderer() de la classe JTable permet d'effectuer cette association. Il suffit de fournir un objet Class et l'afficheur :

table.setDefaultRenderer(Integer.class, afficheur);

Cet afficheur est maitnenant utilisé pour tous les objets du type spécifié.
§

Mise en oeuvre

Je vous propose de réorganiser l'application précédente afin que le poids de chaque fichier soit exprimé en octets avec la prise en compte de la séparation des milliers afin que l'affichage soit plus agréable. Par ailleurs, les fichiers qui ne sont pas desimages sont pas très visibles dans le tableau, je propose donc de renforcer la case à cocher afin d'établir un code de couleur suivant la qualité du fichier.

codage correspondant
package tables;

import java.awt.*;
import java.io.File;
import java.text.DecimalFormat;
import javax.swing.*;
import javax.swing.table.*;

public class Tables extends JFrame {
   private ModèleTableau modèle = new ModèleTableau();
   private JTable tableau = new JTable(modèle);

   public Tables() {
      super("Liste des fichiers");
      tableau.setRowHeight(70);
      tableau.setDefaultRenderer(Long.class, new RenduEntier());
      tableau.setDefaultRenderer(Boolean.class, new RenduCaseACocher());
      add(new JScrollPane(tableau));
      pack();
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }
   
   public static void main(String[] args) { new Tables(); }
   
   private class ModèleTableau extends AbstractTableModel {
      private String[] colonnes = {"Nom fichier", "Extension", "Poids", "Image ?", "Dimension", "Vue"};
      private File[] fichiers = new File("C:/Photos/").listFiles();
      private Object[] lignes = new Object[fichiers.length];

      public ModèleTableau() {       
         for (int i=0; i<fichiers.length; i++) {
            File fichier = fichiers[i];
            String[] découpage = fichier.getName().split("\\.");
            String libellé = découpage[0];
            String extension = découpage[1].toUpperCase();
            long taille = fichier.length();
            Image image = new ImageIcon(fichier.getPath()).getImage();  
            Icon icône = new ImageIcon(image.getScaledInstance(-1, 70, Image.SCALE_DEFAULT));
            boolean isImage = image.getWidth(null) != -1;
            String dimension = isImage ? image.getWidth(null)+" x "+image.getHeight(null) : "Aucune";
            lignes[i] = new Object[]{libellé, extension, taille, isImage, dimension, icône};
         }
      }

      public int getRowCount() {
         return fichiers.length;
      }

      public int getColumnCount() {
         return colonnes.length;
      }

      public Object getValueAt(int ligne, int colonne) {
         Object[] fichier = (Object[]) lignes[ligne];
         return fichier[colonne];
      }

      @Override
      public Class<?> getColumnClass(int colonne) {
         Object[] première = (Object[]) lignes[0];
         return première[colonne].getClass();
      }

      @Override
      public String getColumnName(int colonne) {
         return colonnes[colonne];
      }
   }
   
   private class RenduEntier extends JFormattedTextField implements TableCellRenderer {
      public RenduEntier() {
         super(new DecimalFormat("#,##0 octets"));
         setHorizontalAlignment(RIGHT);
      }
      
      public Component getTableCellRendererComponent(JTable table, Object valeur, boolean sélectionné, boolean focus, int ligne, int colonne) {
         setValue(valeur);
         return this;
      }     
   }
   
   private class RenduCaseACocher extends JCheckBox implements TableCellRenderer {
      public Component getTableCellRendererComponent(JTable table, Object valeur, boolean sélectionné, boolean focus, int ligne, int colonne) {
         setSelected((Boolean)valeur);
         setBackground(isSelected() ? Color.GREEN : Color.RED);
         return this;
      }      
   }
}

 

Choix du chapitre Modifier une cellule de tableau

Pour qu'une cellule soit modifiable, le modèle de tableau doit indiquer quelles cellules sont modifiables en définissant la méthode isCellEditable(). La plupart du temps, vous choisirez de rendre certaines colonnes modifiables.

L'AbstractTableModel définit la méthode isCellEditable() de sorte qu'elle renvoie toujours false. Le DefaultTableModel surchage cette méthode pour qu'elle renvoie toujours true.

A titre d'exemple, je reprends l'application précédente dans laquelle je supprime la visualisation des fichiers image et où je rajoute l'édition possible du nom de fichier.

codage correspondant
package tables;

import java.awt.*;
import java.io.File;
import java.text.DecimalFormat;
import javax.swing.*;
import javax.swing.table.*;

public class Tables extends JFrame {
   private ModèleTableau modèle = new ModèleTableau();
   private JTable tableau = new JTable(modèle);

   public Tables() {
      super("Liste des fichiers");
      tableau.setDefaultRenderer(Long.class, new RenduEntier());
      tableau.setDefaultRenderer(Boolean.class, new RenduCaseACocher());
      add(new JScrollPane(tableau));
      pack();
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }
   
   public static void main(String[] args) { new Tables(); }
   
   private class ModèleTableau extends AbstractTableModel {
      private String[] colonnes = {"Nom fichier", "Extension", "Poids", "Image ?", "Dimension", "Vue"};
      private File[] fichiers = new File("C:/Photos/").listFiles();
      private Object[] lignes = new Object[fichiers.length];

      public ModèleTableau() {       
         for (int i=0; i<fichiers.length; i++) {
            File fichier = fichiers[i];
            String[] découpage = fichier.getName().split("\\.");
            String libellé = découpage[0];
            String extension = découpage[1].toUpperCase();
            long taille = fichier.length();
            Image image = new ImageIcon(fichier.getPath()).getImage();  
            boolean isImage = image.getWidth(null) != -1;
            String dimension = isImage ? image.getWidth(null)+" x "+image.getHeight(null) : "Aucune";
            lignes[i] = new Object[]{libellé, extension, taille, isImage, dimension};
         }
      }

      public int getRowCount() {
         return fichiers.length;
      }

      public int getColumnCount() {
         return colonnes.length;
      }

      public Object getValueAt(int ligne, int colonne) {
         Object[] fichier = (Object[]) lignes[ligne];
         return fichier[colonne];
      }

      public void setValueAt(Object valeur, int ligne, int colonne) {
         Object[] fichier(Object[]) lignes[ligne];
         fichier[colonne] = valeur;
      }

      @Override
      public Class<?> getColumnClass(int colonne) {
         Object[] première = (Object[]) lignes[0];
         return première[colonne].getClass();
      }

      @Override
      public String getColumnName(int colonne) {
         return colonnes[colonne];
      }

      @Override
      public boolean isCellEditable(int ligne, int colonne) {
         return colonne == 0;
      }
   }
   
   private class RenduEntier extends JLabel implements TableCellRenderer {
      public RenduEntier() {
         setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 5));
         setHorizontalAlignment(RIGHT);
      }
      
      public Component getTableCellRendererComponent(JTable table, Object valeur, boolean sélectionné, boolean focus, int ligne, int colonne) {
         DecimalFormat décimal = new DecimalFormat("#,##0 octets");
setText(décimal.format(valeur)); return this; } } private class RenduCaseACocher extends JCheckBox implements TableCellRenderer { public Component getTableCellRendererComponent(JTable table, Object valeur, boolean sélectionné, boolean focus, int ligne, int colonne) { setSelected((Boolean)valeur); setBackground(isSelected() ? Color.GREEN : Color.RED); return this; } } }

Attention, si vous désirez que votre saisie soit prise en compte, vous devez impérativement redéfinir la méthode setValueAt() de votre modèle de tableau afin que la nouvelle valeur s'intègre au bon endroit dans votre enregistrement bidirectionnel, ici l'objet lignes qui est un tableau de tableau d'Object.

Editeur de cellule par défaut

Le composant JTable possède un éditeur de cellule par défaut issu de la classe DefaultCellEditor qui implémente l'interface TableCellEditor. Un DefaultCellEditor peut être construit avec un JTextField, un JCheckBox ou un JComboBox (ou leurs fils). JTable installe automatiquement un éditeur de cases à cocher pour les cellules booléennes et un éditeur de champ de texte pour les cellules qui ne fournissent pas leur propre afficheur. Les champs de texte permettent à l'utilisateur de modifier les chaînes de caractères qui ont été créées en appliquant la méthode toString() à la valeur de retour de la méthode getValueAt() du modèle de tableau.

Lorsque la modification est terminée, la valeur modifiée est récupérée par un appel à la méthode getCellEditorValue() de la classe DefaultCellEditor. Cette méthode doit renvoyer une valeur de type correct (à savoir le type renvoyé par la méthode getColumnType() du modèle).

Pour obtenir un éditeur de menu déroulant (JComboBox), vous devez définir manuellement l'éditeur de la cellule. En effet, le composant JTable n'a aucune idée des valeurs appropriées pour un type particulier.

J'aimerais par exemple changer l'extension d'un fichier image afin de permettre ainsi la conversion vers un autre type de compression.

Voici le code d'initialisation de ce menu déroulant :

JComboBox extensions = new JComboBox(new String[]{"PNG", "GIF", "JPG"});

Pour créer ensuite un DefaultCellEditor, il suffit de fournir le menu déroulant au construteur :

TableCellEditor éditeur = new DefaultCellEditor(extensions);

Nous devons installer ensuite l'éditeur.

Contrairement aux afficheurs de cellules du poids du fichier, cet éditeur ne dépend pas du type de l'objet : nous ne voulons pas forcément l'utiliser pour tous les objets de type String. Au contraire, nous devrons l'installer dans une colonne particulière.

La classe JTable enregistre des informations sur les colonnes du tableau dans des objets de type TableColumn. Un objet TableColumnModel gère les colonnes. Si vous ne voulez pas insérer ou supprimer de colonnes dynamiquement, vous ne vous servirez pas beaucoup du modèle de colonnes du tableau. Cependant, pour obtenir un objet TableColumn particulier, vous devez passer par le modèle de colonnes et lui demander l'objet de colonne correspondant :

TableColumnModel modèleColonne = tableau.getColumnModel();
TableColumn colonne = modèleColonne.getColumn(4);

Pour terminer, vous pouvez installer l'éditeur de cellules :

tableau.setCellEditor(éditeur);
Si vos cellules sont plus hautes que les cellules par défaut, vous devrez également définir la hauteur de la ligne :
tableau.setRowHeight(hauteur);

Par défaut, toutes les lignes d'un tableau ont la même hauteur. Vous pouvez toutefois définir les hauteurs de lignes individuelles en appelant :

tableau.setRowHeight(ligne, hauteur);

La hauteur de ligne réelle est égale à la hauteur de ligne qui a été définie par ces méthodes, réduite de la marge de la ligne. La marge de ligne par défaut est de 1, mais vous pouvez également la modifier par l'appel :

tableau.setRowMargin(marge);

Pour afficher une icône dans l'en-tête, définissez la valeur de l'en-tête :

colonne.setHeaderValue(new ImageIcon("extension.gif"));

L'en-tête de tableau n'est toutefois pas suffisant pour choisir un afficheur approprié pour la valeur de l'en-tête. Vous devez installer l'afficheur manuellement. Par exemple, pour afficher une icône d'image dans un en-tête de colonne, appelez :

colonne.setHeaderRenderer(tableau.getDefaultRenderer(ImageIcon.class))  

Mise en oeuvre de modification de contenu de cellule

Nous allons prendre encore une fois l'application précédente en proposant toutefois pas mal de modifications. Cette fois-ci, seuls les fichiers images seront visibles dans le tableau, et il sera surtout possible de retoucher les images déjà stockées sur le disque dur en proposant de changer le nom du fichier, le format de l'image (type de compression) ainsi que les nouvelles dimensions en respectant le ratio de l'image, c'est-à-dire le rapport entre la largeur et la hauteur.

codage correspondant
package tables;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.geom.*;
import java.awt.image.*;
import java.io.*;
import java.text.DecimalFormat;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.table.*;

public class Tables extends JFrame {
   private ModèleTableau modèle = new ModèleTableau();
   private JTable tableau = new JTable(modèle);
   private JComboBox extensions = new JComboBox(new String[]{"PNG", "GIF", "JPG"});
   private JToolBar valider = new JToolBar();

   public Tables() {
      super("Liste des fichiers");
      TableColumnModel modèleColonne = tableau.getColumnModel();
      modèleColonne.getColumn(1).setCellEditor(new DefaultCellEditor(extensions));
      tableau.setDefaultRenderer(Long.class, new RenduEntier());
      tableau.setRowHeight(52);
      add(new JScrollPane(tableau));
      valider.add(new AbstractAction("Valider changement de la ligne sélectionnée") {
         public void actionPerformed(ActionEvent e) {
            modèle.changerFichier(tableau.getSelectedRow());
         }
      });
      add(valider, BorderLayout.NORTH);
      pack();
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }
   
   public static void main(String[] args) { new Tables(); }
   
   private class ModèleTableau extends AbstractTableModel {
      private String[] colonnes = {"Nom fichier", "Extension", "Poids", "Largeur", "Hauteur", "Image"};
      private File[] fichiers;
      private Object[] lignes;
      
      public ModèleTableau() {       
         constituer();
      }
      
      private void constituer() {
         fichiers = new File("C:/Photos/").listFiles(new FileFilter() {
            public boolean accept(File fichier) {
               String nom = fichier.getName();
               String[] découpage = fichier.getName().split("\\.");
               String extension = découpage[1].toUpperCase();        
               return extension.equals("GIF") || extension.equals("PNG") || extension.equals("JPG");
            }        
         });
         lignes = new Object[fichiers.length];
         for (int i=0; i<fichiers.length; i++) {
            File fichier = fichiers[i];
            String[] découpage = fichier.getName().split("\\.");
            String extension = découpage[1].toUpperCase();   
            long taille = fichier.length();
            BufferedImage image = null; 
            Icon icône = null;
            try {
               image = ImageIO.read(fichier); 
               icône = new ImageIcon(image.getScaledInstance(-1, 50, Image.SCALE_DEFAULT));
            } 
            catch (IOException ex) { }
            lignes[i] = new Object[]{découpage[0], extension, taille, image.getWidth(), image.getHeight(), icône, image};
         }         
      }

      @Override
      public int getRowCount() {
         return fichiers.length;
      }
      
      @Override
      public int getColumnCount() {
         return colonnes.length;
      }

      @Override
      public Object getValueAt(int ligne, int colonne) {
         Object[] image = (Object[]) lignes[ligne];
         return image[colonne];
      }

      @Override
      public void setValueAt(Object valeur, int ligne, int colonne) {
         Object[] image = (Object[]) lignes[ligne];
         if (colonne==3) {
            double largeur = (Integer) image[3];
            double hauteur = (Integer) image[4];
            double ratio = (double)largeur / hauteur;
            double récupération = (Integer) valeur;
            image[4] = (int)(récupération / ratio);            
            tableau.repaint();
         }
         image[colonne] = valeur;
      }      

      @Override
      public Class<?> getColumnClass(int colonne) {
         Object[] première = (Object[]) lignes[0];
         return première[colonne].getClass();
      }

      @Override
      public String getColumnName(int colonne) {
         return colonnes[colonne];
      }

      @Override
      public boolean isCellEditable(int ligne, int colonne) {
         return colonne==0 || colonne==1 || colonne==3 ;
      }
      
      public void changerFichier(int ligne) {
         Object[] infos = (Object[]) lignes[ligne];   
         BufferedImage image = (BufferedImage) infos[6];
         String nom = (String) infos[0];
         String extension = (String) infos[1];
         int largeur = (Integer) infos[3];
         int hauteur = (Integer) infos[4];
         double ratio = (double)largeur / image.getWidth();      
         BufferedImage traitement = new BufferedImage(largeur, hauteur, image.getType());
         AffineTransform retailler = AffineTransform.getScaleInstance(ratio, ratio);
         int interpolation = AffineTransformOp.TYPE_BICUBIC;
         AffineTransformOp retaillerImage = new AffineTransformOp(retailler, interpolation);
         retaillerImage.filter(image, traitement);
         try {
            ImageIO.write(traitement, extension, new File("C:/Photos/" + nom + '.' + extension));
            constituer();
            tableau.revalidate();
         } 
         catch (IOException ex) {}
      }
   }
   
   private class RenduEntier extends DefaultTableCellRenderer {
      public RenduEntier() {
         setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 5));
         setHorizontalAlignment(RIGHT);
      }
      
      @Override
      public Component getTableCellRendererComponent(JTable table, Object valeur, boolean sélectionné, boolean focus, int ligne, int colonne) {
         DecimalFormat décimal = new DecimalFormat("#,##0 octets");
         setText(décimal.format(valeur));
         if (sélectionné) setBackground(tableau.getSelectionBackground());
         else setBackground(Color.WHITE);
         return this;
      }     
   }
}
 

Editeur de cellules de tableau personnalisé

Nous pouvons aller encore plus loin dans cette démarche. Nous pouvons, par exemple, faire en sorte que notre nom de fichier image soit éditer automatiquement au travers d'un sélecteur de fichier.

L'éditeur de nom de fichier ne doit pas être un éditeur de cellule standard, mais une implémentation personnalisée. Pour créer un éditeur de cellules personnalisé, vous devez implémenter l'interface TableCellEditor. Cette interface est assez pénible à utiliser. Heureusement, une classe AbstractCellEditor est fournie pour gérer les détails de l'événement.

Interface TableCellEditor

Pour maîtriser la gestion de votre éditeur personnalisé, vous devez redéfinir un certain nombre de méthodes issues de cette interface TableCellEditor :

  1. getTableCellEditorComponent() : La méthode getTableCellEditorComponent() de l'interface TableCellEditor a besoin d'un composant pour afficher une cellule. C'est exactement comme pour la méthode getTableCellRendererComponent() de l'interface TableCellRenderer, sauf qu'il n'y a cette fois aucun paramètre focus. Comme la cellule doit être modifiée, elle est censée posséder le focus.

    Dans le cas d'un menu déroulant, le composant de l'éditeur remplace temporairement l'afficheur.
    §

  2. isCellEditable() : La classe JTable appelle votre éditeur avec un événement (comme un clic de souris) pour déterminer si cet événement est acceptable pour initialiser le processus de modification. Vous pouvez vous occuper de la gestion événementielle au travers de la méthode isCellEditable() de l'interface TableCellEditor. Nous pouvons par exemple accepter tous les événements.
    @Override 
    public boolean isCellEditable(EventObject événement) {
        return true;
    } 

    Cependant, si cette méthode renvoie false, le tableau n'insérera pas le composant de l'éditeur.
    §

  3. shouldSelectCell() : Une fois que le composant de l'éditeur est installé, la méthode shouldSelectCell() est appelée, avec le même événement. Le processus de modification doit commencer dans cette méthode, par exemple en affichant la fenêtre de sélection de fichier.
    @Override 
    public boolean shouldSelectCell(EventObject événement) {
       sélecteur.showDialog(parent, libelléBouton)  
       return true;
    } 
  4. cancelCellEditing() et stopCellEditing() : Si l'utilisateur doit annuler la modification en cours, le tableau appelle la méthode cancelCellEditing(). Si l'utilisateur a cliqué sur une autre cellule du tableau, il appelle la méthode stopCellEditing(). Lorsque votre méthode stopCellEditing() est appelée, le tableau peut essayer d'utiliser la valeur en cours de modification. C'est pourquoi, il ne faut renvoyer true que lorsque la valeur courante est valide. Pour notre sélecteur de fichier, il peut être judicieux de vérifier que seules les valeurs correctes soient renvoyées à l'éditeur.

    Vous devrez aussi appeler les méthodes de superclasse pour déclencher des événements, faute de quoi la modification ne fonctionnera pas correctement.

    @Override 
    public void stopCellEditing() {
       ...
      super.stopCellEditing();
    } 
  5. getCellEditorValue() : Enfin, vous devez dedéfinir la méthode getCellEditorValue() afin qu'elle délivre la valeur fournie par l'utilisateur au cours de la procédure de modification :
    @Override 
    public Object getCellEditorValue() {
       return sélecteur.getSelectedFile().getName();
    } 
Pour résumer

Pour résumer, votre éditeur personnalisé doit :

  1. Etendre la classe AbstractCellEditor et implémenter l'interface TableCellEditor.
  2. Définir la méthode getTableCellEditorComponent() pour fournir un composant qui va représenté votre valeur. Il peut s'agir d'un composant dummy (si vous afficher une boîte de dialogue) ou un composant pour la modification sur place, comme un menu déroulant ou un champ de texte.
  3. Définir les méthodes shouldSelectCell(), stopCellEditing() et cancelCellEditing() pour gérer le début, la réalisation et l'annulation de la procédure de modification. stopCellEditing() et cancelCellEditing() doivent appeler les méthodes de la superclasse pour s'assurer que les écouteurs sont avertis
  4. Définir la méthode getCellEditorValue() pour renvoyer la valeur qui résulte de la procédure d'édition.
  5. Enfin, vous devez indiquer le moment où l'utilisateur a terminé la modification en appelant explicitement les méthodes stopCellEditing() et/ou cancelCellEditing().
Prise en compte de la modification

Lorsque la modification est terminée, la classe JTable appelle la méthode setValueAt() du modèle de tableau. Comme nous l'avons déjà évoqué au début de ce chapitre, vous devez impérativement surcharger cette méthode pour enregistrer la nouvelle valeur.

void setValue(Object valeur, int ligne, int colonne);

Le paramètre valeur correspond à l'objet renvoyé par l'éditeur de cellules.

  1. Si vous avez implémenté l'éditeur de cellule, vous connaissez le type d'objet renvoyé par la méthode getCellEditorValue().
  2. Dans le cas de DefaultCellEditor, il existe trois possibilités pour cette valeur. Il s'agit d'un Boolean si la cellule concernée est une case à cocher et d'une chaîne pour un champ de texte. Si la valeur provient d'un menu déroulant, il s'agit alors de l'objet sélectionné par l'uitlisateur.

Si l'objet valeur n'est pas du type approprié, vous devrez le convertir. Cela se produit le plus souvent lorsqu'un nombre est modifié dans un champ de texte. Dans notre exemple, nous plaçons des chaînes de caractères formatées suivant le motif proposé par la classe DecimalFormat pour la colonne Poids.

codage correspondant
package tables;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.geom.*;
import java.awt.image.*;
import java.io.*;
import java.text.DecimalFormat;
import java.util.EventObject;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import javax.swing.table.*;

public class Tables extends JFrame {
   private ModèleTableau modèle = new ModèleTableau();
   private JTable tableau = new JTable(modèle);
   private JComboBox extensions = new JComboBox(new String[]{"PNG", "GIF", "JPG"});
   private JToolBar valider = new JToolBar();

   public Tables() {
      super("Liste des fichiers");
      TableColumnModel modèleColonne = tableau.getColumnModel();
      modèleColonne.getColumn(1).setCellEditor(new DefaultCellEditor(extensions));
      modèleColonne.getColumn(0).setCellEditor(new EditeurCellule());
      tableau.setRowHeight(52);
      add(new JScrollPane(tableau));
      valider.add(new AbstractAction("Valider changement de la ligne sélectionnée") {
         public void actionPerformed(ActionEvent e) {
            modèle.changerFichier(tableau.getSelectedRow());
         }
      });
      add(valider, BorderLayout.NORTH);
      pack();
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }
   
   public static void main(String[] args) { new Tables(); }
   
   private class ModèleTableau extends AbstractTableModel {
      private String[] colonnes = {"Nom fichier", "Extension", "Poids", "Largeur", "Hauteur", "Image"};
      private File[] fichiers;
      private Object[] lignes;
      
      public ModèleTableau() {       
         constituer();
      }
      
      private void constituer() {
         fichiers = new File("C:/Photos/").listFiles(new FileFilter() {
            public boolean accept(File fichier) {
               String nom = fichier.getName();
               String[] découpage = fichier.getName().split("\\.");
               String extension = découpage[1].toUpperCase();        
               return extension.equals("GIF") || extension.equals("PNG") || extension.equals("JPG");
            }        
         });
         lignes = new Object[fichiers.length];
         for (int i=0; i<fichiers.length; i++) {
            File fichier = fichiers[i];
            String[] découpage = fichier.getName().split("\\.");
            String extension = découpage[1].toUpperCase();   
            long taille = fichier.length();
            DecimalFormat décimal = new DecimalFormat("#,##0 octets");
            BufferedImage image = null; 
            Icon icône = null;
            try {
               image = ImageIO.read(fichier); 
               icône = new ImageIcon(image.getScaledInstance(-1, 50, Image.SCALE_DEFAULT));
            } 
            catch (IOException ex) { }
            lignes[i] = new Object[]{découpage[0], extension, décimal.format(taille), image.getWidth(), image.getHeight(), icône, image};
         }         
      }

      @Override
      public int getRowCount() {
         return fichiers.length;
      }
      
      @Override
      public int getColumnCount() {
         return colonnes.length;
      }

      @Override
      public Object getValueAt(int ligne, int colonne) {
         Object[] image = (Object[]) lignes[ligne];
         return image[colonne];
      }

      @Override
      public void setValueAt(Object valeur, int ligne, int colonne) {
         Object[] image = (Object[]) lignes[ligne];
         if (colonne==3) {
            double largeur = (Integer) image[3];
            double hauteur = (Integer) image[4];
            double ratio = (double)largeur / hauteur;
            double récupération = (Integer) valeur;
            image[4] = (int)(récupération / ratio);            
            tableau.repaint();
         }
         image[colonne] = valeur;
      }      

      @Override
      public Class<?> getColumnClass(int colonne) {
         Object[] première = (Object[]) lignes[0];
         return première[colonne].getClass();
      }

      @Override
      public String getColumnName(int colonne) {
         return colonnes[colonne];
      }

      @Override
      public boolean isCellEditable(int ligne, int colonne) {
         return colonne==0 || colonne==1 || colonne==3 ;
      }
      
      public void changerFichier(int ligne) {
         Object[] infos = (Object[]) lignes[ligne];   
         BufferedImage image = (BufferedImage) infos[6];
         String nom = (String) infos[0];
         String extension = (String) infos[1];
         int largeur = (Integer) infos[3];
         int hauteur = (Integer) infos[4];
         double ratio = (double)largeur / image.getWidth();      
         BufferedImage traitement = new BufferedImage(largeur, hauteur, image.getType());
         AffineTransform retailler = AffineTransform.getScaleInstance(ratio, ratio);
         int interpolation = AffineTransformOp.TYPE_BICUBIC;
         AffineTransformOp retaillerImage = new AffineTransformOp(retailler, interpolation);
         retaillerImage.filter(image, traitement);
         try {
            ImageIO.write(traitement, extension, new File("C:/Photos/" + nom + '.' + extension));
            constituer();
            tableau.revalidate();
         } 
         catch (IOException ex) {}
      }
   }
 
   private class EditeurCellule extends AbstractCellEditor implements TableCellEditor {
      private JFileChooser sélecteur = new JFileChooser("C:/Photos");
      private JLabel nomFichier = new JLabel();
      private boolean valide;

      public EditeurCellule() {
         sélecteur.addChoosableFileFilter(new FileNameExtensionFilter("Images GIF", "gif"));
         sélecteur.addChoosableFileFilter(new FileNameExtensionFilter("Images PNG", "png"));
         sélecteur.addChoosableFileFilter(new FileNameExtensionFilter("Images JPEG", "jpg", "jpeg"));
      }

      @Override
      public Object getCellEditorValue() {
         if (valide) {
            String[] découpage = sélecteur.getSelectedFile().getName().split("\\."); 
            nomFichier.setText(découpage[0]);
         }
         return nomFichier.getText();
      }

      @Override
      public boolean shouldSelectCell(EventObject anEvent) {
         valide = sélecteur.showDialog(null, "Nom du fichier")==JFileChooser.APPROVE_OPTION;
         stopCellEditing();
         return true;
      }

      @Override
      public Component getTableCellEditorComponent(JTable table, Object valeur, boolean sélectionné, int ligne, int colonne) {
         nomFichier.setText((String) valeur);
         return nomFichier;
      }

      @Override
      public boolean stopCellEditing() {
         nomFichier.repaint();
         return super.stopCellEditing();
      }            
   }
}

 

Choix du chapitre Travailler avec les lignes et les colonnes

Dans ce chapitre, nous allons apprendre à manipuler les lignes et les colonnes d'un tableau. Swing n'est pas du tout symétrique pour la gestion d'un tableau, c'est-à-dire que les opérations supportées par les lignes d'une part et par les colonnes d'autre part ne sont pas les mêmes. Le composant JTable a été optimisé pour afficher des lignes d'informations de même structure, comme le résultat d'une requête d'une base de données, et non pour une grille bidimensionnelle arbitraire d'objets. Nous reviendrons sur cette asymétrie au cours de ce chapitre.

Modifier la taille des colonnes

La classe TableColumn permet de contrôler la taille des colonnes. Vous pouvez ainsi choisir :

  1. void setPreferredSize(int largeur) : la largeur préférée de la colonne.
  2. void setMinWidth(int largeur) : la largeur minimale de la colonne.
  3. void setMaxWidth(int largeur) : la largeur maximale de la colonne.

Cette information est utilisée par le tableau pour la mise en forme des colonnes. Utilisez la méthode setResizable(boolean) pour permettre ou non à l'utilisateur de modifier la largeur d'une colonne. Vous pouvez aussi modifier la largeur d'une colonne avec la méthode setWidth(int).

Lorsque la taille d'une colonne est modifiée, le comportement par défaut est de conserver la largeur totale du tableau. Naturellement, dans ce cas, les changements de largeur de la colonne modifiée doivent être reportés sur les autres colonnes. Avec le comportement par défaut, ces changements seront inégalement reportés sur la colonne à droite de la colonne modifiée. Cela permet à l'utilisateur d'ajuster toutes les colonnes de gauche à droite.

Vous pouvez choisir un autre comportement en utilisant la méthode setAutoResizeMode(int) :
  1. JTable.AUTO_RESIZE_OFF : Ne modifie pas les colonnes, change la taille du tableau.
  2. JTable.RESIZE_NEXT_COLUMN : Modifie uniquement la taille de la colonne suivante.
  3. JTable.RESIZE_SUBSEQUENT_COLUMN : Modifie identiquement toutes les colonnes restantes. C'est le comportement par défaut.
  4. JTable.AUTO_RESIZE_LAST_COLUMN : Modifie uniquement la taille de la dernière colonne.
  5. JTable.AUTO_RESIZE_ALL_COLUMN : Modifie toutes les colonnes du tableau. Ce choix est à éviter parce qu'il devient très difficile d'ajuster la largeur de plusieurs colonnes.

Sélectionner des lignes, des colonnes et des cellules

En fonction du mode de sélection, l'utilisateur peut sélectionner des lignes, des colonnes ou des cellules isolées du tableau.

Par défaut, la sélection de lignes est autorisée. Une ligne entière est sélectionnée lorsque l'utilisateur clique sur une cellule. Appelez la méthode setRowSelectionAllowed(false) de la classe JTable pour supprimer cette sélection par lignes.

Lorsque la sélection par lignes est permise, vous pouvez choisir parmi plusieurs modes de sélection. Vous devrez donc récupérer le modèle de sélection et utiliser sa méthode setSelectionMode(int) :

table.getSelectionMode().setSelectionMode(mode)

Ici mode peut prendre l'une des trois valeurs suivantes :

  1. ListSelectionModel.SINGLE_SELECTION : une seule ligne.
  2. ListSelectionModel.SINGLE_INTERVAL_SELECTION : un ensemble continu de lignes.
  3. ListSelectionModel.MULTIPLE_INTERVAL_SELECTION : n'importe quelles lignes.

La sélection des colonnes est désactivée par défaut. Elle peut être activée avec l'appel de la méthode setColumnSelectionAllowed(true) de la classe JTable.

L'activation des deux types de sélection (lignes et colonnes) équivaut à activer la sélection de cellules. L'utilisateur sélectionne alors des plages de cellules. Vous pouvez également activer ce réglage par l'appel de la méthode setCellSelectionEnabled(true) de la classe JTable.

Vous pouvez identifier les lignes et les colonnes sélectionnées en appelant les méthodes getSelectedRows() et getSelectedColumns() de la classe JTable. Ces deux méthodes renvoient un tableau d'indices int[] correspondant aux éléments sélectionnées.

Cacher et afficher des colonnes

La méthode removeColumn() de la classe JTable supprime une colonne de l'affichage d'un tableau. Les données de la colonne ne sont en fait pas supprimées du modèle, elles sont justes cachées. La méthode removeColumn() prend un argument de type TableColumn. Si vous possédez un numéro de colonne, par exemple à partir d'un appel à getSelectedColumns(), vous devrez encore demander au modèle de l'arbre la colonne réelle de l'arbre qui correspond à ce numéro :

JTable table = new JTable();
TableColumnModel modèleColonne = table.getColumnModel();
TableColumn colonne = modèleColonne.getColumn(i);
table.removeColumn(colonne);
Si vous avez mémorisé ce numéro de colonne, il est possible de l'afficher à nouveau :

table.addColumn(colonne);

Cette méthode ajoute la colonne à la fin du tableau. Si vous préférez qu'elle apparaisse à un autre endroit, vous devez appeler la méthode moveColumn().

Vous pouvez également ajouter une nouvelle colonne qui correspond à un indice de colonne du modèle de tableau, en ajoutant un nouvel objet TableColumn :

table.addColumn(new TableColumn(indexmodèleColonne));

En fait, plusieurs colonnes du tableau peuvent être affichées à partir d'une même colonne du modèle.
§

Cependant, il n'existe pas de méthode dans JTable pour cacher ou afficher des lignes. Si vous souhaitez cacher une ligne, vous devrez avoir recours à un modèle de filtre.

Ajouter et supprimer des lignes dans le modèle de tableau par défaut

La classe DefaultTableModel est une classe concrète qui implémente l'interface TableModel. Elle stocke une grille bidimensionnelle d'objets. Si vos données se trouvent déjà sous cette forme, il n'est pas nécessaire de recopier toutes ces données dans un modèle de tableau par défaut. En revanche, si vous possédez peu de données, il peut être pratique de les copier pour obtenir rapidement un tableau. La classe DefaultTableModel possède des méthodes permettant d'ajouter des lignes et des colonnes et de supprimer des lignes.

Les méthodes addRow() et addColumn() de la classe DefaultTableModel ajoutent une nouvelle ligne ou une nouvelle colonne dans les données. Elles nécessitent un tableau Object[] ou un vecteur qui contient les nouvelles données. Avec la méthode addColumn(), vous devez aussi fournir le nom de la nouvelle colonne. Ces méthodes ajoutent les nouvelles données à la fin de la grille. Pour insérer une nouvelle ligne au milieu des données existantes, utilisez la méthode insertRow(). Inversement la méthode removeRow() supprime une ligne du modèle.

Il n'existe aucune méthode pour insérer une colonne au milieu d'une grille. Il n'existe également aucune méthode pour supprimer une colonne.
§

Comme l'objet JTable s'enregistre lui-même comme un écouteur de modèle de tableau, le modèle avertit le tableau lorsque les données sont insérées ou supprimées. Pour l'instant, le tableau met à jour l'affichage.

Exemple de mise en oeuvre

Le programme suivant montre comment fonctionnent la sélection et la modification. Un modèle de tableau par défaut contient un simple ensemble de données, comme une table de multiplication. Le menu Edition contient les commandes suivantes :

  1. Cacher toutes les colonnes sélectionnées ;
  2. Afficher toutes les colonnes cachées ;
  3. Ajouter une ligne de données à la fin du modèle ;
  4. Supprimer du modèle les lignes sélectionnées ;
  5. Effacer les cellules sélectionnées.

codage correspondant
package tables;

import java.awt.event.*;
import java.util.ArrayList;
import javax.swing.*;
import javax.swing.table.*;

public class Tables extends JFrame {
   private DefaultTableModel modèle = new DefaultTableModel(10, 10);
   private JTable table = new JTable(modèle);
   private final ArrayList<TableColumn> colonnesSupprimées = new ArrayList<TableColumn>();
   private JMenuBar menu = new JMenuBar();
   private JMenu sélection = new JMenu("Sélection");
   private JMenu édition = new JMenu("Edition");
   private JCheckBoxMenuItem lignes = new JCheckBoxMenuItem("Lignes", table.getRowSelectionAllowed());
   private JCheckBoxMenuItem colonnes = new JCheckBoxMenuItem("Colonnes", table.getColumnSelectionAllowed());
   private JCheckBoxMenuItem cellules = new JCheckBoxMenuItem("Cellules", table.getCellSelectionEnabled());
   
   public Tables() {
      super("Sélections dans une table");
      for (int i=0; i<modèle.getRowCount(); i++)
         for (int j=0; j<modèle.getColumnCount(); j++)
            modèle.setValueAt((i+1)*(j+1), i, j);
      add(new JScrollPane(table));
      setJMenuBar(menu);
      menu.add(sélection);
      menu.add(édition);
      sélection.add(lignes);
      sélection.add(colonnes);
      sélection.add(cellules);
      lignes.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
            table.clearSelection();
            table.setRowSelectionAllowed(lignes.isSelected());
            cellules.setSelected(table.getCellSelectionEnabled());
         }
      });
      colonnes.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
            table.clearSelection();
            table.setColumnSelectionAllowed(colonnes.isSelected());
            cellules.setSelected(table.getCellSelectionEnabled());
         }
      });
      cellules.addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
            table.clearSelection();
            table.setCellSelectionEnabled(cellules.isSelected());
            lignes.setSelected(table.getRowSelectionAllowed());           
            colonnes.setSelected(table.getColumnSelectionAllowed());      
         }
      });
      édition.add("Cacher les colonnes").addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
            int[] sélectionnées = table.getSelectedColumns();
            TableColumnModel modèleColonne = table.getColumnModel();
            for (int i=sélectionnées.length-1; i>=0; i--) {
               TableColumn colonne = modèleColonne.getColumn(sélectionnées[i]);
               table.removeColumn(colonne);
               colonnesSupprimées.add(colonne);
            }
         }
      });
      édition.add("Afficher les colonnes").addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
            for (TableColumn colonne : colonnesSupprimées) table.addColumn(colonne);
            colonnesSupprimées.clear();
         }
      });
      édition.add("Ajouter une ligne").addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
            Integer[] nouvellesCellules = new Integer[modèle.getColumnCount()];
            for (int i=0; i<nouvellesCellules.length; i++)
               nouvellesCellules[i] = new Integer((i+1) * modèle.getRowCount()+1);
            modèle.addRow(nouvellesCellules);   
         }
      });
      édition.add("Supprimer des lignes").addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
            int[] sélectionnées = table.getSelectedRows();
            for (int i=sélectionnées.length-1; i>=0; i--) modèle.removeRow(sélectionnées[i]);
         }
      });
      édition.add("Effacer les cellules").addActionListener(new ActionListener() {
         public void actionPerformed(ActionEvent e) {
            for (int i=0; i<table.getRowCount(); i++)
               for (int j=0; j<table.getColumnCount(); j++)
                  if (table.isCellSelected(i, j)) table.setValueAt(0, i, j);
         }
      });      
      pack();
      setDefaultCloseOperation(EXIT_ON_CLOSE);
      setVisible(true);
   }
   
   public static void main(String[] args) { new Tables(); }   
}