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("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é; } } }
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.
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);
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 :
int mode = TreeSelectionModel.SINGLE_TREE_SELECTION;
arbre.getSelectionModel().setSelectionMode(écouteur);
Pour récupérer la sélection courante, il faut interroger l'arbre avec la méthode getSelectionPaths() :
TreePath[] sélections = arbre.getSelectionPaths();
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.
§
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.
... 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()); } } ... }
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.
Le composant JTable comporte un certain nombre de modèles internes qui permettent de respecter l'architecture MVC :
Plusiseurs constructeurs sont aménagés pour résoudre les différentes possibilités de création de tableaux :
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.
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(); } }
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.
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.
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.
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(); } }
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.
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 :
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.
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().
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 :
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.
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.
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]; } } }
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.
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.
table.setDefaultRenderer(Integer.class, afficheur);
Cet afficheur est maitnenant utilisé pour tous les objets du type spécifié.
§
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.
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; } } }
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.
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.
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).
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);
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))
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.
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; } } }
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.
Pour maîtriser la gestion de votre éditeur personnalisé, vous devez redéfinir un certain nombre de méthodes issues de cette interface TableCellEditor :
Dans le cas d'un menu déroulant, le composant de l'éditeur remplace temporairement l'afficheur.
§
@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.
§
@Override public boolean shouldSelectCell(EventObject événement) { sélecteur.showDialog(parent, libelléBouton) return true; }
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(); }
@Override public Object getCellEditorValue() { return sélecteur.getSelectedFile().getName(); }
Pour résumer, votre éditeur personnalisé doit :
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.
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.
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(); } } }
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.
La classe TableColumn permet de contrôler la taille des colonnes. Vous pouvez ainsi choisir :
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.
En fonction du mode de sélection, l'utilisateur peut sélectionner des lignes, des colonnes ou des cellules isolées du tableau.
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 :
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.
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);
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().
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.
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.
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 :
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(); } }