Berechnungen automatisch durchführen - wie in Excel

Hi,

ich habe offenbar etwas Grundlegendes noch nicht verinnerlicht, weiß aber nicht genau, wo mein Denkfehler liegt.
Ich möchte gerne in einer JTable nach der Eingabe von Zahlen jeweils automatisch Berechnungen durchführen und die Ergebnisse in bestimmte Tabellenzellen schreiben lassen.
Das funktioniert zwar, sobald ich aber die ersten 2 Zahlen für eine Berechnung eingegeben habe, steigt die Rechenlast des Rechners dauerhaft auf über 50%. Dabei wird die getValueAt()-Methode meines DefaultTableModel-Typs immer wieder aufgerufen - eine Endlosschleife also.
Die Berechnungen und das Speichern der Ergebnisse erledige ich derzeit noch in der getValueAt()-Methode des Models. Doch das Verhalten des Programms sagt mir, dass das nicht richtig sein kann.

Nur wie macht man das richtig? Kann mich dazu bitte jemand wiedermal aufklären?
Danke.

ein Testprogramm wäre dazu doch so schön

den denkbaren Umfang einer Methode wie getValueAt() solltest du doch wie jeder andere überblicken können,
eine Methode wird aufgerufen und gibt etwas zurück, im einfachsten Falle immer nur Leerstring (*),

wenn komplizierter intern gerechnet wird, kann das keinen Unterschied machen
außer Nebenwirkungen sind explizit eingebaut, das offensichtliche fireXyChanged(), vielleicht durch weitere Aufrufen für Daten usw.

alles andere, eine Änderung des Verhaltens des Programms nur von einer internen lokalen Plus-Rechnung wäre doch revolutionärer Wahnsinn :wink:

(*) vielleicht liegt das Problem an ganz anderer Stelle, schon mit Dummy-Rückgabe Dauerfeuer, Berechnungen haben nichts damit zu tun?
na, ein Testprogramm kann evtl. alles klären, so ist dagegen quasi nichts gegeben

Weiß nicht, ob die getValue des Models eine geeignete Stelle ist, da man ja nicht wirklich die Kontrolle darüber hat wann und von wem diese Methode aufgerufen wird. Mir scheint das Überschreiben der setValue() dafür geeigneter.

Als ich mal so etwas implementiert habe, habe ich mit einem TableModelListener gearbeitet. In diesem habe ich mir gemerkt welche Zelle(n) per automatischer Berechnung als letztes bearbeitet wurde(n) und das beim nächsten Aufruf der tableChanged geprüft, um eine Endlosschleife zu verhindern. Wenn die Zellen mit “Formel” alle in bestimmten Spalten/Zellen stehen, lassen sich die relevanten TableModelEvents noch einfacher herausfiltern.
Falls das Ganze noch komplexer ist, z.B. Formeln in beliebigen Zellen vom Anwender eingetragen werden können (wie in Excel), könnte man sich überlegen in diesen Zellen eigene DatenTypen zu hinterlegen, um dann bei Änderungen auf diesen Typ zu prüfen und diese ggf. ignorieren.

wenn getValueAt() nicht drankommt, dann sieht man auch nichts neues auf dem Bildschirm,
dass dann bereits das richtige gespeichert ist, hilft nicht besonders

vielleicht werden auch Daten aus Programm gesetzt, nicht unbedingt einzeln mit setValueAt(),
zwiespältiger Weg

getValueAt() kann da narrensicherer sein,
dass es aufgerufen wird, dass die JTable die Zeile oder ganze Tabelle neu anzeigt, muss man sowieso veranlassen, fireTableDataChanged() & Co.

auf welches Ereignis reagierst du denn bei der Eingabe?

Wie das immer so passiert, beim Schreiben eines KSKB habe ich meinen Denkfehler wohl gefunden. Die Nachfrage von @Bleiglanz hat mich vielleicht auch in die richtige Richtung geschubst.
Das KSKB soll so lange die Differenz in die nächste Zeile übernehmen, wie das Ergebnis der Rechnung A-B+C > 0 ist.
Die Testausgaben habe ich mal noch dringelassen.

