# JButton auf JPanel in JTable (Darstellungs-/Event-Probleme)



## Kaffeemaschinist (10. Feb 2009)

Hallo!

Ich habe zu diesem Thema die letzten zwei Tage jede Menge Posts hier gelesen, aber so richtig zum Schluss kommt keines.

Es geht darum, dass ein *JTable* angezeigt wird, der 1 Spalte mit einem *JButton* besitzt (welcher der Darstellung wg. auf einem *JPanel* sitzt). Der Button ist sozusagen ein Singleton, da er nur dafür da ist, eine Aktion auszulösen, dessen wichtige Daten ich mir später über den aktuellen JTable-Zustand holen möchte.

Folgende Probleme treten auf: Bei Druck auf den Button funktioniert die Sache, drückt man mit der Maus aber auf das darunterliegende JPanel, dann passieren die wildesten Sachen, vor allem Darstellungsfehler. In meinem Beispiel ist ein sehr einfaches *TableModel*, ein *TableCellRenderer*, ein *TableCellEditor*, die Button-Klasse und die Start-Klasse enthalten.

Meine Frage ist, warum der Klick auf das Panel so seltsame Aktionen auslöst und ob mein Ansatz evtl. in die richtige Richtung geht.


Den Quellcode hab ich mal hier ins Forum hochgeladen:


_Starter.java_

```
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.event.ActionListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;

public class Starter extends JFrame {
	
	public Starter () {
		
		this.setLayout(new GridLayout(2,1));
		
		/* table init */
		TableModel model = new TableModel((ActionListener)null, "actionCommand", "x"); 
        JTable table = new JTable(model);
        table.setPreferredScrollableViewportSize(new Dimension (300,60));
        table.setFillsViewportHeight(true);
        
        /* set default renderer for table buttons */
        table.setDefaultRenderer(JTableButton.class, new ButtonRenderer());
        table.setDefaultEditor  (JTableButton.class, new ButtonEditor());

        /* add table on a scroll pane to this panel */
        this.add(new JScrollPane(table));
        this.add(new JButton("A useless button"));
        
        this.pack();
        this.setVisible(true);
	}

	public static void main(String[] args) {
		Starter starter = new Starter();
	}

}
```

_JTableButton.java_

```
import java.awt.event.ActionListener;

import javax.swing.JButton;

public class JTableButton
extends JButton {
	
	public JTableButton (
			final String caption,
			final String actionCmd,
			final ActionListener actionListener) {
		
		super(caption);
		this.addActionListener(actionListener);
		this.setActionCommand(actionCmd);
	}

}
```

_ButtonRenderer.java_

```
import java.awt.Component;
import java.awt.FlowLayout;

import javax.swing.JPanel;
import javax.swing.JTable;
import javax.swing.table.TableCellRenderer;

public class ButtonRenderer extends JPanel implements TableCellRenderer {
   
	public ButtonRenderer() {
		super(new FlowLayout(FlowLayout.CENTER,0,0));
		this.setOpaque(true);
	}

	public Component getTableCellRendererComponent(
			final JTable table,			final Object value,
			final boolean isSelected,	final boolean hasFocus,
			final int row,				final int column) {

		JTableButton tableButton = (JTableButton) value; 
		this.add(tableButton);
		
		/* emphasize selection by changing label's background color */
		if (isSelected) {
			this.setBackground(table.getSelectionBackground());
		} else {
			this.setBackground(table.getBackground());
		}		
		return this;
	}
}
```

_ButtonEditor.java_

```
import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

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

public class ButtonEditor extends AbstractCellEditor implements TableCellEditor, ActionListener {
	
	private JTableButton originalButton;
	private JPanel buttonPanel;
	
	private boolean isPushed;
	
	public ButtonEditor () {
		this.buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER,0,0));
		this.buttonPanel.setOpaque(true);
	}
	
	public Component getTableCellEditorComponent(
			final JTable table,
			final Object value,
			final boolean isSelected,
			final int row,
			final int column) {
		
		/* return same button on panel */
		this.originalButton = (JTableButton) value;
		this.originalButton.addActionListener(this);	
		this.buttonPanel.add(this.originalButton);	
		isPushed = true;
	
		return this.buttonPanel;
		
	}

	public Object getCellEditorValue() {

		if (isPushed) {
			System.out.println("getCellEditorValue()");
		}
		isPushed = false;
		return this.originalButton;
	}

	public boolean stopCellEditing() {
		/* TODO: oeffne hier einen Dialog */ 
		/* ... */
		
		isPushed = false;
		return super.stopCellEditing();
	}

	protected void fireEditingStopped() {
		super.fireEditingStopped();
	}

	public void actionPerformed(ActionEvent e) {
		this.fireEditingStopped();
	}
}
```

