© 1999-2003, Flemming Koch Jensen
Alle rettigheder forbeholdt
Træstrukturer

Opgaver

Forudsætninger:

At man har læst kapitlet Tabeller, er bekendt med MVC Pattern og har et grundlæggende kendskab til træer

 

 

 

1. Hvilke træstrukturer?

  Umiddelbart kan det være vanskeligt at gætte hvad træstrukturer er, når vi taler grafiske brugergrænseflader, med mindre man lige ved det. Et velkendt eksempel finder man i Windows Explorer (kaldet "Stifinder" på dansk) — ikke Internet Explorer, men den der bruges til at arbejde med directories og filer:
Figur 1:
Udsnit af Windows Explorer
  Vi har her sat en rød ramme om det der er selve træstrukturen.
  I termer af MVC Pattern kan man kalde det indrammede for view og det bagved liggende filesystem modellen. Modellen er et "rigtigt" træ, mens view blot er en måde at illustrere træet.
 

 

2. Java's default eksempel

  Ligesom tabeller i Swing laves med klassen JTable, sådan laves træstrukturer med en klasse der hedder JTree. Inden vi begynder at se på JTree og de klasser den arbejder sammen med, vil vi først studere et simplet eksempel, for at få en forståelse af hvad træstrukturer mere præcist er i forbindelse med Swing.
  Pudsigt nok får man et default eksempel på en træstruktur, hvis man blot laver en instans af JTree:
import java.awt.event.*;
import javax.swing.*;

public class JTreeFrame extends JFrame {
  
  public JTreeFrame( String title ) {
    super( title );

    JTree træ = new JTree();
    
    getContentPane().add( new JScrollPane( træ ) );
    
    setDefaultCloseOperation( EXIT_ON_CLOSE );
    
    pack();
    setVisible( true );
  }
}
  Vi har her anvendt default-konstruktoren, og placeret instansen i en JScrollPane — resultatet bliver:
Figur 2:
Default eksempel
  Vi observerer det samme højdeproblem som for tabeller, når man placerer et JTree i en JScrollPane.
  Havde vi i stedet undladt JScrollPane'n havde vi fået:
Figur 3:
Knuden
sports åbnet
 

 

2.1 Åbne og lukkede knuder

  Denne måde at illustrere træer på, egner sig ikke specielt godt til træer i almindelighed. Den væsentligste egenskab ved denne form for view, er at man kan åbne og lukke knuder rent view-mæssigt. Hvis en knude er lukket kan man ikke se dens børn, modsat hvis knuden er åben.
  Man åbner og lukker knuder ved at klikke på dem med musen, og da view'et derfor udvider sig rent visuelt, vil vi holde os til at bruge en JScrollPane:
  Lad os klikke på sports:
Figur 4:
Knuden
sports åbnet
  sports er nu en åben knude, og man kan se dens børn.
  I relation til MVC Pattern er der nærmest tale om en Document-View variant, i den forstand at interaktionen foregår direkte med den visuelle repræsentation — vi klikker i view. Samtidig adskiller det sig lidt fra Document-View varianten, da det ikke er modellen vi påvirker, men selve view. Når vi klikker på en knude, og henholdsvis åbner og lukker den, ændrer det ikke modellen — knuderne er stadig de samme, men vi ændre på hvilke knuder view vælger at vise. Dette er derfor et eksempel på den relation, der kan være mellem controller og view; hvor controller konfigurerer view.
  Man bemærker de små figurer ud for knuder med børn, som indikerer om en knude er åben eller lukket:
 
Lukket
Åben
  Ud over dette, er der også en række iconer som angiver om en knude har børn eller er et leaf (dk.: blad):
Lukket knude
Åben knude
Leaf
Det kan måske virke lidt underligt, at vi har anfører det samme icon to gange for både lukket og åben knude, men det skyldes at man sætter disse individuelt når man laver sine egne iconer. I Java's default eksempel, har man blot valgt det samme icon for de to tilstande.
Lad os åbne resten af knuderne og resize framen så vi kan se dem alle:
Figur 5:
Alle knuder åbne, og frame manuelt resized
  Som sagt er det lidt pudsigt at JTree's default-konstruktor giver os dette eksempel med farver, sportsgrene og madretter, og man skal ikke lede efter andre Swing komponenter med samme særhed — det er kun forfatteren til JTree, der har gjort det.
 

 

