Tabeller

Opgaver

 

 

 

1. Introduktion

  Inden vi mere systematisk gennemgår mulighederne for at lave tabeller i Swing, vil vi først se nogle simple eksempler, der viser hvad der er idéen med tabeller.

 

1.1 Eksempel: Den lille tabel

 

Først vil vi se et eksempel der er taget fra de første skoleår, hvor den lille tabel stod på programmet.

Figur 1:
Den lille tabel

 

Kildeteksten til eksemplet er beskeden i omfang:

import javax.swing.table.*;

public class LilleTabelModel extends AbstractTableModel {

  public int getColumnCount() {
    return 10;
  }

  public int getRowCount() {
    return 10;
  }

  public Object getValueAt( int row, int col ) {
    return new Integer( (row+1) * (col+1) );
  }
}
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class LilleTabelFrame extends JFrame {

  public LilleTabelFrame( String title ) {
    super( title );
    
    LilleTabelModel tableModel = new LilleTabelModel();

    JTable table = new JTable( tableModel );

    getContentPane().add( table, BorderLayout.CENTER );
    
    setDefaultCloseOperation( EXIT_ON_CLOSE );
    pack();
    setVisible( true );
  }
}
Betegnelsen AbstractTableModel leder tanken hen på MVC Pattern, og Swing bruger da også Document-View varianten til at implementere tabeller. Swings JTable er View-klassen, mens AbstractTableModel er Document-klassen (som vi dog vil kalde modellen i det følgende).
Vi har i LilleTabelModel implementeret tre metoder som view (JTable) baserer sig på.
Først er der:
int getRowCount()
int getColumnCount()
der giver tabellens dimensioner i antal rækker (eng.: rows) og kolonner (eng. columns).
Dernæst er der:
Object getValueAt( int row, int col )
der giver tabellens indhold for den givne række og kolonne.
Man skal implementere disse tre metoder, da de er abstrakte i AbstractTableModel.
Ud fra de oplysninger JTable kan hente fra disse metoder opbygger den sit view.

 

1.2 Eksempel: Porto

Selvom Swing bruger Document-View varianten af MVC Pattern er der mulighed for at bruge én form for controllere, nemlig scrollbars - ja, det nærmest forventes at man gør det!
Lad os se et eksempel på en tabel med porto for almindelige brevforsendelser.

Figur 2:
Tabel med scrollbar

 

Det første man bemærker er naturligvis scrollbaren, men ellers er det kolonnenavne A til G som vækker opmærksomhed.
Lad os se kildeteksten til eksemplet:
import javax.swing.table.*;

public class PortoTabelModel extends AbstractTableModel {

  private int vægt[] =
    {   20,   50,    100,   250,   500,   1000,   1500,   2000 };
  private double danmark[] =
    { 4.00, 5.25,   5.75,  9.75, 17.00,  21.00,  28.00,  30.00 };
  private double færøerne[] =
    { 4.50, 6.75,   8.00, 13.00, 21.00,  27.00,  55.00,  55.00 };
  private double grønland[] =
    { 4.50, 7.50,  10.00, 22.00, 38.00,  65.00, 105.00, 105.00 };
  private double norden[] =
    { 4.50, 6.75,   8.00, 13.00, 21.00,  49.00,  90.00,  90.00 };
  private double europa[] =
    { 4.50, 9.75,  13.00, 22.00, 38.00,  65.00, 105.00, 105.00 };
  private double øvrigt[] =
    { 5.50, 12.25, 18.00, 36.00, 60.00, 110.00, 185.00, 185.00 };
  
  public int getColumnCount() {
    return 7;
  }

  public int getRowCount() {
    return vægt.length;
  }

  public Object getValueAt( int row, int col ) {
    switch ( col ) {
      case 0: return new Integer( vægt[row] );
      case 1: return new Double( danmark[row] );
      case 2: return new Double( færøerne[row] );
      case 3: return new Double( grønland[row] );
      case 4: return new Double( norden[row] );
      case 5: return new Double( europa[row] );
      case 6: return new Double( øvrigt[row] );
      
      default: return null;
    }
  }
}
import javax.swing.*;
import java.awt.*;

