JTable Input Verifier

2019-03-20 12:45发布

问题:

I am trying to create a simple Input Verifier for a JTable. I ended up with overriding the method: editingStopped(). The problem is that the event does not include informations about the cell that has been updated.

This is my "pseudo code":

  If (user finished editing a cell)  {
     Check if cell`s value is "1" or "0" or "-"  (Karnaugh-Veitch)
     If (check = false)
        setValue (cell, "");
   }

The first I tried was this here:

table.getModel().addTableModelListener(new TableModelListener() {
            @Override
            public void tableChanged(TableModelEvent e) {
                inputVerify (e.getColumn(), e.getFirstRow());
            }
});

    public void inputVerify (int column, int row) {
        boolean verified = true;
        String field = table.getValueAt(row, column).toString();

        if (field != null && field.length() == 1) {
            if ( !(field.charAt(0) == '0' || field.charAt(0) == '1' || field.charAt(0) == '-' ))
                verified = false;
        }
        else {
            verified = false;
        }

        if (!verified) {
            table.getModel().setValueAt("", row, column);
            java.awt.Toolkit.getDefaultToolkit().beep();
        }

        System.out.println ("Column = " + column + " Row = " + row + " Value = " + table.getValueAt(row, column) +" Verified = "+verified);
    }

But this ends up with an : StackOverflow Exception. I guess the problem is that: setValueAt(..) fires another tableChanged() event and an endless loop is being generated.

Now, I tried this here:

    table.getDefaultEditor(Object.class).addCellEditorListener(new CellEditorListener() {

        // called when editing stops
        public void editingStopped(ChangeEvent e) {

            // print out the value in the TableCellEditor
            System.out.println(((CellEditor) e.getSource()).getCellEditorValue().toString());

        }

        public void editingCanceled(ChangeEvent e) {
            // whatever
        }
    });

But as you can see I can just retrieve the new value of the cell, not the "coordinates". I need to call: setValueAt( .. ) method, but I dont know how to get the cell`s coordinates.

Or is there a more simple way to create an input verifier??

Best regards Ioannis K.

回答1:

First: input validation on JTable editing is not well supported. A couple of comments

  • tableChanged in a TableModelListener is not a good place to do validation, at that point in time the change already happened (the model notifies its listeners of the fact)
  • as a consequence, whatever validation (verify) method hook you choose, never-ever talk back to the model, you'll end up in an infinite loop (as you have seen)
  • application-provided CellEditorListeners are rather useless because a) there's no guarantee about sequence of notification (JTable might or not have already updated the model) b) the life-cylce of an editor is ill-defined

After all those (incomplete, unfortunately ;-) no-nos, a little hope: best bet is to implement a custom CellEditor which does the validation in stopCellCellEditing: if the new value isn't valid, return false and optionally provide a visual error feedback. Have a look at JTable.GenericEditor to get an idea of how that might be done



回答2:

What worked for me (tip 'o the hat to kleopatra):

private class CellEditor extends DefaultCellEditor {

    InputVerifier verifier = null;

    public CellEditor(InputVerifier verifier) {
        super(new JTextField());
        this.verifier = verifier;

    }

    @Override
    public boolean stopCellEditing() {
        return verifier.verify(editorComponent) && super.stopCellEditing();
    }

}

// ...

private class PortVerifier extends InputVerifier {

    @Override
    public boolean verify(JComponent input) {
        boolean verified = false;
        String text = ((JTextField) input).getText();
        try {
            int port = Integer.valueOf(text);
            if ((0 < port) && (port <= 65535)) {
                input.setBackground(Color.WHITE);
                verified = true;
            } else {
                input.setBackground(Color.RED);
            }
        } catch (NumberFormatException e) {
            input.setBackground(Color.RED);
        }
        return verified;
    }
}

// ...

table.getColumn("Port").setCellEditor(new CellEditor(new PortVerifier()));


回答3:

hmm, there might be a simpler solution to this. Please try this, it worked for me. The key is to remember last selected item, and then perform validation on the current item. If input is wrong, you can roll back to the last selected item, and notify user about that. Roll back is performed using EventQueue.invokeLater(...), therefore avoiding recursive call to the listeners.

private final DefaultTableModel dtm = new DefaultTableModel();
private final JTable table = new JTable(dtm);
private final Object[] lastItem;
private final AtomicInteger lastIndex = new AtomicInteger(-1);
private final ItemValidator validator = new ItemValidator();


public YourConstructor() {

    lastItem = new Object[table.getColumnCount()];


    //store last value of selected table item in an array.
    table.addMouseListener(new MouseAdapter(){
        public void mouseClicked(MouseEvent evt){
            lastIndex.set(table.getSelectedRow());
            int row = lastIndex.get();
            for(int i=0;i<lastItem.length;i++){
                lastItem[i] = table.getValueAt(row, i);
            }
        }
    });

    //for input validation, and database update.
    dtm.addTableModelListener(new TableModelListener(){

        @Override
        public void tableChanged(TableModelEvent e) {
            switch(e.getType()){
            case TableModelEvent.INSERT:
                System.out.println("insert");
                break;
            case TableModelEvent.UPDATE:
                validateUpdate();
                break;
            case TableModelEvent.DELETE:
                System.out.println("delete");
                break;
            default:
                break;
            }
        }

    });
}

public void validateUpdate(){
    String item;
    for(int i=0;i<lastItem.length;i++)
    {
        item = (String)table.getValueAt(lastIndex.get(), i);
        if(i>1 && i<lastItem.length)//column range to be checked
        {
            if(!validator.hasNumericText(item))
            {
                final int col = i;
                final Object lastObject = lastItem[i];
                final int row = lastIndex.get();

                //the most important part, to avoid StackOverflow
                //by using EventQueue, you avoid looping around 
                //the TableModelListener.
                EventQueue.invokeLater(new Runnable(){
                    public void run(){
                        table.setValueAt(lastObject, row, col);
                    }
                });

                System.out.println("Error at " + i);
                break;
            }
        }
    }
}