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("