public class PortoTabelFrame extends JFrame {

  public PortoTabelFrame( String title ) {
    super( title );
    
    PortoTabelModel tableModel = new PortoTabelModel();

    JTable table = new JTable( tableModel );

    JScrollPane pane = new JScrollPane( table );

    getContentPane().add( pane, BorderLayout.CENTER );
    
    setDefaultCloseOperation( EXIT_ON_CLOSE );
    setSize( 500, 150 );
    setVisible( true );
  }
}
Man spejder forgæves efter hvad det er, der får kolonne-navnene til at dukke op. Om kolonne-navnene er der, eller ej, er en af særhederne ved Swing. Er der en scrollbar, så er der også kolonnenavne - ellers ikke! Dvs. det er en ydre omstændighed, der afgør om kolonne-navnene vises, ikke noget der afgøres i viewet (JTable).
Personligt finder jeg det ikke helt tilfredsstillende, at man ikke kan få kolonnenavne med mindre man har en scrollbar, men man kan lige så godt vende sig til det først som sidst - vil man have "ordentlige" tabeller i Swing så skal de altid i en JScrollPane.

 

1.2.1 Kolonnenavne

Kolonnenavnene A til G er default-navne - navne der bruges med mindre vi selv indfører nogen. Default-navnene er inspireret af de kolonne-navne, der er default i regneark.
Ønsker vi at indføre vores egne kolonnenavne skal vi override følgende metode:
String getColumnName( int col )
som vores model arver fra AbstractTableModel.
Hvis vi f.eks. gerne vil have følgende kolonnenavne i vores eksempel med porto:

Figur 3:
Vores egne kolonne-navne

 

Gøres det med følgende ændringer i PortoTabelModel:

import javax.swing.table.*;

public class PortoTabelModel extends AbstractTableModel {
  
  private String navne[] = 
    { "Vægt", "Danmark", "Færøerne", "Grønland",
      "Norden", "Europa", "Øvrigt" };
  
  private int vægt[] = 
    {   20,   50,    100,   250,   500,   1000,   1500,   2000 };
  private double danmark[] = 
    { 4.00, 5.25,   5.75,  9.75, 17.00,  21.00,  28.00,  30.00 };
  private double færøerne[] = 
    { 4.50, 6.75,   8.00, 13.00, 21.00,  27.00,  55.00,  55.00 };
  private double grønland[] = 
    { 4.50, 7.50,  10.00, 22.00, 38.00,  65.00, 105.00, 105.00 };
  private double norden[] = 
    { 4.50, 6.75,   8.00, 13.00, 21.00,  49.00,  90.00,  90.00 };
  private double europa[] = 
    { 4.50, 9.75,  13.00, 22.00, 38.00,  65.00, 105.00, 105.00 };
  private double øvrigt[] = 
    { 5.50, 12.25, 18.00, 36.00, 60.00, 110.00, 185.00, 185.00 };
  
  public String getColumnName( int col ) {
    return navne[col];
  }
  
  public int getColumnCount() {
    return 7;
  }

  public int getRowCount() {
    return vægt.length;
  }

  public Object getValueAt( int row, int col ) {
    switch ( col ) {
      case 0: return new Integer( vægt[row] );
      case 1: return new Double( danmark[row] );
      case 2: return new Double( færøerne[row] );
      case 3: return new Double( grønland[row] );
      case 4: return new Double( norden[row] );
      case 5: return new Double( europa[row] );
      case 6: return new Double( øvrigt[row] );
      default: return null;
    }
  }
}

 

2. TableCellRenderer

Det er en TableCellRenderer, som bestemmer hvordan hver enkelt celle skal se ud. Vi skal senere se, at en TableCellRenderer er en fabrik der producerer det, der bliver vist i en celle, men i første omgang vil vi se en TableCellRenderer som et JComponent.
Når man laver sin egen TableCellRenderer nedarver man normalt fra DefaultTableCellRenderer, der realiserer TableCellRenderer interfacet og er en subklasse til JLabel:

Figur 4:
TableCell-
Renderer
-
klasserne

 

Vi har her kun vist de mest relevante metoder fra de implicerede klasser. JLabel indeholder som bekendt betydelig flere metoder, som kan bruges til at indstille udseendet, her er blot vist de tre mest anvendte.
Når man laver en TableCellRenderer består opgaven først i at override setValue i vores subklasse til DefaultTableCellRenderer, og dernæst i at få fat i den relevante kolonne fra JTable og give TableCellRenderer'en til denne kolonne med setCellRenderer.
Lad os se et eksempel:

 

2.1 Eksempel: class KommatalsRenderer

Vi vil lave en TableCellRenderer, som løser en ofte forekommende problem: At skulle vise beløb, eller andre kommatal, med et vist antal decimaler efter kommaet.
Først er der modellen, der er upåvirket af dette problem:
import javax.swing.table.*;
import java.util.*;

public class KommaTabelModel extends AbstractTableModel {

  private double[][] kommatal;
  private int rows, columns;
  
  public KommaTabelModel( int rows, int columns ) {
    this.rows = rows;
    this.columns = columns;
    
    kommatal = new double[rows][columns];
    
    Random rnd = new Random();
    
    for ( int r=0; r<rows; r++ )
      for ( int c=0; c<columns; c++ )
        kommatal[r][c] = 200 * rnd.nextDouble() - 100;
  }
  
  public String getColumnName( int col ) {
    return "" + col;
  }
  
  public int getColumnCount() {
    return columns;
  }

  public int getRowCount() {
    return rows;
  }

  public Object getValueAt( int row, int col ) {
    return new Double( kommatal[row][col] );
  }
}