_JTableModel.java_

```
import java.awt.Color;
import java.awt.Insets;
import java.awt.event.ActionListener;

import javax.swing.BorderFactory;
import javax.swing.table.AbstractTableModel;

public class TableModel extends AbstractTableModel {

	private final String[] columnNames = {"Delete field", "Field name", "Show distinct value"};
	
	/** button for showing "distinct values" */
	private JTableButton distinctValuesButton; 
	
	private Object[][] data = {
		{false, "name 1", null},{false, "name 2", null},{false, "name 3", null},
	};
	
	public TableModel (
			final ActionListener actionListener,final String actionCommand,	final String caption) {
		
		/* create button */
		this.distinctValuesButton = new JTableButton(caption, actionCommand,actionListener);
		this.distinctValuesButton.setMargin(new Insets(0,0,0,0));
		this.distinctValuesButton.setBorder(BorderFactory.createLineBorder(Color.BLACK));
		
		/* set button */
		for (int i=0; i<data.length; i++) {
			data[i][2] = this.distinctValuesButton;
		}	
	}
	
	public int getColumnCount() {
		return this.columnNames.length;
	}

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

	public Object getValueAt(final int rowIndex,final int columnIndex) {
		return this.data[rowIndex][columnIndex];
	}

	@Override
	public Class<?> getColumnClass(	final int columnIndex) {
		return getValueAt(0, columnIndex).getClass();
	}

	@Override
	public String getColumnName(final int column) {
		return this.columnNames[column];
	}

	@Override
	public boolean isCellEditable(final int rowIndex,final int columnIndex) {
		return true;
	}

	@Override
	public void setValueAt(	final Object value, final int rowIndex,	final int columnIndex) {
		this.data[rowIndex][columnIndex] = value;
		/* fire event */
		this.fireTableCellUpdated(rowIndex,columnIndex);
	}

}
```


----------



## Kaffeemaschinist (11. Feb 2009)

Da das mit der Darstellung nach ewigen Rumgebaue (Swing-Update-Methoden aufrufen, JLabel bereinigen vor neuem Button, ein Button pro Tabellenzeile) immer noch nicht klappt, bin ich jetzt den Umweg über einen *MouseListener* gegangen.

Sprich: Die Tabelle hört MouseClicks auf die Tabellenspalten ab und ruft im Falle, dass ein Click auf eine JButton-Spalte gemacht wurde, die *doClick*-Methode des Buttons auf. Der MouseListener sieht in etwa so aus:


```
public void mouseClicked(final MouseEvent event) {
		
		if (! (event.getSource() instanceof JTable)) {
			LOGGER.fatal("Table mouse listener listens to " 
					+ event.getSource().getClass()
					+ "instead to "+JTable.class);
		} else {
			JTable table = (JTable) event.getSource();
			Object selectedObject = 
				table.getValueAt(table.getSelectedRow(),table.getSelectedColumn());
			
			/* send click event to button */
			if (selectedObject instanceof JTableButton) {
				((JTableButton)selectedObject).doClick();
			}
		}
		
	}
```


Das ist zwar bei weitem keine gute Lösung (etwas durch den Rücken in die Brust geschossen), aber es funktioniert wunderbar und beeinträchtigt die Editierbarkeit anderer Felder nicht. Wichtig ist noch, dass man die Spalten der Tabelle, auf die das mouseClicked-Event anspringen soll (im Beispiel Spalte mit Index 2) als "nicht editierbar" kennzeichnet, also das *TableModel* in dem Falle ein _false_ zurückliefert. Damit spart man sich auch den speziellen *TableCellRenderer*.


