= JTable wie ein Spreadsheet benutzen =

Der eigentliche Sinn einer editierbaren JTable ist meiner Meinung nach, dem Benutzer halbwegs das Gefühl zu geben, das er beim editieren eines Spreadsheets (also in MS Excel oder ~OpenOffice.org Calc) hat. Leider ist das "Feeling" etwas anders, was meiner Meinung nach unnötig die Benutzer verstört. Ich möchte jeden Unterschied, der mich stört, hierbei einzeln behandeln.

== sofortiges löschen beim editieren ==

Wenn man eine Textzelle (z.B. mit der Maus) aktiviert hat und dann eine alphanumerische Taste drückt, wird diese Taste an das Ende des bestehenden Textes angefügt. Das gleiche passiert beim Drücken von F2. In OOo Calc ist es so, daß beim einfachen Drücken der bestehende Wert immer überschrieben wird und man F2 drücken muss, um den Wert editieren zu können.

Dieses Problem ist aufgrund der Struktur der JTable-Klasse gar nicht so trivial, wie ich dachte. Deshalb dokumentiere ich hier einen Teil des Weges, den ich zu meiner Lösung genommen habe, um ggf. später beim nachbessern nicht alles neu suchen zu müssen:

Zuerstmal steht im [Swingwiki|http://www.swingwiki.org/best:edit_cells_like_excel], daß die eine Lösung hätten. Diese Lösung basiert darauf, daß ein ~DefaultCellEditor gesetzt wird, der sich beim Aufruf automatisch komplett selektiert. Das hilft zwar, wenn ich eine alphanumerische Taste drücke, selektiert aber bei F2 ebenfalls den gesamten Text und verhindert somit, das man einen vorhandenen Zelleninhalt editieren kann.

In einem [Sun-Forum|http://forums.sun.com/thread.jspa?forumID=57&threadID=752727] steht ein Quelltext, von dem ich überhaupt nicht verstehe, was der soll. Ich habe ihn nicht ausprobiert, nach der Beschreibung sollte er aber mit meinem Problem zu tun haben.

In einem [Artikel|http://www.informit.com/articles/article.aspx?p=24130&seqNum=11] mit einem relativ einfachen Niveau fand ich den Hinweis, wie Funktionstasten und alphanumerische Tasten überhaupt behandelt werden und wo der Unterschied liegt: Scheinbar wird in ~BasicTableUI entschieden, ob es sich um eine einzufügende Taste oder eine Sondertaste (z.B. Pfeiltaste oder F2) handelt. F2 wird dann in ~BasicTableUI.actionPerformed als Action START_EDITING behandelt, normale Tasten in ~JTable.processKeyBinding().

Parallel fand ich in einem [Forumsthread zum Thema|http://forums.java.net/jive/thread.jspa?threadID=42682&tstart=0] einen Beitrag von "oswald", der versucht hatte, ~processKeyBinding() zu überladen, aber mit dieser Idee nicht vorwärtsgekommen war. Nachdem ich das durchdacht hatte, merkte ich, daß dieser Gedanke der beste war, den ich gesehen hatte. Ich beschäftigte mit intensiver mit dieser Methode, fand das Problem, das oswald hatte, einen Workaround hierfür und habe folgende Lösung implementiert:
{{{
public class SpreadsheetJTable extends JTable {

	protected boolean processKeyBinding(KeyStroke ks, KeyEvent e,
			int condition, boolean pressed) {
		boolean retValue = false;
		/*
		 * Der erste Teil ist aus JComponent.processKeyBinding entnommen. Ich
		 * würde das ja gerne aufrufen, aber das kann ich nicht, weil es
		 * überlagert ist. (Oder geht das doch irgendwie?)
		 */
		InputMap map = getInputMap(condition);
		ActionMap am = getActionMap();
		if (map != null && am != null && isEnabled()) {
			Object binding = map.get(ks);
			Action action = (binding == null) ? null : am.get(binding);
			if (action != null) {
				retValue = SwingUtilities.notifyAction(action, ks, e, this, e
						.getModifiers());
			}
		}

		/*
		 * Der Inhalt dieses if-Blocks ist aus der super-Methode entnommen. Ich
		 * erzeuge hier die Editor-Komponente genauso wie im Original. Dann
		 * selektiere ich allerdings den ganzen Text wie in
		 * Spreadsheet-Applikationen üblich.
		 */
		// Start editing when a key is typed. UI classes can disable this
		// behavior
		// by setting the client property JTable.autoStartsEdit to
		// Boolean.FALSE.
		if (!retValue
				&& condition == WHEN_ANCESTOR_OF_FOCUSED_COMPONENT
				&& isFocusOwner()
				&& !Boolean.FALSE
						.equals(getClientProperty("JTable.autoStartsEdit"))) {
			// We do not have a binding for the event.
			Component editorComponent = getEditorComponent();
			if (editorComponent == null) {
				// Only attempt to install the editor on a KEY_PRESSED,
				if (e == null || e.getID() != KeyEvent.KEY_PRESSED) {
					return false;
				}
				// Don't start when just a modifier is pressed
				int code = e.getKeyCode();
				if (code == KeyEvent.VK_SHIFT || code == KeyEvent.VK_CONTROL
						|| code == KeyEvent.VK_ALT) {
					return false;
				}
				// Try to install the editor
				int leadRow = getSelectionModel().getLeadSelectionIndex();
				int leadColumn = getColumnModel().getSelectionModel()
						.getLeadSelectionIndex();
				if (leadRow != -1 && leadColumn != -1 && !isEditing()) {
					if (!editCellAt(leadRow, leadColumn, e)) {
						return false;
					}
				}
				editorComponent = getEditorComponent();
				if (editorComponent == null) {
					return false;
				}
				// -TB-
				// select everything (like Spreadsheet Apps do)
				if (editorComponent instanceof JTextField) {
					((JTextField) editorComponent).selectAll();
				}
			}
		}
		/*
		 * Jetzt rufe ich den ganzen obigen Sermon u.U. nochmal auf, aber das
		 * kann ich nicht vermeiden. In dieser super-Methode wird das
		 * processKeyBinding des Editors aufgerufen, das ich nicht anders
		 * aufrufen darf, weil Swing das leider nicht public gemacht hat. :-(
		 */
		if (!retValue)
			retValue = super.processKeyBinding(ks, e, condition, pressed);
		return retValue;
	}
}

}}}

Das Ergebnis ist jetzt, das man einerseits mit einer normalen Taste den Zellinhalt überschreibt, aber andererseits mit F2 auch den vorhandenen Inhalt editieren kann! -- ThomasBayen


== Lösung des löschen-beim-editieren Problems mit ~TableCellEditor ==

Eine andere Lösung hat JensKapitza hier vorgestellt. Nach meinen Versuchen funktioniert dieses genauso gut wie meine Lösung. Mir fällt es auch schwer, eine der beiden Lösungen für "besser" zu erklären. ''(Lediglich habe ich bei Jens Lösung das Gefühl, daß man, wenn man dort Delegation durch Ableitung ersetzt, noch einige Zeilen Code sparen kann.)'' -- ThomasBayen

Man kann es auch mal ein wenig anders für jeden Type (hier geht es ja nur um String.class) machen es folg ein kleines Beispiel

{{{
// GPL v2 oder höher ;) 
// (c) 2008 Jens Kapitza ;)
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.KeyEvent;
import java.util.EventObject;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.event.CellEditorListener;
import javax.swing.table.TableCellEditor;
import javax.swing.text.JTextComponent;

public class H {
    public static void main(String[] args) {
// einfaches fenseter
	JFrame f = new JFrame("a");
// ein paar test daten
	String[][] d = new String[][] { { "A", "B", "C" }, { "A", "V", "2" } };
	JTable t = new JTable(d, new String[] { "AAAA", "BBBBBBBBB",
		"CCCCCCCCCCCCCCCC" });
// damit man was sieht 
	JScrollPane p = new JScrollPane(t);
// nun den editor holen
	final TableCellEditor tce = t.getDefaultEditor(Object.class);

// und ein wenig ändern
// siehe flag select und getTableCellEditorComponent sowie isCellEditable 
	t.setDefaultEditor(Object.class, new TableCellEditor() {
// ich muss wissen ob ich diese componente nun überschreiben soll oder nicht wie bei oo calc wenn a getippt wird ist es überschreiben bei F2 z.b. nur ein anhängen!
	    private boolean select = false;

	    @Override
	    public Component getTableCellEditorComponent(JTable table,
		    Object value, boolean isSelected, int row, int column) {
// componente holen
		Component cmp = tce.getTableCellEditorComponent(table, value,
			isSelected, row, column);

		// wenn !f2 dann alles selct
// ist es eine textcomponente nur sie kennt selectAll !!!!!
		if (cmp instanceof JTextComponent) {
		    if (select) { // wenn es selectiert werden soll -> isCellEditable setzt den flag 
// dann überschreiben ermöglichen!
			((JTextComponent) cmp).selectAll();
			select = false;
		    }
		}

		return cmp;
	    }

	    @Override
	    public void addCellEditorListener(CellEditorListener l) {
		tce.addCellEditorListener(l);
	    }

	    @Override
	    public void cancelCellEditing() {
		tce.cancelCellEditing();
	    }

	    @Override
	    public Object getCellEditorValue() {
		return tce.getCellEditorValue();
	    }

	    @Override
	    public boolean isCellEditable(EventObject anEvent) {
		if (anEvent instanceof KeyEvent) { // ist es ein Tasten event 
// wer lust hat kann ja noch die tasten codes abfangen! --> f2 ist kein KeyEvent !!!
// hab aber auch nicht weiter nachgeguckt 
		    select = true;
		}
// ActionEvent kommt raus und dass ist ein AWT event und je nach dem welches kann es ein Keyevent, .... sein
		System.out.println(anEvent);
// mehr will ich nicht machen
		return tce.isCellEditable(anEvent);
	    }

	    @Override
	    public void removeCellEditorListener(CellEditorListener l) {
		tce.removeCellEditorListener(l);
	    }

	    @Override
	    public boolean shouldSelectCell(EventObject anEvent) {
		return tce.shouldSelectCell(anEvent);
	    }

	    @Override
	    public boolean stopCellEditing() {
		return tce.stopCellEditing();
	    }

	});
	p.setPreferredSize(new Dimension(200, 400));
	f.add(p);
	f.pack();
// anzeigen und in der console gucken
	f.setVisible(true);

    }
}


}}}

----
[{Tag Java Swing}]