Java Swing TreeTable Tutorial mit Beispiel Example

12.06.2011

Java Swing TreeTable Tutorial mit Beispiel

TreeTables mit Java sind ein Thema für sich. Eigentlich geht man davon aus, daß so eine elementare Komponente in jeder GUI-Bibliothek vorhanden sind. Bei Java Swing ist das leider nicht der Fall. Allerdings besteht die Möglichkeit sich eine eigene Komponente zu erstellen, die sich wie eine TreeTable verhält und auch so aussieht. Wie dies gemacht werden kann, wird im folgenden Tutorial anhand eines Beispiels erklärt.

Auf der Oracle-Seite (ehemals Sun) findet man ein Beispiel von Philip Milne, wie sich eine TreeTable erstellen läßt. Leider enthält der Quellcode manche "Hacks", die inzwischen nicht mehr gebraucht werden. Das liegt wahrscheinlich daran, daß der Code schon recht alt ist. Außerdem ist es so, daß in die TreeTable eine Reihe von Features eingebaut wurden, die das wesentliche der TreeTable verdecken. Ich habe das Beispiel von den unnötigen Dingen befreit und den Code so umgestellt, daß er verständlich ist und nur das enthält was auch wirklich für die Darstellung einer TreeTable notwendig ist.

Die folgenden beiden Abbildungen zeigen Screenshots der fertigen TreeTable. Einmal auf dem Mac und einmal auf einem Windows-Rechner.

Die folgende Abbildung zeigt die Klassenstruktur der TreeTable. Anhand dieser Abbildungen wird in den nächsten Absätzen erklärt, wie die TreeTable aufgebaut ist. Alle Klassen, die Bestandteil des JDK sind, haben einen Farbverlauf als Hintergrund. Die Klassen, die im folgenden erstellt werden, haben einen einfarbigen Hintergrund. Der Klassenname beginnt mit 'My'.

Als erstes wird das Interface TreeModel durch das Interface MyTreeTableModel erweitert. Durch diese Erweiterung wird die Möglichkeit geschaffen, daß ein Knoten (Node) mehrere Spalten (Columns) haben kann. Das Interface sieht so aus:

package de.hameister.treetable;

import javax.swing.tree.TreeModel;

public interface MyTreeTableModel extends TreeModel {


	/**
	 * Returns the number of available columns.
	 * @return Number of Columns
	 */
	public int getColumnCount();

	/**
	 * Returns the column name.
	 * @param column Column number
	 * @return Column name
	 */
	public String getColumnName(int column);


	/**
	 * Returns the type (class) of a column.
	 * @param column Column number
	 * @return Class
	 */
	public Class<?> getColumnClass(int column);

	/**
	 * Returns the value of a node in a column.
	 * @param node Node
	 * @param column Column number
	 * @return Value of the node in the column
	 */
	public Object getValueAt(Object node, int column);


	/**
	 * Check if a cell of a node in one column is editable.
	 * @param node Node
	 * @param column Column number
	 * @return true/false
	 */
	public boolean isCellEditable(Object node, int column);

	/**
	 * Sets a value for a node in one column.
	 * @param aValue New value
	 * @param node Node
	 * @param column Column number
	 */
	public void setValueAt(Object aValue, Object node, int column);
}

Im nächsten Schritt wird das Interface MyTreeTableModel um die abstrakte Klasse MyAbstractTreeTableModel erweitert. In dieser Klasse wird der Wuzelknoten (root) gespeichert, es gibt eine Methode zum Überprüfen, ob Kinderknoten vorhanden sind und alle EventListener verwerden verwaltet. Die EventListener sorgen dafür, daß Strukturänderungen im Datenmodell auch an den Baum weitergeleitet und angezeigt werden.

package de.hameister.treetable;

import javax.swing.event.EventListenerList;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.TreePath;

public abstract class MyAbstractTreeTableModel implements MyTreeTableModel {
	protected Object root;
	protected EventListenerList listenerList = new EventListenerList();

	private static final int CHANGED = 0;
	private static final int INSERTED = 1;
	private static final int REMOVED = 2;
	private static final int STRUCTURE_CHANGED = 3;

