Att tämja tabeller

JTables kan vara riktigt kraftfulla komponenter. För att få ut så mycket som möjligt av dem för minsta möjliga ansträning gäller det att använda dem på rätt sätt.

Swings JTable-komponent presenterar alla former av data i tabellform. Det finns en uppsjö av olika sätt att konstruera dem på. En JTable är egentligen uppbyggd av flera olika delar, som var och en har hand om någon aspekt av tabellens funktion eller utseende:

  • En TableModel, som berättar vilken data som finns i tabellen. Den kan svara på frågor som t ex "hur många rader finns det?", "hur många kolumner finns det?" och "vad är värdet för cellen på rad i och kolumnen j?"
  • En TableColumnModel, som vet mer om respektive kolumn. Den har i sin tur ett TableColumn-objekt för varje kolumn i tabellen.
  • En mängd TableCellRenderer-objekt. Det är dessa som bestämmer hur varje cell ska se ut. Detta gör de genom att returnera en Component, men som jag ska förklara lite vidare längre ner används inte det Component-objektet riktigt som man är van vid.
  • En mängd TableCellEditor-objekt. Dessa fungerar snarlikt TableCellRenderers, men används när användaren tillåts att direkt ändra på ett värde i en cell i tabellen.
  • En ListSelectionModel. Man kan markera rader, kolumner och/eller enskilda celler (beroende på hur man sätter upp den) i en tabell, och det är ListSelectionModel-objektet som lagrar vad som för tillfället är valt och inte.
  • En JTableHeader, som är en separat komponent som visar rubriker för kolumnerna. I normalfallet placerar man alltid en JTable i en JScrollPane, så att man ska kunna bläddra i tabellen om den blir för lång och/eller bred. Och tack vare att huvudet är en egen komponent (som tabellen dessutom själv lägger till som huvud på den JScrollPane den placeras i) kan man bläddra i tabellen utan att rubrikerna följer med.
  • En RowSorter, som har hand om att sortera raderna i en tabell. Man kan tillåta att användaren sorterar om tabellen, oftast genom att klicka på respektive kolumn-rubrik, och då är det RowSortern som sköter detta. Den kan även användas för att låta användaren filtrera vilka rader som visas.
  • Ett TableUI-objekt. Precis som för alla andra Swing-komponenter är själva utseendet på komponenten delegerar till ett separat UI-objekt, som uppför sig olika beroende på vilken LookAndFeel som används.
  • Själva JTable-objektet, som knyter ihop allting.

För att skapa och använda en tabell för de enklaste tillämpningarna behöver man dock inte bekymra sig om alla de här objekten, utan tabellen skapar själv default-objekt. Oftast behöver man inte ens veta om att de här objekten existerar, för nästan alla de metoder man kan vilja anropa finns även i JTable-objektet, som i sin tur delegerar till det objekt som har ansvar för den aktuella uppgiften. Men när man ska anpassa en tabell för sina egna syften kan man göra det genom att byta ut default-alternativen för ett eller flera objekt till egna objekt, och då är det bra att veta var man ska börja.

Hantera så ren data som möjligt i modellen

Ansvaret för själva innehållet i tabellen ligger som sagt hos datamodellen (TableModel-objektet), och den viktigaste frågan den kan svara på är vilket värde som finns i respektive cell.

I en vanlig, mer eller mindre statisk, tabell kan man nöja sig med den existerande DefaultTableModel som tabellen själv skapar om man inte anger något annat. Den lagrar sin data internt i en tvådimensionell struktur, och man kan även lägga till, ta bort eller ändra data genom metoder på objektet.