Das TableModel:

import javax.swing.table.DefaultTableModel;

public class CalculatorTableModel extends DefaultTableModel {

    public CalculatorTableModel() {
        super(10, 5);
    }   

    @Override
    public String getColumnName(int column) {
        if(column == 0) return "Nummer";
        if(column == 1) return "A";
        if(column == 2) return "B";
        if(column == 3) return "A-B+C";
        if(column == 4) return "C";
        return null;
    }

    @Override
    public Class<?> getColumnClass(int columnIndex) {
        if(columnIndex == 0) return Integer.class;
        return Double.class;
    }

    
    @Override
    public Object getValueAt(int row, int column) {
        System.out.println("getValueAt(Zeile "+row+", Spalte "+getColumnName(column)+") betreten");
        if(column == 0) return row+1;
        if(column == 3) {
            System.out.println("column == A-B+C");
            Vector rowVector = (Vector)dataVector.get(row);
            Object oa = rowVector.get(1); //Column A
            Object ob = rowVector.get(2); //Column B
            Object oc = rowVector.get(4); //Column C

            if(oa != null && ob != null) {
                System.out.println("oa != null && ob != null");
                double da = Double.valueOf(oa.toString());
                double db = Double.valueOf(ob.toString());
                double dc = 0;
                if(oc != null) {
                    dc = Double.valueOf(oc.toString());
                }

                double result = da - db + dc;
                if(result > 0) {
                    System.out.println("Ergebnis in Zeile "+(row+2)+"|Spalte "+getColumnName(1)+" setzen");
                    super.setValueAt(result, row+1, 1); //Ergebnis in Zeile darunter, in die A-Spalte setzen
                }

                return result;
            }
        }
        System.out.println("getValueAt() verlassen");
        return super.getValueAt(row, column);
    }
}```

Die Testklasse:

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

public class CalculatorTable {
    public static void main(String[] args) {
        JFrame frame = new JFrame("CalculatorTable");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        
        JTable table = new JTable(new CalculatorTableModel());
        frame.add(new JScrollPane(table));
        
        frame.setSize(500, 150);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
}```

Das eigentliche Problem war wohl, dass ich immer nur eine Spalte in der getValueAt()-Methode "beobachtet" habe. Immer wenn die Spalte A abgefragt wurde, habe ich nach den anderen Spalten geguckt und auch berechnet. Das war dann wohl der Grund für die Endlosschleife,
Trotzdem würde ich mich freuen, wenn ihr nochmal über den Code gucken könntet. Was JTable angeht bin ich immer noch sehr unsicher.

Wolltest du nicht eigentlich auf eine Änderung reagieren? So wie dein Beispiel jetzt ist, könntest du die berechneten Werte ja gleich im Model vorhalten?

War doch klar, dass der Aufruf von getValueAt innerhalb von getValueAt zu einer Endlosschleife führen könnte, wenn man sich bei den Indexen verhaspelt :slight_smile:

[QUOTE=SlaterB]wenn getValueAt() nicht drankommt, dann sieht man auch nichts neues auf dem Bildschirm,
dass dann bereits das richtige gespeichert ist, hilft nicht besonders[/QUOTE]
Es ist ja Aufgabe des Models allen Listenern bekannt zu machen, dass sich etwas geändert hat und dann obliegt deren Verantwortung sich per getValueAt den aktuellen Datensatz zu holen.

Ich finde es nur merkwürdig sich an eine Methode zu hängen, die Daten für Renderer oder sonstige Interessenten liefert, um im selben Zug andere Daten des Models zu manipulieren. Da erscheint es mir schlüssiger Daten in diesem Moment zu ändern, wenn sich die Daten von denen jenes Datum abhängig ist ändern. Einmal abgesehen davon, dass bei der get-Variante Daten neu berechnet werden, obwohl sich deren Berechnungsgrundlage bzw. die Ausgangsdaten überhaupt nicht verändert haben.
Ob das jetzt in der setValue des Models passieren muss? Hatte ja als Alternative einen TableModelListener genannt.