	public MyAbstractTreeTableModel(Object root) {
		this.root = root;
	}

	public Object getRoot() {
		return root;
	}

	public boolean isLeaf(Object node) {
		return getChildCount(node) == 0;
	}

	public void valueForPathChanged(TreePath path, Object newValue) {
	}

	/**
	 * Die Methode wird normalerweise nicht aufgerufen.
	 */
	public int getIndexOfChild(Object parent, Object child) {
		return 0;
	}

	public void addTreeModelListener(TreeModelListener l) {
		listenerList.add(TreeModelListener.class, l);
	}

	public void removeTreeModelListener(TreeModelListener l) {
		listenerList.remove(TreeModelListener.class, l);
	}

	private void fireTreeNode(int changeType, Object source, Object[] path, int[] childIndices, Object[] children) {
		Object[] listeners = listenerList.getListenerList();
		TreeModelEvent e = new TreeModelEvent(source, path, childIndices, children);
		for (int i = listeners.length - 2; i >= 0; i -= 2) {
			if (listeners[i] == TreeModelListener.class) {

				switch (changeType) {
				case CHANGED:
					((TreeModelListener) listeners[i + 1]).treeNodesChanged(e);
					break;
				case INSERTED:
					((TreeModelListener) listeners[i + 1]).treeNodesInserted(e);
					break;
				case REMOVED:
					((TreeModelListener) listeners[i + 1]).treeNodesRemoved(e);
					break;
				case STRUCTURE_CHANGED:
					((TreeModelListener) listeners[i + 1]).treeStructureChanged(e);
					break;
				default:
					break;
				}

			}
		}
	}

	protected void fireTreeNodesChanged(Object source, Object[] path, int[] childIndices, Object[] children) {
		fireTreeNode(CHANGED, source, path, childIndices, children);
	}

	protected void fireTreeNodesInserted(Object source, Object[] path, int[] childIndices, Object[] children) {
		fireTreeNode(INSERTED, source, path, childIndices, children);
	}

	protected void fireTreeNodesRemoved(Object source, Object[] path, int[] childIndices, Object[] children) {
		fireTreeNode(REMOVED, source, path, childIndices, children);
	}

	protected void fireTreeStructureChanged(Object source, Object[] path, int[] childIndices, Object[] children) {
		fireTreeNode(STRUCTURE_CHANGED, source, path, childIndices, children);
	}

}

In der Klasse MyDataModel wird das konkrete Datenmodell der Ansicht definiert. D.h. es werden die Spalten inklusive Datentyp festgelegt. Außerdem enthält die Klasse die noch nicht implementierten Methoden des Interfaces TreeModel. Zu beachten ist dabei die Methode isCellEditable. Diese muß den Wert true zurückliefern, damit die Listener auf ein treeExpanded oder treeCollapsed reagieren können. Man könnte in dieser Methode auch nur für die erste Spalte (column) den Wert true zurückliefern und sonst den Wert false.

package de.hameister.treetable;

import java.util.Date;

public class MyDataModel extends MyAbstractTreeTableModel {
	// Spalten Name.
	static protected String[] columnNames = { "Knotentext", "String", "Datum", "Integer" };

	// Spalten Typen.
	static protected Class<?>[] columnTypes = { MyTreeTableModel.class, String.class, Date.class, Integer.class };

	public MyDataModel(MyDataNode rootNode) {
		super(rootNode);
		root = rootNode;
	}

	public Object getChild(Object parent, int index) {
		return ((MyDataNode) parent).getChildren().get(index);
	}


	public int getChildCount(Object parent) {
		return ((MyDataNode) parent).getChildren().size();
	}


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


	public String getColumnName(int column) {
		return columnNames[column];
	}


	public Class<?> getColumnClass(int column) {
		return columnTypes[column];
	}

	public Object getValueAt(Object node, int column) {
		switch (column) {
		case 0:
			return ((MyDataNode) node).getName();
		case 1:
			return ((MyDataNode) node).getCapital();
		case 2:
			return ((MyDataNode) node).getDeclared();
		case 3:
			return ((MyDataNode) node).getArea();
		default:
			break;
		}
		return null;
	}

