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.

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.
Avant de poursuivre, je pense qu'il est souhaitable de se mettre d'accord sur quelques éléments de terminologie :
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.
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 :
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.
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);
TreeNode racine = new DefaultMutableTreeNode("Noeud racine");
TreeModel modèle = new DefaultTreeModel(racine);
JTree arbre = new JTree(modèle);
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.
DefaultMutableTreeNode noeud = new DefaultMutableTreeNode("Un noeud"); noeud.setUserObject("Un autre noeud");
Ensuite, il faut établir les relations hiérarchies entre les parents et les enfants pour chaque noeud.
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);
TreeModel modèle = new DefaultTreeModel(racine); JTree arbre = new JTree(modèle);
JTree arbre = new JTree(racine);
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.
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.
§
| 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);
|
![]() |
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 :
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() :
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 :
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-contreprivate 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); } |
![]() |
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 :
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.


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(); ... }
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();
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 :
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".

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)); } } }
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.
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();
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.
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
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.
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.
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);
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.
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.

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)); } } }
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.
Vous pouvez personnaliser l'affichage de trois manières différentes :
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 :
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 :
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(); }
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.
§
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é.
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.
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("