Oftast har man dock, åtminstone om man skriver en något större applikation, redan en eller flera existerande datamodeller. Det kan röra sig om databaser eller vanliga datastrukturer. Och hellre då än att kopiera data från dessa till en separat datamodell i tabellen, med alla problem det innebär med att hålla dessa två representationerna av samma data konsistenta med varandra, kan man anpassa och använda de datamodeller man redan har. Detta gör man genom att låta någon klass (t ex en inre klass inuti det objekt man har som har hand om datan) implementera TableModel eller ärva från AbstractTableModel. Det är en anpassning som oftast är ganska enkel att göra, då man bara ska lösa följande två huvuduppgifter:

  1. Berätta vilken data som finns.
  2. Skapa och skicka events till alla lyssnare om datan ändras.

Något som är viktigt att tänka på är vad man lagrar och returnerar för typ av data i de olika cellerna. Det är lätt att förledas till att tro att datan ska ligga så nära som möjligt hur den senare ska presenteras, men det är faktiskt inte alls en bra taktik. Det är bättre att låta modellen hantera "ren" data och låta renderarna hantera hur den ska presenteras (vilket vi kommer till mer senare). Några exempel:

  • Om du har data som är någon form av objekt, låt hellre modellen hantera objektet än den sträng det ska representeras med. Låt renderaren ta fram strängrepresentationen istället. Faktum är att de inbyggda renderarna redan gör detta genom att anropa objektens toString-metoder.
  • Om du har data som ska översättas till användarens språk (localization), låt hellre modellen hantera den oöversatta strängen (dvs nyckeln) och låt renderaren ta hand om översättningen. Skulle användaren vilja byta språk mitt under körningen blir det bara då att anpassa renderaren hellre än att behöva byta ut all data i tabellmodellen.
  • Om du har ett objekt som ska representeras med en bild (en ikon), låt hellre modellen hantera själva objekten istället för ikon-objekt, Renderaren får ta ansvaret för att ta fram en lämplig bild för objektet. T ex kanske du har en kolumn med boolesk data och vill representera den med två ikoner, en med "tummen upp" och en med "tummen ner". Lagra då hellre de booleska värden i modellen än själva ikonerna.

Att lagra ren data har flera fördelar. T ex har man helt andra möjligheter vad gäller att sortera rader, och även om man behöver söka efter information i en tabellmodell. Om man tillåter användaren att ändra värden direkt i tabellen är det ännu viktigare, så att man slipper den jobbiga uppgiften att omvandla tillbaka från värdets representation till själva värdet.

Renderarna och deras egenheter

Den utseendemässiga representationen av cellernas värden styrs som sagt av objekt som implementerar TableCellRenderer. Det finns två huvudsakliga sätt att styra vilken renderare som ska användas för respektive kolumn:

  1. Du kan explicit sätta en renderare på en kolumn genom att ta fram dess TableColumn-objekt och anropa setCellRenderer på detta.
  2. Du kan sätta en renderare för en viss klass av data med metoden setDefaultRenderer i JTable. Den renderaren kommer då att användas på alla celler i kolumner som innehåller data av den klassen, om ingen renderare explicit har satts på kolumnobjektet.

Interfacet TableCellRenderer innehåller bara en enda metod:

Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)

Alltså, en metod som givet omständigheterna ställer in och returnerar en vanlig Component, vars utseende styr hur värdet representeras. Detta verkar ju jättebra, komponenter vet vi hur vi hanterar, men här är en stor varning på sin plats:

Den returnerade komponenten kommer inte att läggas till tabellen som vanligen sker när vi använder add!

Anledningen till detta är helt enkelt prestanda. Istället använder tabellen komponenten som en slags "stämpel", dvs den tar komponenten, ber den att rita sig den där den cellen ska vara och går vidare till nästa cell. Detta gör att de inbyggda renderarna kan återanvända samma komponent om och om igen. Gången blir som följer:

  1. Tabellen ber renderaren om en komponent genom att anropa getTableCellRendererComponent.
  2. I ovan nämnda metod tar renderaren sin komponent, sätter korrekta värden på den och returnerar den.
  3. Tabellen ber komponenten att rita ut sig.
  4. Tabellen ber renderaren efter komponenten för nästa cell.
  5. Renderaren tar samma komponent, sätter andra värden på den, och returnerar den.
  6. Osv.

