javabog.dk  |  << forrige  |  indhold  |  næste >>  |  programeksempler  |  om bogen

6 Grafiske brugergrænseflader (Swing)


6.1 Introduktion til Swing 100

6.1.1 Eksempel på forskelle mellem Swing og AWT 101

6.2 MVC i Swing- og AWT-komponenter 102

6.2.1 AWT-komponenter 102

6.2.2 Swing-komponenter 102

6.3 Datamodellen i Swing-komponenter 103

6.3.1 Tekstfelter (interfacet Document) 103

6.3.2 Tabeller (interfacet TableModel) 104

6.3.3 Knapper (interfacet ButtonModel) 105

6.3.4 Andre komponenter 106

6.3.5 Opgaver 106

6.3.6 Løsning 107

6.4 Præsentationsdelen i Swing 110

6.4.1 Lister (ListCellRenderer) 110

6.4.2 Eksempel: Præsentation af skrifter i en liste 111

6.4.3 Tabeller (TableCellRenderer) 113

6.4.4 Træer (TreeCellRenderer) 114

6.4.5 Opgaver 114

6.5 Kontroldelen af Swing-komponenter 115

6.5.1 Tabeller (TableCellEditor) 115

6.5.2 Træer (TreeCellEditor) 115

6.5.3 Standardredigering med DefaultCellEditor 115

6.6 Eksempler 116

6.6.1 SwingSet2-demo af JTable 116

6.6.2 Eksempel med JTable 117

6.7 Avanceret: Udseendet af Swing 119

6.7.1 Swing-komponenters standardudseender 119

6.7.2 Andre udseender 119

6.7.3 Kunststoff-udseendet 119

6.7.4 Alloys-udseendet 120

6.7.5 Opgaver 120

Det er en fordel at have læst den første del af kapitel 19 for at have et kendskab til Model-View-Controller-arkitekturen.

Dette kapitel handler hovedsageligt om de ting i Swing, der er umiddelbart svære at forstå. Det behandler nogle af de mere avancerede aspekter af brugergrænseflader og Swing, som bl.a. JTable og MVC (Model-View-Controller-arkitekturen, beskrevet i dybden i kapitel 19).

En mere grundlæggende (om end lidt gammel) introduktion finder man i Kristian Hansens bog 'Avanceret Java-programmering', der kan hentes gratis på http://bog.ing.dk/.

6.1 Introduktion til Swing

I Java 1.2 blev et nyt sæt komponenter føjet til standardbibliotekerne. De svarer meget til AWT-komponenterne, men de er letvægtkomponenter, dvs. de er programmeret helt i Java og er derfor mindre afhængige af det underliggende styresystem.

De kaldes Swing-komponenter og findes i pakken javax.swing, og de starter alle med et J i klassenavnet, f.eks. JButton, JCheckBox, JRadioButton, JLabel, JList, JTextField, JTextArea, JPanel, JApplet og JFrame.

Der er også flere komponenter, som ikke findes i java.awt: JComboBox, JToggleBotton, JTable (en regneark-lignende komponent til at vise og manipulere tabeldata), JTree (viser en træstruktur ligesom stifinderen i Windows) og JTextPane (kan vise og redigere tekst med formatering og indlejrede billeder, herunder HTML- og RTF-dokumenter).

Her er et klassediagram over de vigtigste Swing-komponenter:

Yderst til venstre ses AWT-klasserne Container, Window, Frame, Panel og Applet. Resten er Swing-komponenter, og det ses, hvordan AWT-komponenterne alle har en pendant i Swing med et foranstillet J.