	public boolean isCellEditable(Object node, int column) {
		return true; // Important to activate TreeExpandListener
	}

	public void setValueAt(Object aValue, Object node, int column) {
	}

}

Bei der Klasse MyDataNode handelt es sich um ein einfaches Value Object mit gettern und settern, die die Daten eines Knoten (node) speichern.

package de.hameister.treetable;

import java.util.Collections;
import java.util.Date;
import java.util.List;

public class MyDataNode {

	private String name;
	private String capital;
	private Date declared;
	private Integer area;

	private List<MyDataNode> children;

	public MyDataNode(String name, String capital, Date declared, Integer area, List<MyDataNode> children) {
		this.name = name;
		this.capital = capital;
		this.declared = declared;
		this.area = area;
		this.children = children;

		if (this.children == null) {
			this.children = Collections.emptyList();
		}
	}

	public String getName() {
		return name;
	}

	public String getCapital() {
		return capital;
	}

	public Date getDeclared() {
		return declared;
	}

	public Integer getArea() {
		return area;
	}

	public List<MyDataNode> getChildren() {
		return children;
	}

	/**
	 * Knotentext vom JTree.
	 */
	public String toString() {
		return name;
	}
}

Jetzt ist der Datenmodell-Teil soweit fertig. In einer main-Klasse TreeTableMain kann jetzt ein Datenmodell mit Knoten erstellt werden. Die Klasse MyTreeTable, die für die Darstellung verantwortlich ist, wird in den nächsten Abschnitten beschrieben. In einem realen Anwendungsfall würde man die Datenstruktur höchstwahrscheinlich nicht in einer Methode createDataStructure zu Beginn komplett erstellen, sondern beispielsweise die Daten bei Bedarf zu Laufzeit, aus einer Datenbank auslesen.

package de.hameister.treetable;

import java.awt.Container;
import java.awt.GridLayout;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;

public class TreeTableMain extends JFrame {

	
	public TreeTableMain() {
		super("Tree Table Demo");
			
		setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

		setLayout(new GridLayout(0, 1));

		MyAbstractTreeTableModel treeTableModel = new MyDataModel(createDataStructure());

		MyTreeTable myTreeTable = new MyTreeTable(treeTableModel);

		Container cPane = getContentPane();
		
		cPane.add(new JScrollPane(myTreeTable));
	
		setSize(1000, 800);
		setLocationRelativeTo(null);
		

	}

	
	private static MyDataNode createDataStructure() {
		List<MyDataNode> children1 = new ArrayList<MyDataNode>();
		children1.add(new MyDataNode("N12", "C12", new Date(), Integer.valueOf(50), null));
		children1.add(new MyDataNode("N13", "C13", new Date(), Integer.valueOf(60), null));
		children1.add(new MyDataNode("N14", "C14", new Date(), Integer.valueOf(70), null));
		children1.add(new MyDataNode("N15", "C15", new Date(), Integer.valueOf(80), null));

		List<MyDataNode> children2 = new ArrayList<MyDataNode>();
		children2.add(new MyDataNode("N12", "C12", new Date(), Integer.valueOf(10), null));
		children2.add(new MyDataNode("N13", "C13", new Date(), Integer.valueOf(20), children1));
		children2.add(new MyDataNode("N14", "C14", new Date(), Integer.valueOf(30), null));
		children2.add(new MyDataNode("N15", "C15", new Date(), Integer.valueOf(40), null));
		
		List<MyDataNode> rootNodes = new ArrayList<MyDataNode>();
		rootNodes.add(new MyDataNode("N1", "C1", new Date(), Integer.valueOf(10), children2));
		rootNodes.add(new MyDataNode("N2", "C2", new Date(), Integer.valueOf(10), children1));
		rootNodes.add(new MyDataNode("N3", "C3", new Date(), Integer.valueOf(10), children2));
		rootNodes.add(new MyDataNode("N4", "C4", new Date(), Integer.valueOf(10), children1));
		rootNodes.add(new MyDataNode("N5", "C5", new Date(), Integer.valueOf(10), children1));
		rootNodes.add(new MyDataNode("N6", "C6", new Date(), Integer.valueOf(10), children1));
		rootNodes.add(new MyDataNode("N7", "C7", new Date(), Integer.valueOf(10), children1));
		rootNodes.add(new MyDataNode("N8", "C8", new Date(), Integer.valueOf(10), children1));
		rootNodes.add(new MyDataNode("N9", "C9", new Date(), Integer.valueOf(10), children1));
		rootNodes.add(new MyDataNode("N10", "C10", new Date(), Integer.valueOf(10), children1));
		rootNodes.add(new MyDataNode("N11", "C11", new Date(), Integer.valueOf(10), children1));
		rootNodes.add(new MyDataNode("N12", "C7", new Date(), Integer.valueOf(10), children1));
		rootNodes.add(new MyDataNode("N13", "C8", new Date(), Integer.valueOf(10), children1));
		rootNodes.add(new MyDataNode("N14", "C9", new Date(), Integer.valueOf(10), children1));
		rootNodes.add(new MyDataNode("N15", "C10", new Date(), Integer.valueOf(10), children1));
		rootNodes.add(new MyDataNode("N16", "C11", new Date(), Integer.valueOf(10), children1));
		MyDataNode root = new MyDataNode("R1", "R1", new Date(), Integer.valueOf(10), rootNodes);

		return root;
	}

