Das Modell und der JTable

Willemers Informatik-Ecke

Das einzig seriöse Javabuch :-) Mehr...

Errata
Bei Amazon bestellen

2015-06-11
Ein JTable ist ein Swing-Kontrollelement und stellt eine Tabelle zur Verfügung, wie man sie von Tabellenkalkulationen kennt. Die Tabelle kann nicht nur einmal befüllt werden, sondern wird aus einer Datenquelle gespeist, die als Modell bezeichnet wird.

Erstellen einer Tabelle

Um JTable zu benutzen, muss es importiert werden. Zu Anfang sollte also die folgende Zeile stehen:
import javax.swing.JTable;
Das folgende Beispiel erstellt eine einfache Tabelle einer Hitparade. Die Tabelle wird bei der Erstellung durch String-Arrays befüllt.
import javax.swing.JFrame;
import javax.swing.JTable;
import javax.swing.JScrollPane;

public class SimpleTable extends JFrame {
    SimpleTable() { // Konstruktor
         setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        String [][] inhalt = { {"Beatles","Help"}, {"Beatwatt","Is That All"}, {"ABBA","Waterloo"} };
        String[] titel = {"Interpret", "Titel"};
        JTable table = new JTable(inhalt, titel);
        add(new JScrollPane(table)); // ohne JScrollPane keine Titel!
        setSize(400, 300);
        setVisible(true);
    }

    public static void main(String[] args) {
        new SimpleTable();
    }
}

Das Modell füllt die Tabelle

Das Auffüllen mit Strings über den Konstruktor ist aber hochgradig unbefriedigend, wenn die Daten in der Tabelle von Zeit zu Zeit geändert werden. Darum wird man in der Regel eine JTable in Verbindung mit einem Datenmodellierer aufbauen. Dieser füttert die JTable mit Daten. Das Hauptprogramm unterscheidet sich nur geringfügig.
import javax.swing.JFrame;
import javax.swing.JTable;
import javax.swing.JScrollPane;

public class TableTest extends JFrame {
    TableTest() { // Konstruktor
         setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JTable table = new JTable(new MeinTableModell());
        add(new JScrollPane(table)); // ohne JScrollPane keine Titel!
        setSize(400, 300);
        setVisible(true);
    }

    public static void main(String[] args) {
        new TableTest();
        // Table erzeugen
    }
}

Erweiterung von AbstractTableModel

Der Konstruktor von JTable bekommt nun eine Erweiterung (Ableitung) von AbstractTableModel übergeben, in diesem Fall MeinTableModell.

Das TableModel sorgt das Füllen des JTable-Objekts mit Daten. Das JTable-Objekt wird bei der Darstellung das Modell befragen, wie viele Spalten (getColumnCount) und Zeilen (getRowCount)darstellt werden sollen und wie die Überschriften der einzelnen Spalten heißen (getColumnNam). Um die Inhalte zu ermitteln, ruft JTable die Methode getValueAt auf und übergibt die Koordinaten.

Somit ist die Modellklasse dafür zuständig, die Daten für die Tabelle zu beschaffen, wenn sie benötigt werden.

import javax.swing.table.AbstractTableModel;


public class MeinTableModell extends AbstractTableModel {
    private DatenLieferant daten = null; // wo die Daten herkommen
    
    public MeinTableModell() {
        daten = new DatenLieferant(); // initialisiere Datenquelle
    }
    
    @Override
    public int getColumnCount() {   // Anzahl der Spalten
       return 2;
    }

    @Override
    public String getColumnName(int arg0) {    // Die Spaltenueberschriften
        if (arg0==0) return "Interpret";
        if (arg0==1) return "Titel";
        return null;
    }

    @Override
    public int getRowCount() { // Anzahl der Zeilen, also Datenobjekte
        return daten.getAnzahl();
    }

    @Override
    public Object getValueAt(int zeile, int spalte) { // Die eigentlichen Daten
        if (spalte==0) return daten.getInterpret(zeile);
        if (spalte==1) return daten.getTitel(zeile);
        return null;
    }
}

Implementierung von TableModel

Anstatt die Klasse AbstractTableModel zu erweitern könnte das Table-Modell natürlich auch das Interface TableModel implementieren. Zunächst sind die Unterschiede simpel. In den ersten Zeilen muss es heißen:

import javax.swing.event.TableModelListener;
import javax.swing.table.TableModel;

public class MeinTableModell implements TableModel {

Der Compiler wird sich melden und darauf bestehen, dass es da unimplementierten Methoden gibt. Wenn man Eclipse oder Netbeans verwendet, werden diese mit einem Mausklick mit Default-Werten erzeugt. Allerdings kann es bei einer Methode Ärger geben. Gibt getColumnClass nämlich wie von Eclipse vorgeschlagen null zurück, wird es keine Tabelleninhalte geben. Hier muss die String-Klasse zurückgegeben werden, damit auch Strings ausgelesen werden.

@Override
public Class getColumnClass(int arg0) {
    return String.class;
}

Daran zeigt sich auch die Flexibilität des Konzepts. Jede Spalte kann einen anderen Typ haben.

Der Datenlieferant

Zuletzt wird eine Klasse für den eigentlichen Datenlieferant gebaut. In diesem Fall hätte man es auch gleich in das Modell bauen können, weil es so simpel ist. In der Praxis würde man vielleicht noch weiter gehen und hier nur ein Interface erstellen, das dann von unterschiedlichen Varianten implementiert werden kann, wie beispielsweise einer Datenbankanbindung.

Wir verwenden einen einfachen Datenlieferant, der sogar die Daten liefert, die wir aus dem SimpleTable kennen.


public class DatenLieferant {
    // Eine ganz simple Quelle aus Strings.
    // Man koennte aber auch ein ArrayList oder eine Datenbank hier einbauen.
    String [][] inhalt = { {"Beatles","Help"}, {"Beatwatt","Is That All"}, {"ABBA","Waterloo"} };
    String[] titel = {"Interpret", "Titel"};