Case solved, aber wer noch Anregungen hat, wie es auch mit einem CellRenderer funktioniert, dann her damit


----------



## Ebenius (11. Feb 2009)

Das Problem ist, dass Du die selbe Buttoninstanz für alles benutzt. Folgender Ablauf wird dabei zum Problem: 
Du klickst in eine Zelle (A): Der Button lebt auf dem Panel welches auf der JTable als Editorkomponente liegt
Du klickst in eine andere Zelle (B): Schritt 1 wiederholt sich für die Zelle B
Danach wird die zuvor editierte Zelle (A) per CellRenderer neu gezeichnet. Dazu wird der selbe Button auf das JPanel im CellRenderer gelegt und ist damit nicht mehr im JPanel des Editors, weil eine Komponente immer maximal einen Parent haben kann.
Ganz allgemein: Man sollte niemals die selben Komponenteninstanzen zum Editieren und zum darstellen benutzen. Das gibt meist Probleme.

Hoffe, Dir geholfen zu haben.
Ebenius


----------



## Kaffeemaschinist (12. Feb 2009)

Aha, ok, verstanden

Ich hatte nur gelesen, dass die Komponente, die von
"getTableCellRenderer/EditorComponent" zurückgeliefert wird, ihren Lebenszyklus beim nächsten Aufruf schon wieder verwirkt hat.

Demnach könnte ich sozusagen mit 2 Buttons auskommen: Einen fürs Editieren, einen fürs Rendern, richtig?


----------



## Ebenius (12. Feb 2009)

Kaffeemaschinist hat gesagt.:
			
		

> Demnach könnte ich sozusagen mit 2 Buttons auskommen: Einen fürs Editieren, einen fürs Rendern, richtig?


Jupp. Und üblicher Weise gehören diese Buttons dann auch je 1× in den Renderer und 1× in den Editor; also eben nicht in's Modell. 

Ebenius


----------



## André Uhres (12. Feb 2009)

1234567890


----------



## Kaffeemaschinist (13. Feb 2009)

Hab mir das Beispiel mal angeschaut.

Ich habe aber mal eine Frage zu diesem Teilstück hier:


```
public boolean shouldSelectCell(EventObject anEvent) {
		
		/* falls grad im Edit-Moduds && es wurde ein MousePressed-Event ausgelöst ... */
		if( editorComponent != null && anEvent instanceof MouseEvent
				&& ((MouseEvent)anEvent).getID() == MouseEvent.MOUSE_PRESSED ) {
			
		
			Component dispatchComponent = SwingUtilities.getDeepestComponentAt(editorComponent, 3, 3 );
			MouseEvent e = (MouseEvent)anEvent;
			/* schicke 2 neue MouseEvents los ... 
			 * 1. Maus wird über angeklickten Objekt "losgelassen" */
			MouseEvent e2 = new MouseEvent( dispatchComponent, MouseEvent.MOUSE_RELEASED,
					e.getWhen() + 100000, e.getModifiers(), 3, 3, e.getClickCount(),
					e.isPopupTrigger() );
			dispatchComponent.dispatchEvent(e2);
			/* 2. Maus führt vollständigen Click über Position aus */
			e2 = new MouseEvent( dispatchComponent, MouseEvent.MOUSE_CLICKED,
					e.getWhen() + 100001, e.getModifiers(), 3, 3, 1,
					e.isPopupTrigger() );
			dispatchComponent.dispatchEvent(e2);
		}
		return false;
	}
```

Soweit ich verstanden habe, gibt shouldSelectCell an, ob die angeklickte TableCell angewählt werden kann. Aber warum die zwei MouseEvents weitergeben?

Asso: wenn man dieses Teilstück auskommentiert (bis auf _return false;_), dann funktioniert es auch.


Nachtrag:

Wenn ich den Code drin lasse, werden geworfen:
- MOUSE_PRESSED (Auslöser ist der Click)
- MOUSE_RELEASED (Auslöser ist der Code)
- MOUSE_CLICKED (Aulsöser ist auch der Code)

