# Event bei Änderung der Spaltenbreite in einem JTable



## jueki (10. Aug 2010)

???:LHallo,

für mein Projekt ist es notwendig, dass ich über ein Event, also einen irgendwie gearteten ActionListener oder ähnliches mitbekomme, wenn der Anweder mit der Maus die Spaltenbreite in einer JTable ändert. Meine Tabelle ist in einer JScrollPane eingebettet. Aber weder JTable noch JScrollPane noch mein DefaultTableModel bieten ein geeignetes Event.

Hat jemand eine Lösung?


----------



## Michael... (10. Aug 2010)

Das könnte mit einem TableColumnModelListener am ColumnModel der JTable funktionieren.
Darf man fragen wozu? Das hört sich nach einem Workaround an.


----------



## jueki (10. Aug 2010)

Ja, man darf: ich habe eine horrende Menge Messdaten die in einer Tabelle wie auch graphisch dargestellt werden sollen. Um den Arbeitsspeicher nicht unnötig zu belasten habe ich die Tabelle so gebaut, dass sie nur wenig mehr Daten erhält als sie im gegebenen Fenster darstellen kann. Das klappt prima und ist irre schnell. Dadurch bin ich aber gezwungen, wenn ich mit den Scrollbars verschiebe oder sonstwie neue Daten einlesen muss, die Tabelle immer komplett neu aufzubauen (geht aber wie gesagt ratz-fatz). Die Spaltenbreiten merke ich mir dazu in einer selbstgeschriebenen Datenstruktur. Das einzige Problem ist, wie ich diese Datenstruktur aktuell halte. Im Augenblick des Scrollens oder Resizends ist es dazu bereits zu spät.

Mit dem "TableColumnModelListener" werd ich mal probieren, muss erst mal sehen, wie ich an das Ding überhaut rankomme.


----------



## Michael... (10. Aug 2010)

Ist das Problem, dass beim Update der Daten in der Tabelle die Spaltenbreiten zurückgesetzt werden?
Benutzt Du ein DefaultTableModel und verwendest die Methode setDataVector(...), um neue Daten in die Tabelle einzufügen?
Dann könnte folgender Thread für Dich interessant sein und Dir ein qualvolles expermentieren mit Listenern ersparen: http://www.java-forum.org/awt-swing-swt/104204-jtable-spaltenbreite-beibehalten.html


----------



## ymene (10. Aug 2010)

Da ich an einem ganz ähnlichem Problem arbeite bin auch ich etwas neugierig. Darf man auch fragen, ob du deine Values sowohl spalten- aus auch zeilenspezifische nachlädst, oder gilt das nur für ganze Datensätze? Ich frage, da ich selber aktuell an einer LazyListTableModel-Implementierung arbeite die von der Umsetzung her etwas anders geartet ist, als das was ich bei dir so raushöre. Hast du dich da an ein bestimmtes Vorbild gehalten?

Ich lade die Daten immer in Form von ganzen Datensätzen nach, doch besteht da aktuell das Problem, wie ich mit Situationen umgehe, wie einem aktuellen Tabellenfilter, der über alle Spaltenvalues informiert sein muss, wenn anch einem bestimmten Spalteninhalt gefiltert werden soll. Vielleicht kannst du mich ja da in der Hinsicht auf den richtigen Weg bringen.

Zu deinem Problem: Ich kann da als Implementierungsdetail sehr empfehlen das TableModel selber bestimmen zu lassen, wann Daten nachgeladen werden sollen, da du dich dann um viewspezifische Details (wie Spaltenbreiten) nicht mehr kümmern brauchst, da die Tabelle nicht neu gezeichnet werden muss. Um die Zugriffe etwas zu optimieren kannst du dann einfach immer (so wie du das jetzt auch schon machst, wenn ich dich richtig verstanden habe) ganze Intervalle im TableModel nachladen. Die Table aktualisiert seine Inhalte dann einfach über die TableRenderer automatisch, sobald die neuen Values im TableModel angekommen sind. Du musst nur beim einmaligen Aufbau der Table vorweg wissen, wieviele Datensätze du besitzt und dort Dummydatensätze einfügen, die dann nach und nach durch die geladenen Daten ersetzt werden.

Grüße,
ymene


----------



## Michael... (10. Aug 2010)

Aus welchem Grund ein "Lazyloading" TableModel? Aufgrund der Anzahl der Datensätze oder der Dauer des Lesens der Daten? Woher kommen die Daten?

Wenn man nach einem Spalteninhalt filtert, muss man ja Zwangsweise alle Elemente einer Spalte anschauen. Mögliche Lösungen hängen u.a. wiederum von der Art der "Datenquelle" ab.



ymene hat gesagt.:


> Du musst nur beim einmaligen Aufbau der Table vorweg wissen, wieviele Datensätze du besitzt und dort Dummydatensätze einfügen, die dann nach und nach durch die geladenen Daten ersetzt werden.


Warum muss man die Anzahl der Datensätze vorher kennen und warum Dummydatensätze?


----------



## ymene (10. Aug 2010)

Michael... hat gesagt.:
			
		

> Aus welchem Grund ein "Lazyloading" TableModel? Aufgrund der Anzahl der Datensätze oder der Dauer des Lesens der Daten? Woher kommen die Daten?



Der Grund ist, dass das initiale Laden der Daten zu lang dauert bis die View letztendlich dargestellt werden kann. Auch ich muss eine große Menge an komplexen Daten verwalten, welche lokal in einer beanlist gecached werden, um erneute Ladezugriffe zu vermeiden. Bezogen werden die Daten von einem entfernten SQL-Server. 



			
				Michael... hat gesagt.:
			
		

> Warum muss man die Anzahl der Datensätze vorher kennen und warum Dummydatensätze?



Man muss die Anzahl der Datensätze nicht zwingend kennen. Grund ist aber der, das der User nicht erkennen soll, das die Daten lazy bezogen werden. Somit kann die Table anhand der Dummy-Datensätze die Scrollbalken gleich richtig setzen wodurch der User zu einen beliebigen Zeitpunkt immer an eine beliebige Stelle der Tabelle scrollen/springen kann. Die entsprechenden Daten werden sofort geladen und können angezeigt werden, da das Laden von lediglich 20 Datensätzen sehr schnell geht und die "übersprungenen" Daten einfach keine berücksichtigung finden.



			
				Michael... hat gesagt.:
			
		

> Wenn man nach einem Spalteninhalt filtert, muss man ja Zwangsweise alle Elemente einer Spalte anschauen. Mögliche Lösungen hängen u.a. wiederum von der Art der "Datenquelle" ab.



Das ist korrekt. Genau das ist nun auch mein Problem. Bin auch gerade erst an diesem Punkt angelangt und habe mir noch nicht viele Gedanken gemacht, aber ermöglichen möchte ich das Filtern schon. Daher muss ich mir gegebenenfalls Gedanken machen, ob ich über einen geschickten SQL, bzw. HQL Befehl nur die entsprechenden IDs an der Position X bei einer Sortierung nach der Spalte Y holen kann und dann diese Datensätze nachladen lasse. Ist alles irgendwie bissl tricky, zumal es natürlich genauso schnell gehen soll, wie in der ungefilterten View. Zudem sollte man mehrere Spaltenfilter gleichzeitig anwenden können. Hast du da einen Ansatz oder siehst du meinen grundsätzlichen Ansatz schon als kritisch an?


----------



## jueki (11. Aug 2010)

Hallo,
zunächst einmal zu ymene:

In meinem Projekt habe ich so etwas wie eine kleine Datenbank, aus der ich Object[]-Arrays referenzieren kann, die jeweils alle Daten einer Spalte enthalten. Meine Tabelle liegt in einer JScrollPane, bei der aber die Scrollbars ausgeschaltet sind. Diese wiederum liegt in einer JPanel, zusammen mit zwei "unabhängigen" Scrollbars. Wenn ich nun Daten in der Tabelle Darstellen muss, rechne ich mir aufgrund der gegebenen Fenstergröße und den gespeicherten Spaltenbreiten und Zellenhöhen (eigene Klasse, die geänderte Größen von Spalten und Zeilen dynamisch speichert) die Anzahl der darzustellenden Spalten und Zeilen aus und lege ein Object[][] entsprechender Größe an. Im nächsten Schritt frage ich die Position der Scrollbars ab (die sind auf die Größe der Datenbankarrays eingestellt) und lade aus den Datenbankarrays über Index ab der Scrollbarposition die darzustellenden Daten ein. *Wenn ich Filtern müsste, würde ich genau hier die Werte überspringen, die aus dem Filter rausfallen*. Ragt die Tabelle übrigens über die Daten der Datenbank heraus, werden Leerstrings in das Object[][] geschrieben. Anschließend weise ich das Object[][] einem eigenen DefaultTableModel zu und mit JTable.setModel(DefaultTableModel) weise ich die Daten der Tabelle dann zu. Problem ist wie gesagt, dass die Spaltenbreiten und Zeilenhöhen aus oben beschrienen Gründen immer wieder auf ihren Standardwert gesetzt werden, außerdem gibt es noch ein Problem mit Selektionen, wenn diese über den sichtbaren Tabellenbereich hinausgehen. Aber das löse ich später.
Ich hoffe das hilft Dir.

zu Michael...

jueki





Michael... hat gesagt.:


> Ist das Problem, dass beim Update der Daten in der Tabelle die Spaltenbreiten zurückgesetzt werden?
> Benutzt Du ein DefaultTableModel und verwendest die Methode setDataVector(...), um neue Daten in die Tabelle einzufügen?
> Dann könnte folgender Thread für Dich interessant sein und Dir ein qualvolles expermentieren mit Listenern ersparen: http://www.java-forum.org/awt-swing-swt/104204-jtable-spaltenbreite-beibehalten.html



danke für den Thread. Den habe ich jetzt umgesetzt: die Funktion


```
public void updateData( Object[][] newData )
    {
        this.dataVector = convertToVector(newData);
        this.fireTableDataChanged();
    }
```

in meine DefaultTableModel-Klasse eingesetzt und die Datenzuweisung entsprechend geändert. Es scheint, zumindest für die Spalten zu klappen. Danke also schon mal. Allerdings läuft das ganze ein wenig holprig. Wenn in der Führungsspalte eine Dezimalstellenänderung auftritt, springt die Spaltenbreite wieder in die Standardbreite. Das liegt aber vermutlich an meiner nicht aktualisierten eigenen Formatklasse. Ich glaube ich muss doch das Event fangen...
Ich versuch also doch mal an den "TableColumnModelListener" ran zu kommen...

jueki


----------



## ymene (11. Aug 2010)

Danke jueki, für den detaillierten Blick auf deine Umsetzung der LazyTable. Vielleicht kann ich daraus den ein oder anderen Ansatz mit nehmen, doch fürchte ich auf den ersten Blick, dass das schwierig wird auf Grund der unterschiedlichen Datenmodelle.


----------



## jueki (12. Aug 2010)

Ich habe jetzt eingehend das die Events des "TableColumnModelListener" ausgetestet. Leider werden die bei *jedem* Neuaufbau der Tabelle abgeschossen, also auch wenn sich Tabelle gerade wieder eigenmächtig auf irgendwelche Defaultbreiten zurücksetzt wird. 
Gibt es denn kein Event, das in dem Moment abgeschossen wird, in dem der User die Spaltenbreite ändert? Der Mauszeiger ändert sich dann ja auch automatisch, also muss man doch irgendwas gefeuert werden?


----------



## Michael... (12. Aug 2010)

Wenn die Tabelle eigenmächtig die Spaltenbreiten ändert, scheint irgendwo noch ein Fehler zu sein.

Um die Spaltenbreiten bei Usereingriff zu speichern, könnte man an den TableHeader einen MouseMotionListener dranhängen und dann bei mouseDrappged() die aktuellen Spaltenbreiten in einer Liste speichern. Eventuell wäre eine Kombination aus MouseListener und MouseMotionListener geschickter. Bei mouseDragged(...) ein boolean flag setzen und dieses bei mousReleased(...) überprüfen und ggf. die aktuellen Spaltenbreiten speichern.

Aber wie gesagt, deutet das Verhalten Deiner Tabelle auf irgendeinen Fehler hin.


----------



## jueki (13. Aug 2010)

Grundsätzlich ist das ja ok, dass die Tabelle neue Spaltenbreiten anlegt, sie wird ja auch bei jedem - zB. Scrollevent - vollständig neu aufgebaut. Mein Problem liegt nur darin, die Abfrage der derzeitigen Spaltenbreiten von dem Neuzeichnen zeitlich deutlich zu trennen. Mittlerweile konnte ich feststellen, das eine falsche Spaltenbreite nur dann auftritt, wenn ich sehr nervös mit dem Mausrad hin- und herscrolle: Da man die Fensteraktualisierung ja nicht ausschalten kann, kommt es irgendwann dazu, dass Standardbreiten einer (vermutlich leeren) Tabelle eingelesen werden und in meiner Spaltenbreitenklasse abgespeichert werden. Mit den Mouseevents jongliere ich jetzt auch schon länger rum, ich finde aber keines, das nur auf die Spaltenbreitenänderung mit der Maus reagiert:
- die Mausevents meines DefaultTableModels reagieren nicht in der Kopfzeile,
- der TableColumnModelListener des DefaultTableModels (also der JTableHeader) reagiert ständig, also auch beim Setzen von Standardbreiten und hat auch keine direkten Mausevents. (oder wie kann ich den dazu bringen?)
- Mausevents von der JScrollpane reagieren nicht.
- und irgendwas hatte ich mal (leider wieder gelöscht und ich komme nicht mehr drauf), dass ich einen Klick - aber auch nur einen Klick - in der Kopfzeile abfangen konnte. Der Reagierte aber nicht bei Spaltenbreitenänderungen.
Gibt es also noch irgendwelche Kuriositäten die man ableiten oder sonstwie dienstbar machen kann?


----------



## ymene (13. Aug 2010)

Was spricht denn gegen Michaels Lösung? Das müsste so klappen, wie er es beschrieben hat. Um einen MouseListener oder MouseMotionListener anzumelden machst du einfach folgendes:


```
table.getTableHeader().addMouseListener( new MouseAdapter()
    {
      @Override
      public void mouseReleased( MouseEvent e )
      {
         //Logik
      }
    } );
```

Reagieren tun sie auch auf Verändern der Spaltenbreite.


----------



## jueki (13. Aug 2010)

zunächst einmal nur, dass ich nicht auf die Idee gekommen bin sowas zu programmieren bzw. nicht wusste wie. Ich probiere es mal aus ...


----------



## jueki (13. Aug 2010)

Klasse, jetzt läufts wie am Schnürchen. Das Event reagiert zwar auf jeden "mouseReleased" in der Kopfzeile, also auch wenn nur einfach so auf die Kopfzeile geklickt wird, aber das ist kein Problem, wenn dann ab und an mal die Spaltenbreiten überflüssiger weise ausgelesen werden - hauptsache nicht nahe des Tabellenaufbaus.

Danke 
jueki


----------



## Michael... (13. Aug 2010)

Hier mal eine Demo. Ist zwar nicht so sauber programmiert, sollte aber das Prinzip verdeutlichen. Damit man von der Funktionsweise auch was sieht, läuft da noch ein Runnable in separatem Thread welches mal nur die Daten und mal den kompletten Modelinhalt ändert:

```
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionAdapter;
import java.util.ArrayList;
import java.util.Vector;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableColumnModel;

public class TableHeaderColumnDemo extends JFrame {
	private JTable table;
	private UpdatableTableModel  model;
	private ArrayList<Integer> colWidthList;
	private boolean mouseWasDragged = false;
	
	public TableHeaderColumnDemo() {
		colWidthList = new ArrayList<Integer>();
		
		model = new UpdatableTableModel(new Object[][] {{"valueX", "valueY"}}, new String[] {"ColumnA", "ColumnB"});
		table = new JTable(model);
		this.getContentPane().add(new JScrollPane(table));
		
		table.getTableHeader().addMouseMotionListener(new MouseMotionAdapter () {
			public void mouseDragged(MouseEvent e) {
				mouseWasDragged = true;
			}
		});
		
		table.getTableHeader().addMouseListener(new MouseAdapter() {
			public void mouseReleased(MouseEvent e) {
				if (mouseWasDragged) {
					TableColumnModel colModel = table.getColumnModel();
					colWidthList.clear();
					for (int i =0 ; i<colModel.getColumnCount(); i++)
						colWidthList.add(colModel.getColumn(i).getWidth());
					System.out.println("Spaltenbreiten wurden vom Anwender auf folgende Werte geändert:");
					System.out.println(java.util.Arrays.toString(colWidthList.toArray()));
				}
				mouseWasDragged = false;
			}
		});
		
		new Thread(new SomeStupidRunnable()).start();
	}
	
	class UpdatableTableModel extends DefaultTableModel {
		public UpdatableTableModel(Object[][] data, Object[] colIdentifier) {
			super(data, colIdentifier);
		}
		
		public void updateData(Object[][] data) {
			this.dataVector = DefaultTableModel.convertToVector(data);
			this.fireTableDataChanged();
			System.out.println("Die Daten des TableModels wurden überschrieben. Die vom User gesetzten Spaltenbreiten sollten erhalten bleiben.");
		}
		
		public void setDataVector(Object[][] dataVector, Object[] columnIdentifiers) {
			this.setDataVector(DefaultTableModel.convertToVector(dataVector), DefaultTableModel.convertToVector(columnIdentifiers));
		}
		
		public void setDataVector(Vector dataVector, Vector columnIdentifiers) {
			super.setDataVector(dataVector, columnIdentifiers);
			System.out.println("Das TableModel hat sich komplett geändert. Versuche eventuell vom Anwender eingestellte Spaltenbreiten wieder herzustellen");
			for(int i=0; i<colWidthList.size(); i++)
				table.getColumnModel().getColumn(i).setPreferredWidth(colWidthList.get(i));
		}
	}
	
	class SomeStupidRunnable implements Runnable {
		public void run() {
			while(true) {
				int i = (int)(Math.random()*10);
				if (i<2)
					model.setDataVector(new Object[][] {{"??????", "??????"}}, new String[] {"ColumnA", "ColumnB"});
				else if (i<8) 					
					model.updateData(new Object[][] {{"valueX"+ i, "valueY"+ i}});
				try {
					Thread.sleep(2000);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
		}
	}
	
	public static void main(String[] args) {
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				TableHeaderColumnDemo frame = new TableHeaderColumnDemo();
				frame.setBounds(0, 0, 500, 300);
				frame.setLocationRelativeTo(null);
				frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
				frame.setVisible(true);
			}
		});
	}
}
```


----------