Detta blir då mycket effektivare än om renderarna skulle behöva skapa egna komponenter för varje cell och dessa skulle läggas till på vanligt vis med add, men om man inte känner till att det är så här det fungerar kan man råka ut för oväntade beteenden.

Den förinställda renderarklassen DefaultTableCellRenderer använder en JLabel som komponent. (Faktum är att DefaultTableCellRenderer ärver från JLabel och returnerar sig själv som komponent!) Du kan om du vill ersätta den med en egen klass som implementerar TableCellRenderer och t ex använder en JButton, och då får cellerna utseendet av knappar istället. Men det viktiga här är att cellerna bara ser ut som knappar, men de är inte knappar utan bara "bilder av knappar". Det händer t ex inget om du trycker på dem, och det kommer ingen tjusig hover-effekt när du rör muspekaren över dem. Och du kan lägga hur mycket lyssnare av olika slag som du vill på dina komponenter, t ex MouseListeners, men det kommer ändå inte att hända något. Vilket alltså beror på att det inte är komponenterna som ligger i tabellen, det är bara bilder av dem.

Det enda undantaget är om du anropar setToolTipText på den komponent som du returnerar. Detta beror på att tabellen alltid frågar den returnerade komponenten efter dess tool tip-text och lagrar den, så att den kan använda den som tool tip-text på sig själv när muspekaren är över just den delen av tabellen som motsvarar den cellen.

Man kan få knappar och andra aktiva komponenter som faktiskt uppför sig som de ska som celler i en tabell, men då får man bl a börja blanda in TableCellEditors, och det får vi avhandla någon annan dag.

Som ett litet exempel skapade jag en väldigt enkel tabell över några anställda vid Westbahr, där jag i tre kolumner listade deras namn (en String), hobbies (en array av String) och huruvida de är vegetarianer eller inte (Boolean). Rakt upp och ner, med bara de inbyggda renderarna, såg den ut så här:

I fallet med arrayen blev inte resultatet så bra, då dess toString-metod inte returnerar något visningsbart. Så jag lade till en specialskriven renderare som istället utnyttjar den toString-metod som finns i klassen Arrays:

        table.setDefaultRenderer(
                String[].class,
                new DefaultTableCellRenderer() {
                    
                    private static final long serialVersionUID = 1L;
                    
                    @Override
                    public Component getTableCellRendererComponent(
                            JTable table,
                            Object value,
                            boolean isSelected,
                            boolean hasFocus,
                            int row,
                            int column) {
                        return super.getTableCellRendererComponent(
                                table,
                                Arrays.toString((String[]) value),
                                isSelected,
                                hasFocus,
                                row,
                                column);
                    }
                    
                });

Jag passade också på att göra om "Vegetarian"-kolumnen till att visa värdet genom att ändra sin bakgrundsfärg:

        table
                .getColumnModel()
                .getColumn(2)
                .setCellRenderer(new DefaultTableCellRenderer() {
                    
                    private static final long serialVersionUID = 1L;
                    
                    @Override
                    public Component getTableCellRendererComponent(
                            JTable table,
                            Object value,
                            boolean isSelected,
                            boolean hasFocus,
                            int row,
                            int column) {
                        Component result =
                                super.getTableCellRendererComponent(
                                        table,
                                        null,
                                        isSelected,
                                        hasFocus,
                                        row,
                                        column);
                        setBackground(((Boolean) value)
                                ? Color.GREEN
                                : Color.RED);
                        return result;
                    }
                    
                });

Och då blir resultatet så här:

Om bloggaren

Marcus Björkander

Marcus Björkander

Schlagernörd och småbarnspappa med tvångstankar som jobbar som utvecklare på Westbahr i Göteborg. Favoritspråket är Java, som han tidigare har undervisat i vid Chalmers i många år.

 

Nyckelord/tag moln