Wenn ich auskommentiere, dann werden nur 2 Events geworfen:
- MOUSE_PRESSED (Auslöser ist der Click)
- MOUSE_RELEASED (Auslöser ist der Click)


Jetzt versteh ich erstens nicht, warum der CLICKED-Event hintergeworfen werden muss vom Code (muss auf ein PRESS+RELEASE immer explizit ein CLICK kommen?) und warum der Code den originalen RELEASED-Event verschluckt und neu generiert (allerdings mit falschen X-Y-Koordinaten, also vermutlich eh unbrauchbar).


----------



## Ebenius (13. Feb 2009)

Hier wird erst ein MOUSE_RELEASED und dann ein MOUSE_CLICKED Event für die dispatchComponent vorgetäuscht.

Ebenius


----------



## André Uhres (13. Feb 2009)

1234567890


----------



## Kaffeemaschinist (13. Feb 2009)

Ok, aber an welcher Stelle im Code wird der ursprüngliche MouseRelease verschluckt? Wie gesagt, wenn ich's auskommentiere, läuft der MouseRelease von ganz allein durch.

Und ist es nicht gefährlich, wenn man die Mouse-Position mit 3,3 anigbt anstatt die richtigen Werte zu nutzen?


----------



## André Uhres (14. Feb 2009)

1234567890


----------



## Ebenius (14. Feb 2009)

André Uhres hat gesagt.:
			
		

> Hier werden dummy mouse_release und mouse_clicked Ereignisse auf der Komponente erzeugt.
> Der Grund hierfür ist, dass die TableUI Klasse das Ereignis, das den Edit einleitet, an die Editorcomponent weiterleitet. Da viele Komponenten mouse_released und mouse_clicked Ereignisse erwarten, bedienen wir sie auf diese Art.


Da fällt mir grad ein, dass ich nochwas erwähnen wollte. Ich hab mir den Link oben nicht angesehen. Vielleicht wird die Methode ja von isCellEditable(...) oder so aufgerufen. Falls dem so ist, ist dieser Hinweis unwichtig. Falls nicht: Das steht in der API-Doc JTable.editCellAt(int, int, EventObject): 





> e - event to pass into shouldSelectCell; note that as of Java 2 platform v1.2, the call to shouldSelectCell is no longer made


Ebenius


----------



## André Uhres (14. Feb 2009)

1234567890


----------



## Kaffeemaschinist (14. Feb 2009)

Ok, liegt vllt. auch daran, dass ich mich mit dem Event-Modell von AWT/Swing noch nicht ausreichend beschäftigt habe, aber die Frage zu folgender Zeile bleibt bestehen:


```
Component dispatchComponent = SwingUtilities.getDeepestComponentAt(editorComponent, 3, 3 );
```

Ich vermute mal, du suchst an dieser Stelle nach der Componente, an die du den Klick weiterleitest. _3,3_ ist dabei eine Koordinate in einem Pixelsystem? Wenn ja, warum gerade 3,3 und 4,4 oder Ähnliches? Was würde passieren, hätte die Komponente eine Auflösung von nur 1,1 Punkte?


----------



## André Uhres (14. Feb 2009)

1234567890


----------



## Kaffeemaschinist (14. Feb 2009)

Deswegen also fixe Werte ...

Es ist also gar nicht nötig, die Komponente genau unter der Mausposition zu bekommen, sondern lediglich, die tiefste Komponente in der Zelle, die den Klick dann weiterverteilt, sobald sie ihn bekommen hat, oder?

Was würde passieren, hätte ich zwei Buttons auf einem Panel in einer Tabellenzelle? Würde das theoretisch Probleme bereiten?

Bin ich wieder neugierig ...


----------



## Ebenius (14. Feb 2009)

André Uhres hat gesagt.:
			
		

> Ebenius, dein Hinweis ist wohl in diesem Zusammenhang nicht relevant, denn jedesmal wenn wir auf eine andere Zelle klicken, wird "shouldSelectCell" von der standard API aufgerufen, was sie ja auch logischerweise tun soll.


Die JTable tut das nicht mehr... Aber das BasicTableUI tut's. Dort hatte ich nicht nachgesehen. Alright then.

Danke,
Ebenius


----------

