javaswingjtableinputverifier

Custom cells on a JTable returning wrong values (and not displaying correctly)


I am writing a custom POS system and allowing users to change the quantity straight on the table instead of spawning a dialog box. Here's the first line of the table with dummy data in it. (I haven't linked it to a DB yet.) enter image description here

Price and Amt both have custom cell renderers (not that anyone can tell because they don't render right) to display a money format. EDIT: I nixed the custom editors (not renderers, thank you) and the problem below still persists

The plan was that when the user typed a quantity into the qty box, it would update the amt cell (and a running total at the top of the screen). To do this, I created a verifier which would verify the data was an integer and attached it to a custom cell editor that just adds the verifier to a JTextField.

My verifier which is nested within the screen itself so it had access to the screen's fields and the table itself. I wanted to put it into the table model, but I found I couldn't tell what cell the user had selected. (Verify() works so I'm omitting that)

class QtyVerifier extends InputVerifier
{
    private int qty = 0;
    private BigDecimal pricePerUnit;
    private BigDecimal prevamt;

    @Override
    public boolean shouldYieldFocus(JComponent input) 
    {
        //reset quantity
        this.qty = 0;

        //verify the results
        boolean ok2go = this.verify(input);

        if (ok2go)
        {
            //grab all the current values
            this.qty = new Integer(
                    (String)salesOrderFormTable.getValueAt(rowselected, colselected));
            this.pricePerUnit = new BigDecimal(
                    (String)salesOrderFormTable.getValueAt(rowselected, colselected));
            this.prevamt = new BigDecimal( 
                    (String)salesOrderFormTable.getValueAt(rowselected, colselected));


            //remove previous amount from the total
            addLineCostToRunningTotal(this.prevamt.negate());
            //update sales order total
            addLineCostToRunningTotal(amt);

            //update line total cell
            salesOrderFormTable.setValueAt(actualTotal, rowselected, 6);
            salesOrderFormTable.validate();
        }
        else { ... }
        return ok2go;
    }

    ....
};

Here's where it gets really weird. I tell the qty that the quantity is still 1. It pulls all the proper data from the cells.

Selected: 0,1
Quantity in cell 0,1 is 1
Line price is 1.0
Previous amt is 1.0
New amt is 1.0

Okay, good. So I go to change the value to 5 and hit enter. enter image description here It changes the value to 25 (solved this issue) and also pulls the wrong data from the cells.

Quantity in cell 0,1 is 5 //this is correct
Line price is 5.0 //this is incorrect. It should be 1.
Previous amt is 5.0 //also incorrect. This also should be 1.
New amt is 25.0 //this WOULD be correct if the previous data was.

What is going on with my table?! Why is this giving me completely wrong information in two cells and changing the qty cell to something I didn't type? Is there a simpler way to do this (with or without a verifier)? I cannot have the wrong cost coming up on an order form. I considered changing all my values from doubles to BigDecimals to prevent rounding errors, but this isn't even a rounding error. It's just plain wrong. I'm completely lost now.


Solution

  • Given your code is highly customized and hard to debug without all the pieces put together, I'd start from the scratch appealing to the basic concepts and saying that these requirements can be satisfied working with the TableModel:

    Since there's no info about your table model I'll illustrate the points above using DefaultTableModel as follows:

    String[] header = new String[] { "qty", "price", "amt" };
    DefaultTableModel model = new DefaultTableModel(header, 1) {
    
        @Override
        public Class<?> getColumnClass(int columnIndex) {
            switch (columnIndex) {
                case 0: return Integer.class;
                case 1: 
                case 2: return Double.class;
            }
            throw new ArrayIndexOutOfBoundsException(columnIndex);
        }
    
        @Override
        public boolean isCellEditable(int row, int column) {
            return column < 2;
        }
    
        @Override
        public void setValueAt(Object aValue, int row, int column) {
            super.setValueAt(aValue, row, column);
            if (column < 2) {
                Integer qty = (Integer)getValueAt(row, 0);
                Double price = (Double)getValueAt(row, 1);
                Double amt = (qty != null && price != null) 
                           ? (Double)(qty * price)
                           : null;
                super.setValueAt(amt, row, 2);
            }
        }
    };
    

    Some notes about the example:

    1. In this example I've only set the three columns that actually matter: qty, price and amt.
    2. Only qty and price columns are editable while amt is not editable (it's calculated when the other columns are updated).
    3. Given the getColumnClass() implementation, the default editor won't allow invalid inputs for none of the number columns: whether column class is Integer it will allow only integer values. The same applies for Double class.
    4. If either qty or price column is modified then setValueAt(...) is invoked and amt column is also updated accordingly.

    Finally to apply the currency format when the cell is rendered (not being edited) we need to provide a custom renderer. For example:

    class CurrencyRenderer extends DefaultTableCellRenderer {
    
        private final NumberFormat currencyFormat;
    
        public CurrencyRenderer() {
            currencyFormat = NumberFormat.getCurrencyInstance();
        }
    
        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
            Component renderer = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
            if (value instanceof Number) {
                setText(currencyFormat.format((Number)value));
            }
            return renderer;
        }
    }
    

    Now, there are different ways to provide a renderer, all explained here: Concepts: Editors and Renderers