© 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: | |||||||||
| |||||||||
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: | |||||||||
|
|||||||||
Ud over dette, er der også en række iconer som angiver om en knude har børn eller er et leaf (dk.: blad): | |||||||||
| |||||||||
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: | |||||||||
| |||||||||
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: | |||||||||
| |||||||||
[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: | |||||||||
| |||||||||
på 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: | |||||||||
| |||||||||
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.: | |||||||||
| |||||||||
Men det er selvfølgelig en oplagt grim løsning. | |||||||||
Følgende lille svindelnummer, over for konstruktoren i DefaultTreeModel er bedre: | |||||||||
| |||||||||
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: | |||||||||
|
|||||||||
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: | |||||||||
| |||||||||
Når vi laver en TreeSelectionListener skal den tilmeldes JTree med: | |||||||||
| |||||||||
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: | |||||||||
| |||||||||
på 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: | |||||||||
| |||||||||
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: | |||||||||
| |||||||||
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: | |||||||||
| |||||||||
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: | |||||||||
| |||||||||
Resultatet bliver følgende når framen instantieres: | |||||||||
Figur 10: Initiel sti |
| ||||||||
| |||||||||
Java's default eksempel: | |||||||||
Eksempler på listeners: | |||||||||