	public static void main(final String[] args) {
		Runnable gui = new Runnable() {

			public void run() {
				try {
					UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
				} catch (Exception e) {
					e.printStackTrace();
				}
				new TreeTableMain().setVisible(true);
			}
		};
		SwingUtilities.invokeLater(gui);
	}
}

Da sich die TreeTable aus einer JTree-Komponente und einer JTable-Komponente zusammensetzt, muß dafür gesorgt werden, daß bei der Selektion des Baums oder der Tabelle immer eine durchgehende Zeile markiert wird. Um das zu gewährleisten erstellt man eine Klasse MyTreeTableSelectionModel, die das DefaultTreeSelectionModel erweitert. Dieses SelectionModel wird später dem JTree und dem JTable zugewiesen.

package de.hameister.treetable;

import javax.swing.ListSelectionModel;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.tree.DefaultTreeSelectionModel;

public class MyTreeTableSelectionModel extends DefaultTreeSelectionModel {
	
	public MyTreeTableSelectionModel() {
		super();

		getListSelectionModel().addListSelectionListener(new ListSelectionListener() {
			@Override
			public void valueChanged(ListSelectionEvent e) {
				
			}
		});
	}
	
	ListSelectionModel getListSelectionModel() {
		return listSelectionModel;
	}
}

Um das Aufklappen des Baumes zu ermöglichen, benötigt man einen AbstractCellEditor. Deshalb legt man eine Klasse MyTreeTableCellEditor an, die AbstractCellEditor erweitert und das TableCellEditor-Interface implementiert. Die einzige Funktion der Klasse MyTreeTableCellEditor ist das Weiterleiten eines Doppelklicks an den Baum. In der Methode isCellEditable wird überprüft, ob die erste Spalte (column1) angeklickt wurde. Wenn das der Fall ist, wird ein Doppelklick an dem tree weitergeleitet, so daß die ExpansionListener reagieren können.

package de.hameister.treetable;

import java.awt.Component;
import java.awt.event.MouseEvent;
import java.util.EventObject;

import javax.swing.AbstractCellEditor;
import javax.swing.JTable;
import javax.swing.JTree;
import javax.swing.table.TableCellEditor;

public class MyTreeTableCellEditor extends AbstractCellEditor implements TableCellEditor {

	private JTree tree;
	private JTable table;

	public MyTreeTableCellEditor(JTree tree, JTable table) {
		this.tree = tree;
		this.table = table;
	}

	public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int r, int c) {
		return tree;
	}

	public boolean isCellEditable(EventObject e) {
		if (e instanceof MouseEvent) {
			int colunm1 = 0;
			MouseEvent me = (MouseEvent) e;
			int doubleClick = 2;
			MouseEvent newME = new MouseEvent(tree, me.getID(), me.getWhen(), me.getModifiers(), me.getX() - table.getCellRect(0, colunm1, true).x, me.getY(), doubleClick, me.isPopupTrigger());
			tree.dispatchEvent(newME);
		}
		return false;
	}

	@Override
	public Object getCellEditorValue() {
		return null;
	}

}

Da bei Java Swing auch die GUI-Komponenten immer noch ein Model benötigten, welches ungleich dem eigentlich Datenmodell ist, wird jetzt noch eine Klasse MyTreeTableModelAdapter, welche von AbstractTableModel erbt, angelegt. Diese Klasse wird später in der Klasse MyTreeTable als Model für die JTable verwendet. Wenn die TreeTable später nach Werten fragt, die angezeigt werden sollen, muß unterschieden werden, ob die angefragten Werte vom Tree oder direkt von Datenmodell MyAbstractTreeTableModel geliefert werden können. Außerdem wird in der Klasse noch der TreeExpansionListener erzeugt und registriert. Dieser reagiert auf Klicks im Baum und sorgt dafüf, daß der Baum auf- und zugeklappt wird.

package de.hameister.treetable;

import java.awt.Rectangle;

import javax.swing.JTree;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeExpansionListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.tree.TreePath;

public class MyTreeTableModelAdapter extends AbstractTableModel {
	
	 JTree tree;
	MyAbstractTreeTableModel treeTableModel;

	public MyTreeTableModelAdapter(MyAbstractTreeTableModel treeTableModel, JTree tree) {
		this.tree = tree;
		this.treeTableModel = treeTableModel;

		tree.addTreeExpansionListener(new TreeExpansionListener() {
			public void treeExpanded(TreeExpansionEvent event) {
				fireTableDataChanged();
			}

			public void treeCollapsed(TreeExpansionEvent event) {
				fireTableDataChanged();
			}
		});
	}


	
	public int getColumnCount() {
		return treeTableModel.getColumnCount();
	}

	public String getColumnName(int column) {
		return treeTableModel.getColumnName(column);
	}

	public Class<?> getColumnClass(int column) {
		return treeTableModel.getColumnClass(column);
	}

	public int getRowCount() {
		return tree.getRowCount();
	}

	protected Object nodeForRow(int row) {
		TreePath treePath = tree.getPathForRow(row);
		return treePath.getLastPathComponent();
	}

	public Object getValueAt(int row, int column) {
		return treeTableModel.getValueAt(nodeForRow(row), column);
	}

	public boolean isCellEditable(int row, int column) {
		return treeTableModel.isCellEditable(nodeForRow(row), column);
	}

	public void setValueAt(Object value, int row, int column) {
		treeTableModel.setValueAt(value, nodeForRow(row), column);
	}
}

Als letztes muß jetzt noch der JTree und die JTable erstellt werden. Die Tree-Komponente MyTreeTableCellRenderer erbt von JTree und implementiert das TableCellRenderer-Interface. Diese Klasse sorgt dafür, daß die Zeilenhöhen von Tree and Table gleich sind und das bei der Selektion die Hintergrundfarben richtig gesetzt werden. Außerdem wird dafür gesorgt, daß die Elemente des Baums je nach Ebene richtig eingerückt werden.

 
package de.hameister.treetable;

import java.awt.Component;
import java.awt.Graphics;

import javax.swing.JTable;
import javax.swing.JTree;
import javax.swing.table.TableCellRenderer;
import javax.swing.tree.TreeModel;


public class MyTreeTableCellRenderer extends JTree implements TableCellRenderer {
	/** Die letzte Zeile, die gerendert wurde. */
	protected int visibleRow;
	
	private MyTreeTable treeTable;
	
	public MyTreeTableCellRenderer(MyTreeTable treeTable, TreeModel model) {
		super(model);
		this.treeTable = treeTable;
		
		// Setzen der Zeilenhoehe fuer die JTable
		// Muss explizit aufgerufen werden, weil treeTable noch
		// null ist, wenn super(model) setRowHeight aufruft!
		setRowHeight(getRowHeight());
	}