Vi har her valgt en model, som består af et to-dimensionalt array af doubles, der repræsenterer tilfældige reelle tal i intervallet ]-100:100[.

 

Dernæst kommer det mest interessante - selve vores TableCellRenderer:
import javax.swing.table.*;
import java.awt.*;

public class KommatalsRenderer extends DefaultTableCellRenderer {

  private int decimaler;
  
  public KommatalsRenderer( int decimaler ) {
    this.decimaler = decimaler;
    
    setHorizontalAlignment( RIGHT );
  }

  public void setValue( Object tal ) {
    
    if ( tal instanceof Double ) {
      double værdi = ((Double) tal).doubleValue();
      
      // om det er et negativt beløb
      
      if ( værdi < 0 )
        setForeground( Color.red );
      else
        setForeground( Color.blue );
      
      // cifre der skal skrives
      
      String cifre = "" + værdi;
      
      if ( cifre.indexOf( '.' ) < 0 )
        cifre += ".";
      
      while ( cifre.indexOf( '.' ) + decimaler + 1 > cifre.length() )
        cifre += "0";
      
      while ( cifre.indexOf( '.' ) + decimaler + 1 < cifre.length() )
        cifre = cifre.substring( 0, cifre.length()-1 );
      
      setText( cifre );
    } else
      setText( "No Double" );
  }
}
Vi bruger konstruktoren til at indstille antallet af decimaler vi ønsker efter kommaet, og sætter alignment til højre (Bemærk, at vi ikke behøver at skrive JLabel.RIGHT, da vores klasse har JLabel som en af sine super-klasser).
Metoden setValue indeholder kernen i dette eksempel. Efter at have konstateret, at der er tale om en double, gør vi to ting.
Først sætter vi forgrundsfarven, så den bliver rød ved negative tal, ellers blå.

Dernæst bearbejder vi en tekststreng, så den indeholder en tekstuel repræsentation af værdien med det rigtige antal decimaler efter kommaet (vi vil ikke her gennemgå hvordan, da det falder udenfor emnet). Vi bruger JLabel's setText til at vise det resultat vi når frem til.

 

Det er i framen, vi sætter vores KommatalsRenderer til de enkelte kolonner i tabellen:
import javax.swing.*;

public class KommaTabelFrame extends JFrame {

  public KommaTabelFrame( String title ) {
    super( title );
    
    KommaTabelModel tableModel = new KommaTabelModel( 40, 8 );

    JTable tableView = new JTable( tableModel );
    
    // Renderer
    for ( int i=0; i<tableModel.getColumnCount(); i++ )
      tableView.getColumn( ""+i ).setCellRenderer( new KommatalsRenderer( 2 ) );
    
    JScrollPane pane = new JScrollPane( tableView );

    getContentPane().add( pane );
    
    setDefaultCloseOperation( EXIT_ON_CLOSE );
    pack();
    setVisible( true );
  }
}

Bemærk, at vi her anvender de kolonne-navnene som vi selv har indført med metoden getColumnName i KommaTabelModel, når vi kalder getColumn på vores JTable.

 

Resultatet bliver:

Figur 5:
Anvendelse af Kommatals-
Renderer

 

 

2.2 getTableCellRendererComponent

Som vi gjorde ovenfor, plejer man at lave en TableCellRenderer ved at nedarve fra DefaultTableCellRenderer, men man kan naturligvis selv realisere TableCellRenderer interfacet - det har trods alt kun én metode.
public interface TableCellRenderer {
  Component getTableCellRendererComponent(
                        JTable table, Object value,
					    boolean isSelected, boolean hasFocus, 
					    int row, int column);
}
Hvis man gør det, vil man dog opdage hvor rodet DefaultTableCellRenderer's implementation er på dette punkt. getTableCellRendererComponent-metoden er en fabriks-metode!
Realiseringen i DefaultTableCellRenderer er ikke nogen fabriks-metode, da den slutter med linien:
return this;
før denne linie er der en lang række linier der indstiller JLabel'et og bla. et polymorft kald af setValue - metoden som vi har overrided ovenfor.
Designet bliver dermed et rodsammen af en fabriks-metode og noget prototype-lignende, der alligevel ikke rigtig er det.
Fordelen ved at lave sin egen implementation af TableCellRenderer er, at man kan anvende andre grafiske komponenter til at vise indholdet af tabellens celler, i stedet for at være begrænset til at bruge JLabel's

 

3. TableCellEditor

Når man vil lave en TableCellEditor, til at ændre værdier i en tabels celler, skal man ikke til at lave nye klasser (med mindre man vil lave meget specielle editorer).
Modellen skal indrettes efter, at der er en TableCellEditor til visse af kolonnerne, og framen skal sætte en TableCellEditor til de kolonner de vedrører.
En TableCellEditor får man ved at lave en instans af DefaultCellEditor. Konstruktoren tager som parameter enten et JTextField, en JCheckBox eller en JComboBox.
Lad os se et eksempel:

 

3.1 Eksempel: Reservation af Auditoriet

Vi vil lave en tabel der har form af et skema. Det skal være et skema der dækker alle hverdage for et bestemt lokale: Auditoriet.
Skemaet får følgende udseende:

Figur 6:
Reservations-skema

 

Vi ønsker at kunne ændre status for de enkelte lektioner ved at vælge mellem forskellige muligheder fra en combobox, når man klikker på den pågældende lektion.

Vi opnår dette ved at knytte en TableCellEditor til de fem af kolonnerne og giver instansen af DefaultCellEditor en JComboBox der har valgmulighederne.

 

Modellen bliver som følger:
import javax.swing.table.*;

public class SkemaTabelModel extends AbstractTableModel {

  public static final String[] TILSTANDE =
    { "Ledig", "1.sem", "2.sem", "3.sem", "4.sem", "Valgfag" };

  private String[][] skema;
  
  public static final String[] DAGE = {
    "Mandag",
    "Tirsdag",
    "Onsdag",
    "Torsdag",
    "Fredag",
  };
  
  private String[] tider = {
    "08:10-08:55",
    "09:05-09:50",
    "10:10-10:55",
    "11:05-11:50",
    "12:10-12:55",
    "13:05-13:50"
  };
  
  public SkemaTabelModel() {
    skema = new String[DAGE.length][tider.length];
    
    for ( int dag=0; dag<DAGE.length; dag++ )
      for ( int lektion=0; lektion<tider.length; lektion++ )
        skema[dag][lektion] = TILSTANDE[0];
  }
  
  public String getColumnName( int col ) {
    if ( col > 0 )
      return DAGE[col-1];
    else
      return "Lektion";
  }
  
  public int getColumnCount() {
    return DAGE.length + 1;
  }

  public int getRowCount() {
    return tider.length;
  }
  
  public boolean isCellEditable( int row, int col ) {
    return col > 0;
  }
  
  public void setValueAt( Object value, int row, int col ) {
    if ( col > 0 )
      skema[col-1][row] = (String) value;
  }

  public Object getValueAt( int row, int col ) {
    if ( col > 0 )
      return skema[col-1][row];
    else
      return tider[row];
  }
}
Her er to arrays gjort til public klasse-konstanter, da de bruges i forbindelse med opsætningen af editorer i framen
Man skal specielt bemærke metoderne: isCellEditable og setValueAt.
isCellEditable bruges til at angive hvilke af tabellens celler det er tilladt at rette. Vi vil kun tillade at der rettes i de fem hverdage - ikke i tidspunkter for lektioner.

setValueAt kaldes af DefaultCellEditor'en når den ønsker at ændre en værdi i tabellen. Ikke alene kan denne metode ændre i tabellen, men man kan også foretage en evt. kontrol af de værdier, der indgår i ændringen. Vores ændringer kommer fra en combobox der kun kender tilladte værdier, så vi foretager ingen kontrol.

 

Dernæst følger framen, der tildeler de fem kolonner deres editorer:
import javax.swing.*;

public class SkemaTabelFrame extends JFrame {

  public SkemaTabelFrame( String title ) {
    super( title );
    
    SkemaTabelModel tabelModel = new SkemaTabelModel();

    JTable tabelView = new JTable( tabelModel );
    
    // Editors
    for ( int i=0; i<SkemaTabelModel.DAGE.length; i++ )
      tabelView.getColumn( SkemaTabelModel.DAGE[i] )
        .setCellEditor( 
          new DefaultCellEditor( 
            new JComboBox( SkemaTabelModel.TILSTANDE ) ) );
    
    JScrollPane pane = new JScrollPane( tabelView );

    getContentPane().add( pane );
    
    setDefaultCloseOperation( EXIT_ON_CLOSE );
    setSize( 500, 143 );
    setVisible( true );
  }
}

Vi henter kolonnernes navne fra arrayet med dagenes navne og giver comboboxen arrayet med de mulige tilstande.

 

Resultatet bliver:

Figur 7:
Anvendelse af JComboBox
som editor

 

 

4. "Det grå felt"

I eksemplerne ovenfor har vi ofte anvendt at resize framen så den har passet til tabellens størrelse. I eksemplet med "Reservation af Auditoriet" er dette særlig tydeligt, idet der anvendes en højde, på framen, på 143 pixels. Havde vi i stedet brugt et kald af pack-metoden, ville vi have fået følgende:

Figur 8:
Det grå felt

 

Man får en gråt område, som man i de fleste tilfælde gerne ville være foruden. Det grå felt opstår fordi det område som tabellen gerne vil have når den indgår i sammenhænge, hvor den kan scroll'es er sat til en fast størrelse og ikke til tabellens egen størrelse - hvilket ofte ville være at foretrække.
Problemet kan løses ved at indsætte følgende linie efter instantieringen af tableView (en JTable) i SkemaTabelFrame's konstruktor:
tableView.setPreferredScrollableViewportSize( tableView.getPreferredSize() );
Med metoden: setPreferredScrollableViewportSize, kan vi selv sætte den ønskede størrelse, og vi vælger har at tage denne fra tabellens egen foretrukne størrelse. Der er her tale om to forskellige foretrukne størrelser: I almindelighed, og i scroll-sammenhæng. Vi vælger at sætte disse til det samme!
Laver man en subklasse til JTable kan man i stedet vælge at override den nedarvede getPreferredScrollableViewportSize-metode:
public Dimension getPreferredScrollableViewportSize() {
  return getPreferredSize();
}
Såfremt man nedarver fra JTable, er det smar og behag, hvad man vælger at gøre.