Utilizzo di javax.swing.JTree
In questo tutorial viene illustrato l’utilizzo del componente Swing JTree.
Il modo migliore per visualizzare una struttura dati gerarchica (come può essere un file xml) è usare un albero. Il componente Swing che lo implementa è javax.swing.JTree, supportato dalle classi contenute nel package javax.swing.tree per quanto riguarda la manipolazione del contenuto e da alcune classi del package javax.swing.event per la gestione degli eventi.
Costruttori e prime personalizzazioni
La classe JTree fornisce diversi costruttori, un paio sono:
1 2 3 |
JTree() JTree(TreeModel modello) JTree(TreeNode nodoRadice) |
Il costruttore senza parametri crea un albero con un contenuto predefinito (cioè ha già inseriti alcuni nodi e sotto-nodi). Il secondo costruttore crea un albero a cui viene associato il modello indicato come parametro mentre il terzo costruttore prende in ingresso il nodo che sarà poi la radice dell’albero.
A meno di esigenze particolari, è meglio creare un albero con solo la radice e caricare i sotto-nodi in un secondo momento. Dopo aver istanziato l’albero conviene dargli qualche ritoccatina, a seconda delle nostre esigenze. Diversi sono i metodi di JTree utili a queste “ritoccatine”. Alcuni sono:
1 2 3 4 5 |
setEditable(boolean editabile) setRootVisible(boolean radiceVisibile) setShowsRootHandles(boolean maniglieRadiceVisibili) setVisibleRowCount(int numeroRigheVisibili) setModel(TreeModel modello) |
- setEditable() stabilisce se l’utente può modificare o meno il nome dei nodi dopo che questi sono stati selezionati col doppio click. Se si passa true l’utente può modificare, altrimenti no;
- setRootVisible() stabilisce invece se il nodo radice è visibile o meno. Se si passa true il nodo radice sarà visibile come tutti gli altri nodi, altrimenti i primi nodi ad essere visibili saranno i figli della radice;
- setShowsRootHandles() stabilisce se la “maniglia” del nodo radice deve essere visualizzata (true) o meno (false);
- setVisibleRowCount() imposta il numero di righe che l’albero mostrerà prima di far apparire le barre di scorrimento (sempre che le mettiate, le barre);
- setModel() permette di associare all’albero un modello (eventualmente) personalizzato;
Inserire nodi
Ogni nodo di un JTree è un oggetto della classe DefaultMutableTreeNode (contenuta in javax.swing.tree). La classe DefaultMutableTreeNode implementa MutableTreeNode (che a sua volta implementa TreeNode) quindi gli oggetti di questa classe possono essere passati al costruttore di JTree senza problemi. Un primo nodo viene inserito nell’albero quando questo viene istanziato:
1 2 3 4 5 6 |
// crea il nodo radice DefaultMutableTreeNode nodoRadice = new DefaultMutableTreeNode("nome del nodo"); // crea l'albero con il nodo creato per radice JTree albero = new JTree(nodoRadice); |
N.B.: il nodo radice può essere creato direttamente nella chiamata del costruttore di JTree. Conviene però tenere un riferimento alla radice poichè per alcune operazioni il nodo radice è indispensabile. In ogni caso lo si può ottenere in qualunque momento chiamando il metodo getRoot() del modello associato all’albero.
Per aggiungere nodi ad un albero ci sono due metodi: uno utilizza il modello associato all’albero mentre l’altro utilizza direttamente i nodi.
1) Utilizzando il modello associato all’albero
Innanzitutto è necessario ottenere un riferimento al modello:
1 |
DefaultTreeModel modello = (DefaultTreeModel)albero.getModel(); |
Dopo aver ottenuto il modello, usarlo per chiamare il metodo insertNodeInto con i giusti parametri (nell’ordine: il nodo da aggiungere, il nodo a cui aggiungere il sotto-nodo e l’indice in cui inserire il nodo):
1 2 3 4 5 6 7 8 9 10 11 12 |
// creo il nodo da aggiungere alla radice DefaultMutableTreeNode nodo1 = new DefaultMutableTreeNode("primo nodo"); // creo un sotto-nodo di nodo1 DefaultMutableTreeNode sottonodo1 = new DefaultMutableTreeNode("primo sotto nodo di nodo1"); // aggiungo nodo1 alla radice modello.insertNodeInto(nodo1, nodoRadice, nodoRadice.getChildCount()); // aggiungo sottonodo1 a nodo1 modello.insertNodeInto(sottonodo1, nodo1, nodo1.getChildCount()); |
Nell’esempio qui sopra creo due nodi. Il primo viene agganciato alla radice mentre l’altro viene agganciato al primo nodo. Per ottenere l’indice chiamare il metodo getChildCount: ogni nodo memorizza i propri sotto-nodi di primo livello in un oggetto Vector e getChildCount restituisce l’indice del primo elemento del vettore che può memorizzare il nuovo nodo.
2) Utilizzando direttamente i nodi
Per aggiungere un sotto-nodo ad un nodo, chiamare il metodo add sul nodo a cui agganciare il nuovo nodo, passando come parametro il nodo da agganciare.
1 2 3 4 5 |
// aggancio nodo1 al nodo radice nodoRadice.add(nodo1); // aggancio sottonodo1 a nodo1 nodo1.add(sottonodo1); |
In entrambi i casi, per rendere visibili le modifiche fatte occorre chiamare sul modello il metodo reload:
1 2 3 4 |
modello.reload(); // oppure // modello.reload(nodoDaRicaricare); |
reload ricarica tutto l’albero mentre reload(nodoDaRicaricare) ricarica solo il nodo indicato (ed è particolarmente utile quando si rinomina un nodo). Se si utilizza reload tutti i nodi aperti vengono immediatamente chiusi. Se si vuole mostrare un particolare nodo (magari l’ultimo inserito), occorre chiamare il metodo scrollPathToVisible sull’istanza di JTree, indicando il percorso (come oggetto TreePath) del nodo da rendere visibile:
1 2 |
albero.scrollPathToVisible( new TreePath(nodoDaRendereVisibile.getPath())); |
se il JTree contiene molti nodi e utilizza delle barre di scorrimento, scrollPathToVisible non “scrolla” lo ScrollPane. Se si vuole che il nodo venga portato “a vista”, occorre chiamare setSelectionPath dopo scrollPathToVisible:
1 2 3 4 5 6 7 8 |
// rende visibile il nodo indicato albero.scrollPathToVisible( new TreePath(nodoDaRendereVisibile.getPath())); // scrolla lo ScrollPane su cui e' posizionato l'albero // in modo da rendere effettivamente visibile il nodo indicato albero.setSelectionPath( new TreePath(nodoDaRendereVisibile.getPath()).getParentPath()); |
Rinominare un nodo
Per rinominare un nodo, è sufficiente chiamare il metodo setUserObject sul nodo da rinominare, passando come parametro il nuovo nome:
1 |
nodo.setUserObject("nuovo nome"); |
se il nuovo nome occupa più spazio (in pixel) di quello vecchio, sull’etichetta associata al nome appariranno una serie di puntini (per indicare, appunto, che il nuovo nome è troppo lungo). Per far sparire i puntini è necessario aprire e chiudere il nodo (in questo modo, quando verrà riaperto le dimensioni dell’etichetta verranno ricalcolate) oppure, molto semplicemente, chiamare il metodo reload passando come parametro il nodo rinominato:
1 2 3 4 5 |
// rinomina il nodo nodo.setUserObject("nuovo nome"); // ricarica il nodo modello.reload(nodo); |
Rimuovere un nodo
Per rimuovere un nodo si utilizzano diversi metodi, sia usando il modello, sia usando direttamente i nodi:
1 2 3 4 |
removeNodeFromParent(MutableTreeNode nodoDaCancellare) removeAllChildren() remove(MutableTreeNode nodoDaCancellare) removeFromParent() |
- removeNodeFromParent va invocato sul modello e rimuove dall’albero il nodo indicato come parametro;
- removeAllChildren rimuove tutti i sotto-nodi del nodo che lo chiama;
- remove rimuove dal nodo chiamante il nodo indicato tra le parentesi;
- removeFromParent rimuove dall’albero il nodo che lo invoca;
1 2 3 4 |
modello.removeNodeFromParent(nodoDaRimuovore); nodo.removeAllChildren(); nodo.remove(nodoDaRimuovore); nodoDaRimuovore.removeFromParent(); |
Esplorare l’albero
Ogni nodo possiede diversi metodi per ottenere altri nodi:
1 2 3 4 5 |
TreeNode getChildAfter(TreeNode nodo) TreeNode getChildAt(int i) TreeNode getChildBefore(TreeNode nodo) TreeNode getParent() Enumeration children() |
- getChildAfter restituisce il sotto-nodo che segue il sotto-nodo passato come parametro;
- getChildAt restituisce l’i-esimo sotto-nodo del nodo che lo invoca;
- getChildBefore restituisce il sotto-nodo che precede il sotto-nodo passato come parametro;
- getParent restituisce il nodo genitore del nodo chiamante;
- children restituisce tutti i sotto-nodi di primo livello del nodo chiamante all’interno di una enumerazione.
Per ottenere i singoli nodi dovrete scorrere nodo per nodo l’enumerazione, come se fosse un iteratore:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// ottengo i nodi figli Enumeration sottonodi = nodoGenitore.children(); // scorro l'enumerazione while (sottonodi.hasMoreElements()) { // ottengo i nodi singolarmente DefaultMutableTreeNode nodo = (DefaultMutableTreeNode)sottonodi.nextElement(); ... ... } |
Rilevare quando l’utente seleziona un nodo dell’albero
Quando l’utente seleziona un qualsiasi nodo dell’albero viene generato un TreeSelectionEvent (classe contenuta nel package javax.swing.event). Per rilevare un TreeSelectionEvent bisogna predisporre l’ascoltatore adeguato (un TreeSelectionListener, sempre nel package javax.swing.event), attraverso, ad esempio, una classe interna anonima:
1 2 3 4 5 6 7 8 9 |
albero.addTreeSelectionListener(new TreeSelectionListener() { public void valueChanged(TreeSelectionEvent eventoDiSelezione) { // codice in risposta all'evento ... ... } }); |
non è ovviamente obbligatorio usare una classe interna anonima, volendo si può anche creare la classe listener in un file a parte, ma visto l’utilizzo limitato che ha, la soluzione con classe interna anonima è senza dubbio la migliore (a meno di esigenze particolari).
Altri metodi utili
Quando si lavora con i nodi, magari può essere utile sapere a che distanza si trova un particolare nodo dalla radice. A questo scopo esiste il metodo getLevel, che restituisce appunto la distanza del nodo che lo invoca dalla radice:
1 |
int distanza = nodo.getLevel(); |
Altre funzionalità sono mostrate nell’esempio completo più sotto. Questo esempio riunisce gran parte di ciò che ho mostrato in questo articolo, introducendo anche alcune cosette nuove. Il programma è composto da una JDialog su cui si trova un JTree e due bottoni, uno per confermare la scelta, l’altro per terminare. Nel JTree viene caricato il contenuto di cartelle e sottocartelle della directory corrente.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 |
import java.awt.BorderLayout; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; import java.io.FileFilter; import java.util.StringTokenizer; import javax.swing.JButton; import javax.swing.JDialog; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTree; import javax.swing.ScrollPaneConstants; import javax.swing.border.TitledBorder; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreeSelectionModel; public class FinestraCercaFile extends JDialog { // bottone di conferma private JButton bottoneOk = null; // albero private JTree albero = null; // modello dell'albero private DefaultTreeModel modelloAlbero = null; // radice dell'albero private DefaultMutableTreeNode nodoRadiceAlbero = null; // percorso del file selezionato private String percorsoSelezionato = null; public FinestraCercaFile() { // titolo finestra this.setTitle("Selezionatore di file " + "--- by Cocco Alessandro"); // imposto la finestra come modale this.setModal(true); // finestra non ridimensionabile this.setResizable(false); // chiudi finestra e libera le risorse quando si clicca sulla x this.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); // gestore di layout this.getContentPane().setLayout(new BorderLayout()); // compongo l'interfaccia della finestra this.getContentPane().add(preparaPannelloAlbero(), BorderLayout.CENTER); this.getContentPane().add(preparaPannelloBottoni(), BorderLayout.SOUTH); // compatto la finestra this.pack(); // centro la finestra rispetto allo schermo this.setLocationRelativeTo(null); // rendo visibile la finestra this.setVisible(true); } private JScrollPane preparaPannelloAlbero() { // creo la radice this.nodoRadiceAlbero = new DefaultMutableTreeNode(""); // istanzio l'albero this.albero = new JTree(this.nodoRadiceAlbero); // ottengo il modello this.modelloAlbero = (DefaultTreeModel) this.albero.getModel(); // impostazioni varie this.albero.setEditable(false); this.albero.setShowsRootHandles(true); this.albero.setRootVisible(false); this.albero.setVisibleRowCount(25); // ascoltatore this.albero.addTreeSelectionListener(new TreeSelectionListener() { public void valueChanged(TreeSelectionEvent e) { // ottengo il nome del nodo selezionato String nomeNodoSelezionato = leggiNomeNodoSelezionato(); // se vale null non ci sono nodi selezionati if (nomeNodoSelezionato == null) { // disattivo il bottone di conferma bottoneOk.setEnabled(false); } else { // ottengo il percorso del nodo selezionato String percorsoDaSpezzettare = albero.getSelectionPath() .toString(); // il percorso del nodo selezionato e' nel formato // [radice, sottonodo1, sottonodo2, ...., sottonodoN] // da questa stringa voglio un percorso del tipo // radice/sottonodo1/sottonodo2/.../sottonodoN // tolgo le parentesi dall'inizio e dalla fine percorsoDaSpezzettare = percorsoDaSpezzettare.substring(1, percorsoDaSpezzettare.length() - 1); // spezzetto la stringa (con la virgola come delimitatore StringTokenizer spezzettatore = new StringTokenizer( percorsoDaSpezzettare, ",", false); // cancello l'eventuale percorso precedente percorsoSelezionato = new String(); // ricompongo il percorso while (spezzettatore.hasMoreTokens()) { percorsoSelezionato += spezzettatore.nextToken().trim() + "/"; } // tolgo la barra finale percorsoSelezionato = percorsoSelezionato.substring(0, percorsoSelezionato.length() - 1); // abilito il bottone di selezione bottoneOk.setEnabled(true); } } }); // popolo l'albero a partire dalla cartella corrente // le sotto-cartelle vengono caricate per ricorsione caricaAlbero(new File("./").listFiles(), nodoRadiceAlbero); // ricarico il contenuto dell'albero modelloAlbero.reload(); // pannello scorrevole per l'albero JScrollPane pannelloScorrevole = new JScrollPane(this.albero, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS); return pannelloScorrevole; } private JPanel preparaPannelloBottoni() { JPanel pannelloSotto = new JPanel(new GridLayout(1, 4)); pannelloSotto.setBorder(new TitledBorder("")); // creo i bottoni bottoneOk = new JButton("Ok"); JButton bottoneAnnulla = new JButton("Annulla"); bottoneOk.setEnabled(false); // ascolatore bottoneOk bottoneOk.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { dispose(); setVisible(false); } }); // ascolatore bottoneAnnulla bottoneAnnulla.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { percorsoSelezionato = null; dispose(); setVisible(false); } }); // le etichette vuote servono per l'impaginazione // (in questo modo posso allineare i bottoni in basso // a destra senza altri pannelli) pannelloSotto.add(new JLabel("")); pannelloSotto.add(new JLabel("")); pannelloSotto.add(bottoneOk); pannelloSotto.add(bottoneAnnulla); return pannelloSotto; } private void caricaAlbero(File[] files, DefaultMutableTreeNode nodoGenitore) { // scorro i file della cartella corrente for (int i = 0; i < files.length; i++) { // se il file in realta' e' una cartella // creo un nuovo nodo e avvio // la ricorsione if (files[i].isDirectory()) { // creo il nuovo nodo DefaultMutableTreeNode nuovoNodo = new DefaultMutableTreeNode( files[i].getName()); // lo aggiungo all'albero modelloAlbero.insertNodeInto( nuovoNodo, nodoGenitore, nodoGenitore.getChildCount()); // chiamo la funzione con i //file contenuti nella cartella corrente caricaAlbero(files[i].listFiles(), nuovoNodo); } else // il file e' effettivamente un file { // aggiungo il nome del file all'albero modelloAlbero.insertNodeInto( new DefaultMutableTreeNode( files[i].getName()), nodoGenitore, nodoGenitore .getChildCount()); } } } public DefaultMutableTreeNode leggiNodoSelezionato() { try { // restituisco il nodo selezionato return (DefaultMutableTreeNode) (albero.getSelectionPath() .getLastPathComponent()); } catch (NullPointerException ex) { // se non si sta selezionando // un nodo, restituisce null return null; } } public String leggiNomeNodoSelezionato() { try { // restituisco il nome del nodo selezionato return leggiNodoSelezionato().toString(); } catch (NullPointerException ex) { // se non si sta selezionando // un nodo, restituisce null return null; } } public String percorsoSelezionato() { return percorsoSelezionato; } public static void main(String[] args) { String percorsoSelezionato = new FinestraCercaFile() .percorsoSelezionato(); System.out.println("percorso selezionato: " + percorsoSelezionato); } } |