For at undgå blinkeri bruger alle Swing-containere som standard dobbelt tegnebuffer (eng.: double buffering, hvor komponenterne, når de skal gentegnes, ikke tegnes direkte på skærmen, men i en separat grafikbuffer, som derefter kopieres ind på skærmen.

6.1.1 Eksempel på forskelle mellem Swing og AWT

Dette eksempel illustrerer de væsentligste forskelle mellem Swing og AWT (forskellene er markeret med fed):

import java.awt.*;
import javax.swing.*;

public class SwingVindue extends JFrame
{
  JLabel labelHvadErDitNavn = new JLabel();
  JTextField textFieldNavn = new JTextField();
  JButton buttonOpdater = new JButton();
  JTextArea textAreaHilsen = new JTextArea();

  public void paint(Graphics g) {
    // vigtigt! Kald den oprindelige paint() så Swing-komponenter bliver tegnet
    super.paint(g);

    g.drawLine(0,0,50,50);
    g.fillOval(5,20,300,30);
    g.setColor(Color.green);
    String navn = textFieldNavn.getText();
    for (int i=0; i<50; i=i+10)
      g.drawString("Hej "+navn+" !",100+i,30+i);
  }

  public SwingVindue() {
    try {
      jbInit();
    }
    catch(Exception e) {
      e.printStackTrace();
    }
  }

  private void jbInit() throws Exception {
    // Swing-komponenter tillader HTML-koder i deres tekst:
    labelHvadErDitNavn.setText("<html><b>Hvad er</b> dit <i>navn</i>?</html>");
    labelHvadErDitNavn.setBounds(new Rectangle(15, 69, 108, 15));
    textFieldNavn.setText("Jacob");
    textFieldNavn.setBounds(new Rectangle(129, 61, 95, 29));

    buttonOpdater.setText("<html><font size=+1>op</font>da<u>ter</u>!</html>");
    buttonOpdater.setBounds(new Rectangle(231, 60, 91, 32));
    textAreaHilsen.setText("Her kommer en tekst...");
    textAreaHilsen.setBounds(new Rectangle(6, 102, 316, 78));

    // vigtigt! Tilføj komponenterne til "content pane" på en Swing-container
    this.getContentPane().setLayout(null);
    this.getContentPane().add(labelHvadErDitNavn, null);
    this.getContentPane().add(textAreaHilsen, null);
    this.getContentPane().add(buttonOpdater, null);
    this.getContentPane().add(textFieldNavn, null);
  }

  public static void main(String[] arg) {
    SwingVindue vindue = new SwingVindue();
    vindue.setSize(350,120);
    vindue.setVisible(true);
  }
}

Som det ses af eksemplet, understøtter Swing-komponenter visning af HTML-kode, så man kan få knapper og etiketter, hvor dele af teksten er f.eks. fed, kursiv eller af en anden størrelse.

Bruger man Swing-komponenter og samtidig har defineret paint()-metoden, skal man huske at kalde den oprindelige paint()-metode, ellers "forsvinder" Swing-komponenterne:

  public void paint(Graphics g) 
  {
    // vigtigt! Kald den oprindelige paint() så Swing-komponenter bliver tegnet
    super.paint(g);

    // herunder kommer vores egen tegningskode
  }

Det skyldes, at Swing-komponenter bliver tegnet fra containerens (superklassens) paint()-metode. Bliver paint() ikke kaldt i superklassen, bliver Swing-komponenterne ikke tegnet.

En anden vigtig forskel er, at man ikke føjer komponenter direkte til containeren, men til dens "content pane", med this.getContentPane().add( komponent ).

I AWT skrev man blot this.add( komponent ).

6.2 MVC i Swing- og AWT-komponenter

Selvom MVC (Model-View-Controller-arkitekturen beskrevet i kapitel 19) egentlig er beregnet på at adskille et programs logik fra dets brugergrænseflade, kan tankegangen også bruges internt i en grafisk komponent til at adskille de data, som komponenten repræsenterer, fra fremvisningen og ændringen af dem.

Lad os se på eksempelvis et indtastningsfelt (TextField eller JTextField):

6.2.1 AWT-komponenter

AWT-komponenterne, de oprindelige simple komponenter, som Java blev født med, fungerer som bro til platformens egne grafiske komponenter, sådan at når et program opretter f.eks. et Button-objekt, vil der faktisk blive oprettet en Windows-knap under Windows, en Linux-knap under Linux, en MacOS-knap på Macintosh osv.. Button-objektet vil så fungere som platformsuafhængig facade ned mod den meget platformsspecifikke knap.

Det er derfor ikke muligt at påvirke, hvordan en AWT-komponent virker, og model, præsentation og kontrol-del er uadskillelige. Det gør dem relativt simple at arbejde med, men også relativt ufleksible, hvis man skulle ønske at ændre dem, så de virker lidt anderledes.

F.eks. er det meget besværligt (hvis ikke umuligt) at have to tekstfelter, der redigerer i den samme tekst, eller at lave et tekstfelt, som kun viser store bogstaver.

6.2.2 Swing-komponenter

Swing-komponenter er implementeret i ren Java, og derfor har det været muligt at gøre dem langt mere fleksible end AWT-komponenterne.

I Swing-komponenterne er datamodellen skilt ud fra komponenten. Det vil sige, at programmøren har mulighed for at selv bestemme, hvilket objekt der skal bruges som datamodel. Præsentationen og kontroldelen har man også adgang til, omend mere begrænset.

6.3 Datamodellen i Swing-komponenter

Modellen i en komponent er de data, som komponenten repræsenterer. Hvilken slags model der anvendes afhænger meget af komponenten.

6.3.1 Tekstfelter (interfacet Document)

I JTextField, JTextArea og de andre tekstredigeringsfelter er datamodellen et objekt af typen Document, og læses hhv. sættes med metoderne getDocument() og setDocument():

public Document getDocument()
public void setDocument(Document doc)

Document er et interface, der er ret indviklet af finde rundt i, fordi det er beregnet til også at understøtte RTF- og HTML-formaterede dokumenter (JEditorPane og JTextPane), så det vil vi ikke beskrive i detaljer.

Eksempel: To tekstfelter, der deler det samme dokument

Har man to tekstfelter...

  JTextField jTextField1 = new JTextField();
  JTextField jTextField2 = new JTextField();

kan man få dem til at redigere i den samme tekst ved at sætte den ene til at bruge den andens model:

  Document datamodel = jTextField1.getDocument();
  jTextField2.setDocument( datamodel );

Eksempel: Et tekstfelt, der kun accepterer store bogstaver

Ved at lave sin egen datamodel kan man selv bestemme præcist hvad der kan ske med teksten.

I stedet for at lave en implementation af Document-interfacet fra grunden (det er ret indviklet) vælger man ofte at arve fra standardmodellen (der hedder PlainDocument) og bare tilsidesætte den metode, der kaldes for at indsætte tekst i dokumentet:

import javax.swing.text.*;
public class StortDokument extends PlainDocument
{
  public void insertString(int o, String s, AttributeSet a)
    throws BadLocationException
  {
    // kald den oprindelige metode med strengen med store bogstaver
    super.insertString(o, s.toUpperCase(), a);      
    // log ændringen, så vi kan følge med i hvad der sker
    System.out.println("insertString("+o+", '"+s+"') kaldt.\n");
  }
}

Ovenstående klasse kan bruges som datamodel i et tekstfelt med koden:

  Document datamodel = new StortDokument();
  jTextField2.setDocument( datamodel );

Af andre klasser, der implementerer Document-interfacet, kan nævnes DefaultStyledDocument og HTMLDocument. I disse dokumenttyper har tegnene attributter såsom skriftstørrelse, fed, kursiv, understreget osv. (beregnet til JEditorPane og JTextPane).

6.3.2 Tabeller (interfacet TableModel)

Tabeller (JTable) bruger en TableModel som model og har metoder til at læse/sætte den:

public TableModel getModel()
public void setModel(TableModel dataModel)

JTable kalder modellens metoder for at vide, hvad den skal vise.

Modellen har interfacet:

package javax.swing.table;

public interface TableModel
{
  public int getRowCount();
  public int getColumnCount();
  public String getColumnName(int kollonneindeks);
  public Class getColumnClass(int kollonneindeks)
  public boolean isCellEditable(int rækkeindeks, int kollonneindeks);
  public Object getValueAt(int rækkeindeks, int kollonneindeks);
  public void setValueAt(Object værdi, int rækkeindeks, int kollonneindeks);
  public void addTableModelListener(TableModelListener lytter);
  public void removeTableModelListener(TableModelListener lytter);
}

En måde at lave en tabel på er altså at lave en klasse, der implementerer TableModel-interfacet, og så bruge den som datamodel. Det er dog lidt besværligt, fordi man selv skal holde styr på lyttere og implementere addTableModelListener() og removeTableModelListener().

Man kan også vælge at arve fra AbstractTableModel (eller DefaultTableModel), der implementerer TableModel og selv sørger for at håndtere lyttere.

Her er en klasse, der får et endimensionalt array af strenge til at fungere som en TableModel, så den kan vises af JTable (sådan en hjælpeklasse, der får 'noget til at passe ind i noget', kaldes en adapter, se afsnit 17.2).

Tabellen kommer til at vise to kolonner, første kolonne med rækkenummeret og anden kolonne er indholdet af arrayet på det pågældende rækkenummer.

import javax.swing.table.*;

public class StrengArrayTableModel extends AbstractTableModel
{
  private String[] data;
  public  StrengArrayTableModel(String[] dat) { data = dat; }

  public int getColumnCount()    { return 2; }
  public int getRowCount()       { return data.length ;}
  public Object getValueAt(int ræk, int kol)
  {
    if (kol == 0) return new Integer(ræk); // første kolonne er indeks
    else return data[ræk];                 // anden kolonne er strengenes værdi
  }
}

Her er et lille program, der benytter adapteren til at vise et array af strenge:

import javax.swing.*;

public class BenytStrengArrayTableModel
{
  public static void main(String[] args)
  {
    JFrame f = new JFrame();
    JTable t;
    f.setSize(200, 200);

    String[] arr = {"a","b","hej","c"}; // strenge
    t = new JTable(new StrengArrayTableModel(arr));

    f.getContentPane().add(t);
    f.setVisible(true);
  }
}

Man kan naturligvis også bare lade, som om man viser nogle data uden at hente dem fra nogen datastruktur. Eksempelvis er her en datamodel, der repræsenterer den lille tabel. Den har 10 rækker og 10 kolonner, og hver celle har samme værdi som rækkenummer+1 gange kolonnenummer+1.

import javax.swing.table.*;

public class DenLilleTabel extends AbstractTableModel 
{
  public int getColumnCount()    { return 10; }
  public int getRowCount()       { return 10;}
  public Object getValueAt(int r, int k) { return new Integer((r+1)*(k+1)); }
}

Vist i en JTable giver klassen følgende data:

En anden måde er at angive et todimensionalt array (eller en Vector af Vector-objekter) med startværdier i konstruktøren til JTable. Da vil den selv oprette en datamodel, der repræsenterer data.

6.3.3 Knapper (interfacet ButtonModel)

Alle knapper og lignende, dvs. JButton, JToggleButton, JMenuItem, JMenu, JCheckbox og JRadiobox bruger en ButtonModel som datamodel.

Modellen læses og sættes med metoderne:

ButtonModel getModel()
void setModel(ButtonModel nyModel)

Selve ButtonModel er et interface med metoderne

public interface ButtonModel extends ItemSelectable
{
  public void setArmed(boolean b);
  public void setSelected(boolean b);
  public void setEnabled(boolean b);
  public void setPressed(boolean b);
  public void setRollover(boolean b);
  public void setMnemonic(int key);
  public int  getMnemonic();
  public void setActionCommand(String s);
  public String getActionCommand();
  public void setGroup(ButtonGroup group);

  // ... flere metoder
}

6.3.4 Andre komponenter

I de fleste andre hedder metoderne til at aflæse/udskifte datamodellen hhv. getModel() og setModel().

JComboBox bruger modellen ComboBoxModel:

void setModel(ComboBoxModel nyModel)
ComboBoxModel getModel()

JList bruger modellen ListModel:

public ListModel getModel()
public void setModel(ListModel nyModel)

JProgressBar, JScrollBar og JSlider, dvs. komponenter, der på en eller anden måde grafisk skal vise en værdi i et interval, bruger modellen BoundedRangeModel:

public BoundedRangeModel getModel()
public void setModel(BoundedRangeModel nyModel)

JTree bruger modellen TreeModel:

public TreeModel getModel()
public void setModel(TreeModel nyModel)

... og så videre.

Enhver Swing-komponent har altså en datamodel, der beskriver komponentens tilstand.

6.3.5 Opgaver

  1. Lav et program med med to tekstfelter, der deler samme dokument.

  2. Tilføj et tekstfelt, der kun accepterer store bogstaver til programmet.

  3. Prøv at lade to knapper dele datamodel. Hvad sker der så? Hvad med en menuindgang og en knap?

6.3.6 Løsning

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

6.4 Præsentationsdelen i Swing

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

6.4.1 Lister (ListCellRenderer)

For JComboBox og JList skal fremviser-objektet implementere ListCellRenderer:

package javax.swing;

public interface ListCellRenderer
{
  /**
   * Return a component that has been configured to display the specified
   * value. That component's <code>paint</code> method is then called to
   * "render" the cell.  If it is necessary to compute the dimensions
   * of a list because the list cells do not have a fixed size, this method
   * is called to generate a component on which <code>getPreferredSize</code>
   * can be invoked.
   *
   * @param list The JList we're painting.
   * @param value The value returned by list.getModel().getElementAt(index).
   * @param index The cells index.
   * @param isSelected True if the specified cell was selected.
   * @param cellHasFocus True if the specified cell has the focus.
   * @return A component whose paint() method will render the specified value.
   *
   * @see JList
   * @see ListSelectionModel
   * @see ListModel
   */
  Component getListCellRendererComponent(
    JList list,
    Object value, int index,
    boolean isSelected, boolean cellHasFocus);
}

Fremvisningen sættes med metoden setCellRenderer(ListCellRenderer cellRenderer).

Har man ikke angivet nogen fremviser, bruges klassen DefaultListCellRenderer, der kan vise ikoner (Icon) og tekster. Vil man lave sin egen fremvisning, anbefales det, at man arver fra denne og omdefinerer getListCellRendererComponent() eller paint().

Eksempel på præsentationsdel af en liste

Her er et eksempel på en liste, der viser alle sine punkter overstreget. Det er gjort ved at arve fra DefaultListCellRenderer og omdefinere paint()-metoden til at tegne en skrå streg.

import java.awt.*;
import javax.swing.*;
public class OverstregedeCeller extends DefaultListCellRenderer {
  public void paint(Graphics g) {
    super.paint(g);                          // tegn cellen
    g.drawLine(0,0,getWidth(),getHeight());  // tegn en streg hen over den
  }
}

6.4.2 Eksempel: Præsentation af skrifter i en liste

Det følgende eksempel på en præsentationsdel til lister (ListCellRenderer) viser en liste over skrifttyper, hvor hver skrifttype bliver vist som den rent faktisk ser ud på skærmen:

Fremvisningen sker ved at arve fra DefaultListCellRenderer og tilsidesætte metoden getListCellRendererComponent():

import java.awt.*;
import java.util.*;
import javax.swing.*;

public class Skriftfremviser extends DefaultListCellRenderer
{
  Map skrifter = new HashMap(); // afbildning fra skriftnavn til Font-objekt

  public Component getListCellRendererComponent(
    JList liste,
    Object værdi, int index,
    boolean valgt, boolean harFokus)
  {
    Component visningsKomponent =
      super.getListCellRendererComponent(liste, værdi, index, valgt, harFokus);

    System.out.println(index +" "+ værdi); // se hvor ofte metoden bliver kaldt

    if (værdi != null)
    {
      Font skrifttype = (Font) skrifter.get(værdi);
      if (skrifttype==null) 
      {
        skrifttype = new Font( (String) værdi, Font.PLAIN, 14); // indlæs skrift
        skrifter.put(værdi, skrifttype);                        // ..og husk den
      }
      visningsKomponent.setFont(skrifttype);
    }

    return visningsKomponent;
  }
}

I getListCellRendererComponent() kalder vi først superklassens oprindelige metode for at få ordnet andre ting, såsom at afgøre hvilken tekst, der skal vises og om cellen (indgangen) skal have en speciel farve, fordi den er valgt/har fokus.

Derefter prøver vi at indlæse den pågældende skrifttype. For at undgå at oprette alt for mange Font-objekter laver vi en afbildning fra skriftnavne til Font-objekter og cacher Font-objekterne.

Her er et program, der benytter Skriftfremviser.

import java.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class BenytSkriftfremviser extends JFrame implements ActionListener
{
  JComboBox jComboBox1 = new JComboBox();
  JTextArea jTextArea1 = new JTextArea();

  public BenytSkriftfremviser() {
    setTitle("BenytSkriftfremviser");

    String[] skriftnavne = GraphicsEnvironment.
      getLocalGraphicsEnvironment().getAvailableFontFamilyNames();

    jTextArea1.setLineWrap(true);
    jTextArea1.setText("En snegl på vejen er tegn på regn\ni Spanien\n\n"
      +"Hvornår smager en Tuborg bedst?\nHvergang!");

    jComboBox1.setModel(new DefaultComboBoxModel(skriftnavne));
    jComboBox1.setRenderer(new Skriftfremviser());
    jComboBox1.addActionListener(this);

    JPanel jPanel1 = new JPanel();
    jPanel1.setLayout(new BorderLayout());
    jPanel1.add(jComboBox1, BorderLayout.EAST);

    getContentPane().setLayout(new BorderLayout());
    getContentPane().add(jPanel1, BorderLayout.NORTH);
    getContentPane().add(jTextArea1, BorderLayout.CENTER);
  }

  public void actionPerformed(ActionEvent e) { // kaldes når valglisten ændres
    String skriftnavn = (String) jComboBox1.getSelectedItem();
    System.out.println(skriftnavn + " " + e);
    jTextArea1.setFont( new Font( skriftnavn, Font.PLAIN, 20) );
  }

  public static void main(String[] args) {
    BenytSkriftfremviser vindue = new BenytSkriftfremviser();
    vindue.pack();
    vindue.setVisible(true);
  }
}

6.4.3 Tabeller (TableCellRenderer)

For JTable kan man sætte fremvisningen på hver kolonne (hentet med getColumn() og navnet på kolonnen) med metoden: setRenderer(TableCellRenderer fremviser).

package javax.swing.table;

public interface TableCellRenderer
{ 
  /**
   *  Returns the component used for drawing the cell.  This method is
   *  used to configure the renderer appropriately before drawing.
   *
   * @param table     the <code>JTable</code> that is asking the
   *          renderer to draw; can be <code>null</code>
   * @param value     the value of the cell to be rendered.  It is
   *          up to the specific renderer to interpret
   *          and draw the value.  For example, if
   *          <code>value</code>
   *          is the string "true", it could be rendered as a
   *          string or it could be rendered as a check
   *          box that is checked.  <code>null</code> is a
   *          valid value
   * @param isSelected  true if the cell is to be rendered with the
   *          selection highlighted; otherwise false
   * @param hasFocus    if true, render cell appropriately.  For
   *          example, put a special border on the cell, if
   *          the cell can be edited, render in the color used
   *          to indicate editing
   * @param row     the row index of the cell being drawn.  When
   *          drawing the header, the value of
   *          <code>row</code> is -1
   * @param column    the column index of the cell being drawn
   */

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

Her er et eksempel på et præsentationsobjekt oprettet direkte ud fra interfacet. Det er beregnet til at vise Color-objekter. I getTableCellRendererComponent() konfigureres en JLabel derfor til at vise den pågældende farve, hvorefter denne JLabel returneres:

import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;

public class Farvepraesentation implements TableCellRenderer
{
  JLabel jLabel = new JLabel();          // genbrug den samme komponent

  public Component getTableCellRendererComponent(JTable tabel, Object værdi,
          boolean erValgt, boolean harFokus, int række, int kolonne)
  {
    if (værdi instanceof Color)          // er værdien af type Double?
    {
      Color f = (Color) værdi;
      jLabel.setBackground(f);           // hele baggrunden viser farven
      jLabel.setForeground(f.darker());  // tekstfarven er lidt mørkere
      jLabel.setOpaque(true);            // uigennemsigtig så baggrunden kan ses
      jLabel.setText("farve");           // vis bare en eller anden tekst
    } else {
      jLabel.setBackground(Color.white); // er objektet ikke en farve, så vis
      jLabel.setForeground(Color.black); // det som toString() giver
      jLabel.setText(værdi.toString());
    }
    return jLabel;                       // returnér komponenten der skal vises
  }
}

I stedet for at implementere TableCellRenderer direkte tilrådes det dog, at man arver fra klassen DefaultTableCellRenderer og definerer metoden setValue(). Det følgende eksempel præsenterer tal (af typen Double) med sort skrift, hvis de er positive, og rød skrift, hvis de er negative:

import javax.swing.*;
import javax.swing.table.*;
import java.awt.*;

public class Talpraesentation extends DefaultTableCellRenderer
{
  public void setValue(Object værdi)
  {
    if (værdi instanceof Double) {
      if (((Double) værdi).doubleValue()<0) setForeground(Color.red);
      else setForeground(Color.blue);
    }
    else setForeground(Color.black);

    setText(værdi.toString());
  }
}

Som det ses, er det altså simplere at arve fra DefaultTableCellRenderer (som implementerer TableCellRenderer). Samtidigt er DefaultTableCellRenderer optimeret beregnet på den ret specielle brug, som JTable gør af den, så det giver også mere effektiv kode.

Begge klasser kan ses brugt i afsnit 6.6.2.

6.4.4 Træer (TreeCellRenderer)

Tilsvarende har JTree metoden setCellRenderer(TreeCellRenderer fremviser) og en DefaultTreeCellRenderer, der bruges, hvis en anden fremviser ikke specificeres.

6.4.5 Opgaver

  1. Prøv Skriftfremviser-eksemplet.

  2. Lav din egen Farvefremviser, der giver brugeren mulighed for at vælge mellem et antal farver.

6.5 Kontroldelen af Swing-komponenter

På tilsvarende måde som med Renderer findes der (for tabeller og træer) også klasser og metoder til at specificere, hvordan redigering af indholdet af komponenten skal ske .

Måden at arbejde med disse svarer ret meget til måden at arbejde med præsentations-delen. Kontrol-delene skal implementere interfacet CellEditor:

public interface CellEditor {
  public Object getCellEditorValue();
  public boolean isCellEditable(EventObject anEvent);
  public boolean shouldSelectCell(EventObject anEvent);
  public boolean stopCellEditing();
  public void cancelCellEditing();
  public void addCellEditorListener(CellEditorListener l);
  public void removeCellEditorListener(CellEditorListener l);
}

I stedet for at skrive sin egen editor anbefales det at anvende (eller eventuelt arve fra) klassen DefaultCellEditor (beskrevet nedenfor).

Det er der eksempler på i afsnit 6.6.2.

6.5.1 Tabeller (TableCellEditor)

Interfacet TableCellEditor (der udvider CellEditor) skal implementeres hvis man selv vil bestemme hvordan redigeringen af en tabel skal foregå. Det har metoden:

  Component getTableCellEditorComponent(
    JTable table, 
    Object value,
    boolean isSelected,
    int row, 
    int column
  );

som skal defineres til at returnere en (korrekt konfigureret) komponent, der står for redigeringen af den pågældende celle.

6.5.2 Træer (TreeCellEditor)

Interfacet TreeCellEditor (der udvider CellEditor) skal implementeres hvis man selv vil bestemme, hvordan redigeringen af et træ skal foregå. Det har metoden:

  Component getTreeCellEditorComponent(
    JTree tree, 
    Object value,
    boolean isSelected,
    boolean expanded,
    boolean leaf, 
    int row
  );

som skal defineres til at returnere en (korrekt konfigureret) komponent, der står for redigeringen af den pågældende gren af træet.

6.5.3 Standardredigering med DefaultCellEditor

DefaultCellEditor implementerer både TableCellEditor og TreeCellEditor. Den bruges af både JTable og JTree, hvis ingen anden editor er angivet. I dens konstruktør kan man angive en JTextField, JCheckBox eller JComboBox, som da vil blive brugt til redigeringen.

6.6 Eksempler

Sammen med JDK'en følger der en række eksempler (ligger i f.eks. jdk1.4/demo/). Det er en virkelig god idé at prøve nogle af disse eksempler, da de illustrerer de muligheder, man har som programmør, og samtidig følger kildeteksten med til alle eksemplerne, så man kan se, hvordan det er programmeret.

6.6.1 SwingSet2-demo af JTable

Et eksempel, der illustrerer de fleste af mulighederne med Swing-komponenter, hedder SwingSet2. Det ligger i jdk1.4/demo/jfc/SwingSet2 og startes ved at dobbeltklikke på jar-filen eller fra kommandolinjen skrive: java -jar SwingSet2.jar.

Herunder ses skærmbilledet af SwingSet2's demo af JTable:

Der er lavet kontroller til de fleste ting, kan "dreje på" for en JTable, bl.a.:

// kan brugeren bytte om på rækkefølgen kolonnerne vises i ved at trække i dem
jTable.getTableHeader().setReorderingAllowed(flag);

// skal der vises vandrette/lodrette linjer mellem cellerne
jTable.setShowHorizontalLines(flag);    jTable.setShowVerticalLines(flag);

// afstand mellem cellerne
jTable.setIntercellSpacing(new Dimension(bredde, højde));

// skal der vælges hele kolonner/rækker når brugeren klikker i en celle
jTable.setColumnSelectionAllowed(flag); jTable.setRowSelectionAllowed(flag);

// rækkehøjden 
jTable.setRowHeight(height);

Kildeteksten til SwingSet2 er lidt svær at overskue, da den samtidig demonstrerer internationalisering (programmet kan også vises på kinesisk!), tastaturgenveje og en masse andet.

6.6.2 Eksempel med JTable

Herunder ses et simplere eksempel på brug af JTable (inspireret af SwingSet2).

import java.awt.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;

public class BenytJTable extends JFrame {
  JTable jTable1 = new JTable();
  JScrollPane jScrollPane1 = new JScrollPane();

  public static void main(String[] args)  {
    BenytJTable vindue = new BenytJTable(); // opret vinduet
    vindue.pack();                          // tilpas størrelsen til indholdet
    vindue.setVisible(true);                // vis vinduet
  }

  private void jbInit() throws Exception {
    this.getContentPane().add(jScrollPane1, BorderLayout.CENTER);
    jScrollPane1.getViewport().add(jTable1, null);
  }

  public BenytJTable() {
    try {
      jbInit();
    }
    catch(Exception e) {
      e.printStackTrace();
    }

    // Hjælpevariabler
    ImageIcon æble     = new ImageIcon("apple.jpg",     "æble");
    ImageIcon asparges = new ImageIcon("asparagus.jpg", "asparges");
    ImageIcon banan    = new ImageIcon("banana.jpg",    "banan");
    ImageIcon broccoli = new ImageIcon("broccoli.jpg",  "broccoli");
    ImageIcon gulerod  = new ImageIcon("carrot.jpg",    "gulerod");
    ImageIcon kiwi     = new ImageIcon("kiwi.jpg",      "kiwi");
    ImageIcon løg      = new ImageIcon("onion.jpg",     "løg");
    Boolean ja  = Boolean.TRUE;
    Boolean nej = Boolean.FALSE;

    /////////////////////////////////////////////////
    // Data
    /////////////////////////////////////////////////
    final String[] kolonner =
      {"Navn",   "Yndlingsfarve", "Yndlingstal","Yndlingsret","Enlig?","Sexet?"};

    final Object[][] data = {
      {"Hans Petersen",   Color.blue,     new Double(44),   æble,    ja,  nej},
      {"Jacob Sørensenn", Color.yellow,   new Double(42),   kiwi,    ja,  ja},
      {"Grethe Ibsen",    Color.blue,     new Double(-37),  løg,     nej, nej},
      {"Kurt Ibsen",      Color.darkGray, new Double(1),    banan,   nej, nej},
      {"Karin Ibsen",     Color.green,    new Double(10.5), gulerod, nej, ja}
    };

    jTable1.setRowHeight(2*æble.getIconHeight()/3); // sæt højden af cellerne

    /////////////////////////////////////////////////
    // Oprettelse af datamodellen
    /////////////////////////////////////////////////

    /* Man kan oprette en datamodel direkte ud fra interfacet...
    class DatamodelFraInterface implements TableModel
    {
      public int getColumnCount()              { return kolonner.length; }
      public int getRowCount()                 { return data.length; }
      public Object getValueAt(int r, int k)   { return data[r][k]; }
      public String getColumnName(int k)       { return kolonner[k]; }
      public Class getColumnClass(int k)       { return data[0][k].getClass();}
      public boolean isCellEditable(int r, int k)    { return k != 1; }
      public void setValueAt(Object v, int r, int k) { data[r][k]=v; }
      public void addTableModelListener(TableModelListener p0) {}    // hmm...
      public void removeTableModelListener(TableModelListener p0) {} // hmm...
    };
    jTable1.setModel(new DatamodelFraInterface());
    */

    // eller oprette fra en hjælpeklasse der sørger for bl.a. hændelser
    class DatamodelFraHjaelpeklasse extends AbstractTableModel
    {
      public int getColumnCount()               { return kolonner.length; }
      public int getRowCount()                  { return data.length; }
      public Object getValueAt(int r, int k)    { return data[r][k]; }
      public String getColumnName(int k)        { return kolonner[k]; }
      public Class getColumnClass(int k)        { return data[0][k].getClass();}
      public boolean isCellEditable(int r, int k)   { return k != 1; }
      public void setValueAt(Object v, int r, int k){ data[r][k]=v; }
    };
    jTable1.setModel(new DatamodelFraHjaelpeklasse());

    // ... eller bruge standardklassen DefaultTableModel der sørger for
    // det hele (men ikke altid helt som man vil ha' det)...
    //jTable1.setModel(new DefaultTableModel(data,kolonner));

    /////////////////////////////////////////////////
    // Oprettelese af præsentationsobjekter
    /////////////////////////////////////////////////

    // brug et Farvepraesentation-objekt til kolonnen "Yndlingsfarve"
    TableCellRenderer farvepræsentation = new Farvepraesentation();
    jTable1.getColumn("Yndlingsfarve").setCellRenderer( farvepræsentation );

    // brug et Talpraesentation-objekt til kolonnen "Yndlingstal"
    TableCellRenderer talpræsentation = new Talpraesentation();
    jTable1.getColumn("Yndlingstal").setCellRenderer( talpræsentation );
///////////////////////////////////////////////// // Oprettelse af redigeringsobjekter ///////////////////////////////////////////////// // Lav en valgliste til at vælge mellem retterne. JComboBox comboBox = new JComboBox(); comboBox.addItem(æble); comboBox.addItem(asparges); comboBox.addItem(banan); comboBox.addItem(broccoli); comboBox.addItem(gulerod); comboBox.addItem(kiwi); comboBox.addItem(løg); jTable1.getColumn("Yndlingsret").setCellEditor( new DefaultCellEditor(comboBox)); // Og en til vælge mellem enlig og ikke-enlig. comboBox = new JComboBox(); comboBox.addItem( new Boolean(true)); comboBox.addItem( new Boolean(false)); jTable1.getColumn("Enlig?").setCellEditor(new DefaultCellEditor(comboBox)); } }

6.7 Avanceret: Udseendet af Swing

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

6.7.1 Swing-komponenters standardudseender

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

6.7.2 Andre udseender

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

6.7.3 Kunststoff-udseendet

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

6.7.4 Alloys-udseendet

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.

6.7.5 Opgaver

Dette afsnit er ikke omfattet af Åben Dokumentslicens.
Du skal købe bogen for at måtte læse dette afsnit.
Jeg erklærer, at jeg allerede har købt bogen
Jeg lover at anskaffe den i nær fremtid.
javabog.dk  |  << forrige  |  indhold  |  næste >>  |  programeksempler  |  om bogen
http://javabog.dk/ - af Jacob Nordfalk.
Licens og kopiering under Åben Dokumentlicens (ÅDL) hvor intet andet er nævnt (71% af værket).

Ønsker du at se de sidste 29% af dette værk (362838 tegn) skal du købe bogen. Så får du pæne figurer og layout, stikordsregister og en trykt bog med i købet.