JTable setValueAt StackOverflowError

2019-06-09 10:43发布

问题:

I've been searching for whole day and I still can't find simple anwser to my problem: how can I make JTable cell update it's value when I edit it in another one?

I wanted to use somehow fireTableCellUpdated but I don't really understand how can I use it, when and on what object.

What I want is to obtain is some kind of listener that listens whether that values changes or not. In this particular scenario I have editable third column in which I store the amount and I want that listener to automatically calculate and set values in other cells in a row. I've come up with something like this:

@Override
public void tableChanged(TableModelEvent e)
{
    BigDecimal withoutTax, tax, withTax;

    for(int i = 0; i < table.getRowCount(); i++)
    {
            BigDecimal amount = new BigDecimal(String.valueOf(table.getValueAt(i, 3)).replace(",", "."));
            BigDecimal price = new BigDecimal(String.valueOf(table.getValueAt(i, 4)).replace(",", "."));
            withoutTax = amount.multiply(price, new MathContext(2));
            table.setValueAt(withoutTax, i, 5);
            tax = withoutTax.multiply(new BigDecimal(0.23), new MathContext(2));
            table.setValueAt(tax, i, 7);
            withTax = withoutTax.add(tax, new MathContext(2));
            table.setValueAt(withTax, i, 8);
    }
}

But this results in StackOverflowError and I'm guessing that's because table.setValueAt fires tableChanged listener so it's going into infinite loop.

Can someone explain me how can I accomplish that?

回答1:

tableChanged is been called when a change to the TableModel is taking place, which is been trigged by the setValueAt method and around you go...

The solution? Do it inside the setValue method of the TableModel...

public class TestModel extends ... { // Some TableModel

    //...

    @Override
    public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
        if (columnIndex == 3) {
            // Set the value been passed to in (probably from the editor)...            
            fireTableCellUpdated(rowIndex, columnIndex);
            BigDecimal amount = new BigDecimal(String.valueOf(getValueAt(rowIndex, 3)).replace(",", "."));
            BigDecimal price = new BigDecimal(String.valueOf(getValueAt(rowIndex, 4)).replace(",", "."));
            BigDecimal withoutTax = amount.multiply(price, new MathContext(2));
            // Set the value for row x 5 directly within the backing store of the model...
            //table.setValueAt(withoutTax, i, 5);
            BigDecimal tax = withoutTax.multiply(new BigDecimal(0.23), new MathContext(2));
            // Set the value for row x 7 directly within the backing store of the model...
            //table.setValueAt(tax, i, 7);
            BigDecimal withTax = withoutTax.add(tax, new MathContext(2));
            // Set the value for row x 8 directly within the backing store of the model...
            //table.setValueAt(withTax, i, 8);

            fireTableCellUpdated(rowIndex, 5);
            fireTableCellUpdated(rowIndex, 7);
            fireTableCellUpdated(rowIndex, 8);

            // It might actually be easier to use...
            //fireTableRowsUpdated(rowIndex, rowIndex);
        }
    }

For example...

This is a basic example, while it only uses a single row, the idea holds true for multiple rows all the same...

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.table.AbstractTableModel;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() {
            setLayout(new BorderLayout());
            JTable table = new JTable(new MultiplicationTableMode());
            add(new JScrollPane(table));
        }

    }

    public class MultiplicationTableMode extends AbstractTableModel {

        private List<List<Integer>> values;

        public MultiplicationTableMode() {
            values = new ArrayList<>(1);
            List<Integer> cols = new ArrayList<>(11);
            for (int index = 0; index < 11; index++) {
                cols.add(0);
            }
            values.add(cols);
        }

        @Override
        public int getRowCount() {
            return values.size();
        }

        @Override
        public int getColumnCount() {
            return 10;
        }

        @Override
        public String getColumnName(int column) {
            return column == 0 ? "?" : "x" + Integer.toString(column);
        }

        @Override
        public boolean isCellEditable(int rowIndex, int columnIndex) {
            return columnIndex == 0;
        }

        @Override
        public Object getValueAt(int rowIndex, int columnIndex) {
            List<Integer> columns = values.get(rowIndex);
            return columns.get(columnIndex);
        }

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


        @Override
        public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
            if (columnIndex == 0) {
                if (aValue instanceof Integer) {
                    List<Integer> columns = values.get(rowIndex);
                    int intValue = (int) aValue;
                    columns.set(0, intValue);
                    for (int index = 1; index < columns.size(); index++) {
                        columns.set(index, intValue * index);
                    }
                    fireTableRowsUpdated(rowIndex, rowIndex);
                }
            }
        }

    }

}

Updated...

It occurred to me that if the other column values are not editable, then you don't actually need to maintain their values at all, but can simply calculate them dynamically whenever getValueAt is called, for example...

public class MultiplicationTableMode extends AbstractTableModel {

    private List<Integer> values;

    public MultiplicationTableMode() {
        values = new ArrayList<>(1);
        values.add(0);
    }

    @Override
    public int getRowCount() {
        return values.size();
    }

    @Override
    public int getColumnCount() {
        return 10;
    }

    @Override
    public String getColumnName(int column) {
        return column == 0 ? "?" : "x" + Integer.toString(column);
    }

    @Override
    public boolean isCellEditable(int rowIndex, int columnIndex) {
        return columnIndex == 0;
    }

    @Override
    public Object getValueAt(int rowIndex, int columnIndex) {
        int value = values.get(rowIndex);
        if (columnIndex > 0) {
            value *= columnIndex;
        }
        return value;
    }

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

    @Override
    public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
        if (columnIndex == 0) {
            if (aValue instanceof Integer) {
                values.set(rowIndex, (int)aValue);
                fireTableRowsUpdated(rowIndex, rowIndex);
            }
        }
    }

}