    public int getAnzahl() {
        return 3;
    }

    public String getInterpret(int zeile) {
        return inhalt[zeile][0];
    }

    public String getTitel(int zeile) {
        return inhalt[zeile][1];
    }
}

JTable-Methoden

Im Zusammenspiel mit einem Swing-Programm werden Methoden von JTable verwendet. Als Beispiel wird in den Listings jeweils die Tabelle meinJTable verwendet.

Selektierte Zeilen in der Tabelle

Die aktuell selektierte Zeile wird über folgende Code-Sequenz ermittelt:

if (meinJTable.getSelectedRowCount()==1) {
    int zeile = meinJTable.getSelectedRow();

In der ersten Zeile wird ermittelt, ob wirklich genau eine Zeile selektiert ist. Dann wird die Zeilennummer ermittelt.

Um Zeilen zu selektieren, wird zunächst jede Selektion aufgehoben. Dann wird bei jeder gewünschten Zeile die Selektion gewechselt. Im Beispiel befindet sich in der ArrayList<Integer> eine Liste der Zeilen, die selektiert werden sollen.

meinJTable.clearSelection();
for (int i=0; i<funde.size(); i++) {
    meinJTable.changeSelection(funde.get(i), WIDTH, true,false);
}

Änderungen aktualisieren

Stellt das Programm fest, dass sich der Dateninhalt der Tabelle geändert hat, muss die Tabelle aktualisiert werden. Mit der Methode invalidate funktioniert dies nicht zuverlässig.

Der Aufruf der Methode tableChanged erwirkt ein Neuladen der Daten von JTable. Wird als Parameter null übergeben, wird alles nachgeladen. Je nach Datenquelle kann der Aufwand natürlich recht hoch sein.

meinJTable.tableChanged(null); // funktioniert, ist aber recht grob

Wird nur eine Zeile geändert, kann JTable mit dem folgenden Aufruf dazu gebracht werden, nur diese eine Zeile zu aktualisieren, deren Nummer sich in der Variablen zeile befindet.

meinJTable.tableChanged(new TableModelEvent(meinJTable.getModel(), zeile));

Die beiden folgenden Aufrufe werden verwendet, um auf das Einfügen und Löschen von Zeilen zu reagieren.

meinJTable.tableChanged(new TableModelEvent(meinJTable.getModel(), 
        0, 0, TableModelEvent.ALL_COLUMNS, TableModelEvent.INSERT ));
meinJTable.tableChanged(new TableModelEvent(meinJTable.getModel(),
        zeile[i], zeile[i], TableModelEvent.ALL_COLUMNS, TableModelEvent.DELETE ));

Eine einfache Lösung ist es, wenn man die Modell-Klasse durch den Aufruf von fireTableCellUpdated() einen Neuaufbau der Tabelle erzwingen lässt. Das kann beispielsweise in der Methode setValueAt() erfolgen.

public void setValueAt(Object value, int row, int col) {
    data[row][col] = value;
    fireTableCellUpdated(row, col);
}

Editierer und Renderer

In der TableModel-Klasse kann durch Überschreiben der Methode getColumnClass() festgelegt werden, welchen Typ eine Spalte aufnehmen kann. Daraus wird auch die Darstellung und der Editor abgeleitet:

Die Methode isCellEditable() liefert für jede Zelle den booleschen Wert, ob sie editierbar ist.

Der Standardeditor prüft beispielsweise Zahlen, ob sie zulässig sind und verhindert ein Verlassen des Feldes, bis die Eingabe korrekt ist.

import javax.swing.JTable;
import javax.swing.table.AbstractTableModel;
import javax.swing.JScrollPane;
import javax.swing.JFrame;
import java.awt.BorderLayout;
import java.awt.Dimension;

public class EditRender extends JFrame {

    DasTableModel myModel = new DasTableModel();

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

    public EditRender() {
        super("EditRender");
        JTable table = new JTable(myModel);
        table.setPreferredScrollableViewportSize(new Dimension(400, 200));
        JScrollPane scrollPane = new JScrollPane(table);
        getContentPane().add(scrollPane, BorderLayout.CENTER);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    class DasTableModel extends AbstractTableModel {
        final String[] spaltenBeschriftung = { "String", "Double", "Integer", "Boole" };
        final Object[][] data = {
                { "", new Double(0.0), new Integer(0), new Boolean(false) },
                { "", new Double(0.0), new Integer(0), new Boolean(false) },
                { "", new Double(0.0), new Integer(0), new Boolean(false) },
                { "", new Double(0.0), new Integer(0), new Boolean(false) },
                { "", new Double(0.0), new Integer(0), new Boolean(false) },
        };

        public int getColumnCount() {
            return spaltenBeschriftung.length;
        }

        public int getRowCount() {
            return data.length;
        }

        public String getColumnName(int col) {
            return spaltenBeschriftung[col];
        }

        public Object getValueAt(int row, int col) {
            return data[row][col];
        }

        public Class getColumnClass(int c) {
            return getValueAt(0, c).getClass();
        }

        public boolean isCellEditable(int row, int col) {
            return true;
        }

        public void setValueAt(Object value, int row, int col) {
            data[row][col] = value;
            fireTableCellUpdated(row, col);
            System.out.println("setVal"+value); // da sind die Daten!
        }
    }
}
Wie erfährt die Anwendung von den Aktionen des Benutzers? Alle Änderungen wandern durch die Methode setValueAt() und können dort abgegriffen werden.


Homepage (C) Copyright 2015 Arnold Willemer