Wenn das also nicht korrekt ist, wie würde man dann solche Excel-like-Berechnungen richtig machen?

Wie soll das aussehen?
Gibt’s noch andere Lösungen?

die Strukturen sind hier in der Tat etwas obskur,
nur wenn getValueAt() für Spalte C in Zeile n aufgerufen wird, wird etwas in die Spalte A in Zeile n+1 gespeichert

das ist kein typisches Cache-Verhalten,
für sich selber zu sorgen ist normaler: falls getValueAt für A drankommt und nichts eigenes vorhanden, dann bei C in Zeile vorher nachschauen, Wert übernehmen

freilich müsste dann irgendwie der Wert von A in der nächsten Spalte entfernt werden, kommt fast aufs gleiche hinaus,
und immer noch die Frage, ob und wie man der Tabelle Bescheid sagt, der setValueAt()-Aufruf ist nicht schön, aber erreicht dieses

überhaupt kann es hier eine korrekte Kettenreaktion geben, falls alle Zeilen befüllt sind, eine Änderung wirkt sich nach und nach überall aus
setValueAt() ändert und aktualisiert nur eine Zelle in der nächsten Zeile, dort Spalte C wird nicht mehr automatisch aktualisiert


drei Strategien dürften hier (wie auch allgemein woanders) sinnvoll sein:

  1. alle Formel-Felder invalidieren + ganze Tabelle neu zeichen, das ist eine einfache Aufgabe bei der Änderung, braucht kein spezielles Wissen,
    getValueAt() kann für jede Zelle (für sich selber!) wenn nötig fehlende Daten zusammensuchen

  2. alle Formel-Felder nicht nur invalidieren sondern gleich neuberechnen + ganze Tabelle neu zeichen,

beim Neuberechnen muss man auf Reihenfolge achten, kann man in Schleifen ausdrücken wenn feste Konzepte wie hier immergleiche Formeln je Zeile,

bei dynamisch gesetzen Formeln bietet sich ein ‚Selbstfindungsalgorithmus‘ an
Zelle 0,1 soll anfangen → wenn die was anderes braucht, dann rekursiv das aufrufen und speichern usw.,
so ähnlich wie ein getValueAt() aus 1.

passiert bei 1. durch Neuzeichnen von JTable im Grunde genauso, wobei die vielleicht zunächst nur den sichtbaren Bereich drannimmt,
wenn man das nicht mag, kann man es manuell vorher anstoßen

  1. von einer konkreten Änderung aus wissen, was passiert, nach und nach Werte ersetzen,
    rekursiv Folgeänderungen mitberücksichtigen, nebenher evtl. auch für Aktualisierung der Anzeige dieser sorgen usw.,

ein wenig der Ansatz von Posting #6, getValueAt() ist dafür aber in der Tat kein guter Platz, setValueAt() eher, ja :wink:


edit:
TableModelListener kann man ja suchen :wink:
aber setValueAt() ist praktisch auch so eine Reaktion, wenn es nicht stört den Code direkt im eigenen Model zu haben, eher ein Vorteil

So ganz habe ich noch nicht verstanden, wann welche Zelle neu berechnet werden soll und wie genau die Abhängigkeiten sind.

Folgendes KSKB nutzt einen TableModelListener und reagiert (dem ersten Anschein nach) genau wie Dein Codebeispiel - allerdings immer direkt nach Eingabe eines Wertes in die Zellen A, B oder C


import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.DefaultTableModel;

@SuppressWarnings("serial")
public class TableCalcDemo extends JFrame {
	private DefaultTableModel model;
	private boolean flag = false;