	/**
	 * Tree und Table muessen die gleiche Hoehe haben.
	 */
	public void setRowHeight(int rowHeight) {
		if (rowHeight > 0) {
			super.setRowHeight(rowHeight);
			if (treeTable != null && treeTable.getRowHeight() != rowHeight) {
				treeTable.setRowHeight(getRowHeight());
			}
		}
	}

	/**
	 * Tree muss die gleiche Hoehe haben wie Table.
	 */
	public void setBounds(int x, int y, int w, int h) {
		super.setBounds(x, 0, w, treeTable.getHeight());
	}

	/**
	 * Sorgt fuer die Einrueckung der Ordner.
	 */
	public void paint(Graphics g) {
		g.translate(0, -visibleRow * getRowHeight());
		
		super.paint(g);
	}
	
	/**
	 * Liefert den Renderer mit der passenden Hintergrundfarbe zurueck.
	 */
	public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
		if (isSelected)
			setBackground(table.getSelectionBackground());
		else
			setBackground(table.getBackground());

		visibleRow = row;
		return this;
	}
}

Nachdem das Datenmodell, die Hilfskomponenten und die Main-Klasse stehen, fehlt jetzt noch die eigentliche TreeTable. Dazu wird die Klasse MyTreeTable angelegt. Diese erbt von JTable. Da bei Java Mehrfachvererbung nicht möglich ist, wird die Tree-Komponente über eine Assoziation in der Klasse MyTreeTableCellRenderer eingebunden. Das Datenmodell MyAbstractTreeTableModel wird sowohl an den MyTreeTableCellRenderer (Tree) als auch an das Object MyTreeTableModelAdapter (Table) übergeben. Als Modell wird die Klasse MyTreeTableModelAdapter gesetzt. Für das gleichzeitige Selektieren von Tree und Table wird das MyTreeTableSelectionModel für Tree und Table verwendet. Dann muß noch ein Standard-Renderer für den Tree festgelegt und ein Default-Editor für die Table gesetzt werden.

package de.hameister.treetable;

import java.awt.Dimension;

import javax.swing.JTable;

public class MyTreeTable extends JTable {

	private MyTreeTableCellRenderer tree;
	
	
	public MyTreeTable(MyAbstractTreeTableModel treeTableModel) {
		super();

		// JTree erstellen.
		tree = new MyTreeTableCellRenderer(this, treeTableModel);
		
		// Modell setzen.
		super.setModel(new MyTreeTableModelAdapter(treeTableModel, tree));
		
		// Gleichzeitiges Selektieren fuer Tree und Table.
		MyTreeTableSelectionModel selectionModel = new MyTreeTableSelectionModel();
		tree.setSelectionModel(selectionModel); //For the tree
		setSelectionModel(selectionModel.getListSelectionModel()); //For the table

		
		// Renderer fuer den Tree.
		setDefaultRenderer(MyTreeTableModel.class, tree);
		// Editor fuer die TreeTable
		setDefaultEditor(MyTreeTableModel.class, new MyTreeTableCellEditor(tree, this));
		
		// Kein Grid anzeigen.
		setShowGrid(false);

		// Keine Abstaende.
		setIntercellSpacing(new Dimension(0, 0));

	}
}

Wenn die einzelnen Klasse nun mit javac kompiliert werden, dann sollte man eine funktionierende TreeTable-Komponente haben. Wie aber oben schon angedeutet, ist es nicht zu verstehen, daß so eine Komponente nicht Bestandteil von Java ist. Unglaublich finde ich, daß man 8 Klassen implementieren muß, um eine wirklich einfache TreeTable zu erhalten. Anzumerken ist, daß Funktionalitäten, wie

  • Editieren
  • Verbundene Zeilen und Spalten
  • Eingefärbte Zeilen und Spalten
  • SwingWorker für langlaufende Expansion-Events
  • Icons im Baum
  • Swing-Komponenten in der Table (ComboBox, Images, ...)

überhaupt noch nicht berücksichtigt sind. Man kann sich sicher leicht vorstellen, wie viel Quellcode und Zeit in diese Features fließen müßten, damit man eine funktionierende Komponente hat.