3. Modellen

  Lad os rette en oplagt fejl i Java's default eksempel — det er ikke på dansk. Vi vil derfor lave en model med det samme eksempel på dansk:
Figur 6:
Dansk udgave af Java's default eksempel
  Vi kan lave ovenstående danske udgave med følgende frame-klasse:
import javax.swing.*;
import javax.swing.tree.*;

public class VorJTree extends JTree {
  
  public VorJTree() {
    DefaultMutableTreeNode rod = new DefaultMutableTreeNode( "JTræ" );

      DefaultMutableTreeNode farver = new DefaultMutableTreeNode( "Farver" );
      rod.add( farver );
  
        farver.add( new DefaultMutableTreeNode( "blå" ) );
        farver.add( new DefaultMutableTreeNode( "violet" ) );
        farver.add( new DefaultMutableTreeNode( "rød" ) );
        farver.add( new DefaultMutableTreeNode( "gul" ) );

      DefaultMutableTreeNode sport = new DefaultMutableTreeNode( "Sport" );
      rod.add( sport );
  
        sport.add( new DefaultMutableTreeNode( "basketball" ) );
        sport.add( new DefaultMutableTreeNode( "fodbold" ) );
        sport.add( new DefaultMutableTreeNode( "amerikansk fodbold" ) );
        sport.add( new DefaultMutableTreeNode( "ishockey" ) );

      DefaultMutableTreeNode mad = new DefaultMutableTreeNode( "Mad" );
      rod.add( mad );
  
        mad.add( new DefaultMutableTreeNode( "hotdogs" ) );
        mad.add( new DefaultMutableTreeNode( "pizza" ) );
        mad.add( new DefaultMutableTreeNode( "ravioli" ) );
        mad.add( new DefaultMutableTreeNode( "bananer" ) );
  
    setModel( new DefaultTreeModel( rod ) );
  }
}
  Vi anvender her to klasser fra javax.swing.tree: DefaultMutableTreeNode og DefaultTreeModel.
 

 

3.1 DefaultMutableTreeNode

  Selve træet opbygges med instanser af DefaultMutableTreeNode - et forfærdelig langt klassenavn — specielt da der ikke findes nogen tilsvarende klasse: DefaultImmutableTreeNode (se evt. immutable). Vi anvender her set-konstruktoren:
DefaultMutableTreeNode( Object obj )
  [Jeg har ingen erfaringer med at bruge andet end en String som aktuel parameter. Jeg vil antage, at der analogt til tabeller, bliver taget en toString på objektet når det skal "vises"]
  Hver knude er en container der kan have en reference til et vilkårligt antal børn. Her anvendes add-metoden:
void add( MutableTreeNode child )
  DefaultMutableTreeNode.
DefaultMutableTreeNode er en subklasse af interfacet MutableTreeNode, som igen er en subklasse af interfacet TreeNode. Alle disse interface's erklærer en række container-metoder:
Figur 7:
TreeNode klasserne
  DefaultMutableTreeNode's metoder er ikke vist i figuren, da deres antal er betydeligt (ca. 50). Dog er der en vi vil nævne, som supplerer de to interfaces metoder, nemlig:
Object getUserObject()
  Sammen med metoderne i de to interfaces, gør det DefaultMutableTreeNode til en velegnet kandidat til generel knude-klasse når man arbejder med træer i stort set enhver sammenhæng - ikke kun i forbindelse med Swing.
  Vi vil ikke her begynde at gennemgå de utallige metoder — de fleste af dem er intuitive, og resten kan man slå op.
  Bemærk at en knudes børn har et index (fra 0 og fremefter), der kan bruges til at referere til det i forbindelse med flere af metoderne.
 

 

3.2 DefaultTreeModel

  DefaultTreeModel realiserer også et interface:
Figur 8:
TreeModel klasserne
  Igen er antallet af metoder stort — DefaultTreeModel har ca. 25 metoder!
  Metoderne har ikke alle lige meget med selve Swing at gøre, men giver mange muligheder for at manipulere modellen.
   
  Fremgangsmåden, når man opbygger en model er derfor den, at man konstruerer træet med DefaultMutableTreeNode's og dens add-metode. Man holder fast i roden, som man giver til en DefaultTreeModel, som man sætter som model på ens JTree, med setModel-metoden.
 

 

3.3 Subklasse DefaultTreeModel

  En eller anden kunne måske få den oplagte idé at subklasse DefaultTreeModel. På den måde skulle vi blot kalde setModel-metoden på vores JTree, med en instans af vores model-klasse.
  Problemet er bare, at det er ikke så enkelt. Problemet er at DefaultTreeModel ikke har nogen default-konstruktor. Det gør det vanskeligt når man vil opbygge træet i konstruktoren (eller en fabriksmetode). DefaultTreeModel har to konstruktorer der begge kræver at få roden i træet, men da vi ikke har lavet træet endnu kan det jo reelt ikke lade sig gøre (Hvis man forsøger at give dem null, som aktuel parameter, får man blot en exception smidt tilbage).
  Én måde at løse problemet på, er at gøre fabriksmetoden statisk. F.eks.:
import javax.swing.*;
import javax.swing.tree.*;

public class DanskModel extends DefaultTreeModel {
  
  public DanskModel() {
    super( buildTree() );
  }
  
  private static DefaultMutableTreeNode buildTree() {
    DefaultMutableTreeNode rod = new DefaultMutableTreeNode( "JTræ" );
  
      DefaultMutableTreeNode farver = new DefaultMutableTreeNode( "Farver" );
      rod.add( farver );
    
      ...
    
    return rod;
  }
}
  Men det er selvfølgelig en oplagt grim løsning.
Følgende lille svindelnummer, over for konstruktoren i DefaultTreeModel er bedre:
import javax.swing.*;
import javax.swing.tree.*;

public class DanskModel extends DefaultTreeModel {
  
  public DanskModel() {
    super( new DefaultMutableTreeNode( "dummy-root" ) );
    
    setRoot( buildTree() );
  }
  
  private DefaultMutableTreeNode buildTree() {
    DefaultMutableTreeNode rod = new DefaultMutableTreeNode( "JTræ" );
  
      DefaultMutableTreeNode farver = new DefaultMutableTreeNode( "Farver" );
      rod.add( farver );
    
      ...
    
    return rod;
  }
}
  Her giver man konstruktoren en instans af DefaultMutableTreeNode — så er den tilfreds — men inden denne rod nogensinde bliver brugt til noget, erstatter man den med den rigtige, som man får fra fabriksmetoden.
  Det giver ikke tilsvarende udfordringer at subklasse DefaultMutableTreeNode; hvilket kan være bekvemt.
 

 

4. Eventhåndtering

  Eventhåndtering fungerer på sædvanlig vis med listeners i et Observer Pattern.
  Der er fire listeners i forbindelse med JTree:
 
TreeSelectionListener Modtager besked om ændringer i hvad der er valgt i træet
TreeModelListener Modtager besked om ændringer i modellen
TreeExpansionListener Modtager besked om ændringer i hvilke knuder der er åbne og lukkede
TreeWillExpandListener Modtager besked lige før der sker ændringer i hvilke knuder der er åbne og lukkede
  Disse er på vanlig vis fire interfaces i javax.swing.event.
  Der dog en væsentlig ting som vi skal have på plads inden vi ser nærmere på disse fire listeners, nemlig stier i JTree.
 

 

4.1 Stier

  [Man kan nøjes med det der står i det næste afsnit om TreeSelectionListener, men jeg kan skrives mere om TreePath-klassen ved lejlighed]
 

 

4.2 TreeSelectionListener

  TreeSelectionListener interfacet erklærer én metode:
void valueChanged( TreeSelectionEvent e )
  Når vi laver en TreeSelectionListener skal den tilmeldes JTree med:
void addTreeSelectionListener( TreeSelectionListener listener )
  Lad os starte med den simpleste situation: Vi ønsker at modtage besked, hver gang brugeren klikker på et blad i vores træ. Hvordan finder vi ud af hvilket blad der er blevet klikket på?
  Her er det en nærliggende tanke, at vi som listener kalder getSource på den event som vi modtager, men så enkelt er det ikke. Det objekt som getSource returnerer vil skuffende nok være selve instansen af JTree; hvor event'en er sket, ikke den DefaultMutableTreeNode som vi har klikket på.
  Det er her vi får brug for vores viden om TreePath. Vi kan få hele stien til det blad vi har klikket på ved at kalde:
TreePath getPath()
  TreeSelectionEvent'en
  På denne TreePath kan vi så kalde getLastPathComponent, og vi har endelig instansen af DefaultMutableTreeNode
  Lad os se et eksempel:
  Følgende TreeSelectionListener vil udskrive navnet på et blad når man klikker på det:
import javax.swing.event.*;
import javax.swing.tree.*;

public class PrintListener implements TreeSelectionListener {
  
  public void valueChanged( TreeSelectionEvent e ) {
    TreePath path = e.getPath();

    DefaultMutableTreeNode node =
      (DefaultMutableTreeNode) path.getLastPathComponent();
    
    System.out.println( node );
  }
}
 

 

4.3 TreeModelListener

  <Må vente til senere>
 

 

4.4 TreeExpansionListener

  <Må vente til senere>
 

 

4.5 TreeWillExpandListener

  <Må vente til senere>
 

 

5. Rendere

  Lad os se de klasser der vedrører rendering af træer:
Figur 9:
TreeCell-Renderer klasserne
  Der er her grundlæggende tale om det samme design som for tabeller (sammenlign evt. med den tilsvarende figur i kapitlet om Tabeller)
 

 

5.1 DefaultTreeCellRenderer

  Det mest almindelig er at man anvender en DefaultTreeCellRenderer. Specielt anvender man ofte de tre setIcon-metoder. Man bemærker her muligheden for at sætte forskellige iconer for åbne og lukkede knuder, som blev omtalt ovenfor i forbindelse med Java's default eksempel.
  Som oftest vil man hente iconerne fra gif-filer, og det er i den forbindelse nyttigt at anvende instanser af ImageIcon, som realiserer Icon interfacet.
  Når man har lavet en instans af DefaultTreeCellRenderer, og sat de tre ikoner, bruger man setCellRenderer-metoden på JTree til at sætte renderen.
 

 

6. Editorer

  <Må vente til senere>
 

 

7. JTree

Vi har i de foregående afsnit anvendt flere af JTree's metoder, og vi vil i dette afsnit se på nogle af de muligheder vi endnu ikke har berørt.

 

7.1 Sætte en sti

Det er muligt at sætte en sti for JTree. Det kunne f.eks. være en initiel sti; hvilket vil sige, at view'et åbner træstrukturen ind til den pågældede knude, og markerer den.
Man gør dette ved at anvende følgende metode på JTree:
void setSelectionPath( TreePath path )
Hvis vi f.eks. ønskede at vores danske udgave af default eksemplet skulle åbnes ind til bananer, kunne det gøres med følgende ændring af slutningen på VorJTree's konstruktor:
      ...
			  
      mad.add( new DefaultMutableTreeNode( "hotdogs" ) );
      mad.add( new DefaultMutableTreeNode( "pizza" ) );
      mad.add( new DefaultMutableTreeNode( "ravioli" ) );
      DefaultMutableTreeNode bananer = new DefaultMutableTreeNode( "bananer" );
      mad.add( bananer );

  setModel( new DefaultTreeModel( rod ) );
  
  addTreeSelectionListener( new PrintListener() );
  
  DefaultMutableTreeNode initialPath[] = new DefaultMutableTreeNode[3];
  initialPath[0] = rod;
  initialPath[1] = mad;
  initialPath[2] = bananer;
  
  setSelectionPath( new TreePath( initialPath ) );
}
Her opbygger vi et array med detre knuder der udgør stien ud til bananer, og benytter en af TreePath's konstruktorer, der tager et array af knuder som parameter:
TreePath( Object[] path )
Resultatet bliver følgende når framen instantieres:
Figur 10:
Initiel sti

Java's default eksempel:

JTreeFrame.java
Test.java

Eksempler på listeners:

PrintListener.java