	public TableCalcDemo() {
		model = new DefaultTableModel(10, 5) {
			private final String[] colName = { "Nummer", "A", "B", "A-B+C", "C" };

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

			public Object getValueAt(int row, int col) {
				if (col == 0)
					return row + 1;
				return super.getValueAt(row, col);
			}

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

		model.addTableModelListener(new TableModelListener() {
			public void tableChanged(TableModelEvent evt) {
				int column = evt.getColumn();
				int row = evt.getFirstRow();

				System.out.println(String.format("Cell (%s, %s) changed. Flag is %s ", row, column, flag));

				switch (column) {
				case 1:
				case 2:
				case 4:
					if (!flag)
						calc(model.getValueAt(row, 1), model.getValueAt(row, 2), model.getValueAt(row, 4), row);
					break;
				}
			}

			private void calc(Object aObj, Object bObj, Object cObj, int row) {
				if (aObj != null && bObj != null) {
					double abc = Double.parseDouble(aObj.toString()) - Double.parseDouble(bObj.toString());
					double c = 0;
					if (cObj != null) {
						c = Double.parseDouble(cObj.toString());
						abc += c;
					}
					flag = true;
					model.setValueAt(abc, row, 3);
					if (c > 0)
						model.setValueAt(abc, row + 1, 1);

					flag = false;
				}
			}
		});
	}

	public void startDemo() {
		JFrame frame = new TableCalcDemo();
		frame.setBounds(0, 0, 500, 300);
		frame.setLocationRelativeTo(null);
		frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		frame.add(new JScrollPane(new JTable(model)), BorderLayout.CENTER);
		frame.setVisible(true);
	}

	public static void main(String[] args) {
		new TableCalcDemo().startDemo();
	}
}```

Tatsächlich hatte ich auch schon öfter (aber nicht so “ernsthaft”) mal in Betracht gezogen, so eine Art “Mini-Tabellenkalkulation” auf Basis einer JTable zu erstellen. Könnte ganz unterhaltsam sein.

Ich denke, SlaterB hat die wesentlichen Optionen zusammengefasst, auch wenn die Abgrenzung zwischen den Fällen nicht 100% klar ist.

Nochmal etwas anders (Vielleicht ist es auch nur anders formuliert?) :

  1. getValueAt() berechnet “seinen” Inhalt vollkommen autark. Die Zelle ist dabei rein funktional und rein passiv. D.h. die Daten für diese Zelle werden nicht gespeichert, sondern immer on-the-fly ausgerechnet. (Bei jeder Änderung wird brutal und pauschal mit fireTableDataChanged() dafür gesorgt, dass alle Listener von den neuen Werten was mitbekommen - im speziellen also dass die JTable mit den neuen, eventuell geänderten Werten frisch gezeichnet wird).

  2. Bei einem setValueAt() wird über die ganze Tabelle gegangen, und mit einer “internen” Funktion der neue Wert berechnet, der dann gesetzt wird:

for (each cell r,c) {
    Object value = computeNewValueAt(r,c);
    setValueAt(value, r,c);
}

Der entscheidende Unterschied wäre, dass die Daten (also die Endergebnisse) wirklich in den Zellen gespeichert sind.

Die dritte Variante…
3. von einer konkreten Änderung aus wissen, was passiert, nach und nach Werte ersetzen,
fällt IMHO erstmal weg: Eine Tabellenzelle sollte nicht direkt wissen, welche anderen Zellen von “ihrem” Wert abhängen

Und die Frage, ob sie das praktikabel überhaupt wissen, kann, ist auch interessant - das führt zum nächsten:

IMHO fällt aus dem gleichen Grund auch die 2. Variante weg: Wenn die Zellen die Werte wirklich speichern, weiß man praktisch nie, ob eine Zelle einen aktuellen Wert enthält oder nicht.


Das “wegfallen” bezieht sich auf den Fall, dass man eine einfache Lösung will.

Die einzige Option, die ich sehen würde, um die 2. oder die 3. Option umzusetzen, wäre ein AST. In der Zelle müßte ein Abstract Syntax Tree gespeichert sein, an dem eindeutig und programmatisch (!) erkannt werden kann, welche Zelle von welcher anderen abhängt. Das dürfte aber etwas aufwändiger sein…

Ich hätte eigentlich vermutet, dass die erste Variante die einfachste sein könnte - GROB anskizziert:

package bytewelt;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;

public class FunctionCalculatorTableModel extends DefaultTableModel {
    
    interface FunctionalCell
    {
        Double getValue(int row, int column);
    }
    
    static FunctionalCell createComputingFunctionalCell(
        final TableModel tableModel)
    {
        return new FunctionalCell()
        {
            @Override
            public Double getValue(int row, int column)
            {
                double A = asDouble(tableModel.getValueAt(row, 1));
                double B = asDouble(tableModel.getValueAt(row, 2));
                double C = asDouble(tableModel.getValueAt(row, 4));
                return A-B+C;
            }
        };
    }
    
    static FunctionalCell createConditionalFunctionalCell(
        final TableModel tableModel)
    {
        return new FunctionalCell()
        {
            @Override
            public Double getValue(int row, int column)
            {
                if (row == 0)
                {
                    return null;
                }
                double otherValue = asDouble(tableModel.getValueAt(row-1, 3));
                if (otherValue < 0)
                {
                    return otherValue;
                }
                return null;
            }
        };
    }
    
    private static double asDouble(Object object)
    {
        if (object == null)
        {
            return 0.0;
        }
        if (object instanceof Number)
        {
            Number number = (Number)object;
            return number.doubleValue();
        }
        return 0.0;
    }
    
    
    public FunctionCalculatorTableModel() {
        super(10, 5);
        
        for (int r=0; r<10; r++)
        {
            setValueAt(createComputingFunctionalCell(this), r, 3);
        }
        for (int r=0; r<10; r++)
        {
            setValueAt(createConditionalFunctionalCell(this), r, 1);
        }
        
    }  

    @Override
    public String getColumnName(int column) {
        if(column == 0) return "Nummer";
        if(column == 1) return "A";
        if(column == 2) return "B";
        if(column == 3) return "A-B+C";
        if(column == 4) return "C";
        return null;
    }

    @Override
    public Class<?> getColumnClass(int columnIndex) {
        if(columnIndex == 0) return Integer.class;
        return Double.class;
    }
    
    @Override
    public void setValueAt(Object aValue, int row, int column)
    {
        super.setValueAt(aValue, row, column);
        fireTableDataChanged();
    }


    @Override
    public Object getValueAt(int row, int column) {
        System.out.println("getValueAt(Zeile "+row+", Spalte "+getColumnName(column)+") betreten");
        if(column == 0) return row+1;
        Object value = super.getValueAt(row, column);
        if (value instanceof FunctionalCell)
        {
            FunctionalCell functionalCell = (FunctionalCell)value;
            Double functionalValue = functionalCell.getValue(row, column);
            return functionalValue;
        }
        return value;
    }
}

Aaaber ich weiß nicht, ob man um eine programmatische, an einen Abstract Syntax Tree angelehnte Repräsentation der Zelleninhalte/Formeln drumrumkommt. Spätestens bei Zirkelbezügen würde man sonst immer auf die Fresse fliegen:

Column A = B+C
Column B = A+C
Column C = B+A

Hab’s gerade mal getestet: Excel erkennt das (natürlich), und blendet dann einen Warndialog ein…

Danke Gast, SlaterB und Marco13 für eure Überlegungen.
Ist dann leider komplizierter, als ich vermutet hatte…

Wäre aber interessant, zu hören, was da rauskommt. Websuchen liefert erstaunlich wenig dazu, zumindest nichts freies, und kaum was akutelles. Sowas wie http://www.eteks.com/jeks/en/ sieht ziemlich alt aus. (Und dass ich mich nicht mehr an http://forum.byte-welt.net/threads/4570-Spreadsheet-JTable erinnert habe, ist etwas verwunderlich, passt aber auch nicht ganz)