Lunch & Learn Visma
Ät en god lunch och lyssna på Lars Roos från Visma Software som tillsammans med oss berättar om hur Visma Business kan hjälpa er att optimera verksamheten.
Westbahr är en komplett IT-leverantör med lång erfarenhet och med ett ständigt fokus på modern teknik, affärer och god kvalité.
Läs mer »Ät en god lunch och lyssna på Lars Roos från Visma Software som tillsammans med oss berättar om hur Visma Business kan hjälpa er att optimera verksamheten.
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:
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?"TableColumnModel, som vet mer om respektive kolumn. Den har i sin tur ett TableColumn-objekt för varje kolumn i tabellen.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.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.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.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.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.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.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.
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:
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:
toString-metoder.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.
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:
TableColumn-objekt och anropa setCellRenderer på detta.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:
getTableCellRendererComponent.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:
