setAutoCreateRowSorter doesn't sort table column correctly after update

前端 未结 2 1717
抹茶落季
抹茶落季 2020-12-21 11:15

While developing a small task manager, I have noticed that columns aren\'t sorted correctly. To discard problems with my program, I have created a minimal version but it sti

相关标签:
2条回答
  • 2020-12-21 11:44

    Using setSortsOnUpdates(), suggested here by @trcs, is the best general solution, but you may be able to optimize updates by the choice of TableModelEvent available to subclasses of AbstractTableModel.

    The critical issue is the implementation of setValueAt(). If you meant fireTableRowsUpdated(), instead of fireTableRowUpdated(), note that the parameters represent a range of rows, not a row & column. In this case, because "all cell values in the table's rows may have changed," the revised example below invokes fireTableDataChanged(). I've also changed the model to manage a List<Integer> and normalized the size, N.

    import java.awt.BorderLayout;
    import java.awt.Dimension;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Random;
    import javax.swing.*;
    import javax.swing.table.AbstractTableModel;
    
    /** @see https://stackoverflow.com/a/36522182/230513 */
    public class TableSortTest extends JFrame {
    
        private final JTable table;
        private final ATableModel model;
    
        public TableSortTest() {
            setDefaultCloseOperation(EXIT_ON_CLOSE);
    
            model = new ATableModel();
            table = new JTable(model){
                @Override
                public Dimension getPreferredScrollableViewportSize() {
                    return new Dimension(200, 500);
                }
            };
            table.setFillsViewportHeight(true);
            table.setAutoCreateRowSorter(true);
    
            add(new JScrollPane(table), BorderLayout.CENTER);
            pack();
            setLocationRelativeTo(null);
            setVisible(true);
    
            Worker worker = new Worker();
            worker.execute();
        }
    
        private class Pair {
    
            int index;
            int value;
        }
    
        private class Worker extends SwingWorker<Void, Pair> {
    
            private static final int N = 100;
            private final Random r = new Random();
    
            @Override
            protected Void doInBackground() {
                while (!isCancelled()) {
                    for (int i = 0; i < N; i++) {
                        int index = r.nextInt(N);
                        Pair p = new Pair();
                        p.index = index;
                        p.value = Math.abs(r.nextInt());
                        publish(p);
                    }
    
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException ie) {
                        ie.printStackTrace();
                    }
                }
    
                return null;
            }
    
            @Override
            public void process(List<Pair> items) {
                for (Pair p : items) {
                    model.setValueAt(p.value, p.index, 0);
                }
            }
        }
    
        private class ATableModel extends AbstractTableModel {
    
            private static final int N = 100;
            private final List<Integer> data = new ArrayList<>(N);
    
            public ATableModel() {
                final Random r = new Random();
                for (int i = 0; i < N; i++) {
                    data.add(Math.abs(r.nextInt()));
                }
            }
    
            @Override
            public int getColumnCount() {
                return 1;
            }
    
            @Override
            public int getRowCount() {
                return data.size();
            }
    
            @Override
            public Object getValueAt(int rowIndex, int columnIndex) {
                return data.get(rowIndex);
            }
    
            @Override
            public void setValueAt(Object value, int rowIndex, int columnIndex) {
                data.set(rowIndex, (Integer) value);
                fireTableDataChanged();
            }
    
            @Override
            public Class getColumnClass(int columnIndex) {
                return Integer.class;
            }
    
            @Override
            public String getColumnName(int col) {
                return "Column";
            }
        }
    
        public static final void main(String[] args) {
            SwingUtilities.invokeLater(() -> {
                new TableSortTest();
            });
        }
    }
    

    Recognizing that this is just an example, the variation below optimizes updates by publishing a List<Integer>, which is passed en bloc to the TableModel via process().

    import java.awt.BorderLayout;
    import java.awt.Dimension;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Random;
    import javax.swing.*;
    import javax.swing.table.AbstractTableModel;
    
    /**
     * @ see https://stackoverflow.com/a/36522182/230513
     */
    public class TableSortTest extends JFrame {
    
        private final JTable table;
        private final ATableModel model;
    
        public TableSortTest() {
            setDefaultCloseOperation(EXIT_ON_CLOSE);
    
            model = new ATableModel();
            table = new JTable(model) {
                @Override
                public Dimension getPreferredScrollableViewportSize() {
                    return new Dimension(200, 500);
                }
            };
            table.setFillsViewportHeight(true);
            table.setAutoCreateRowSorter(true);
    
            add(new JScrollPane(table), BorderLayout.CENTER);
            pack();
            setLocationRelativeTo(null);
            setVisible(true);
    
            Worker worker = new Worker();
            worker.execute();
        }
    
        private class Worker extends SwingWorker<List<Integer>, List<Integer>> {
    
            private static final int N = 100;
            private final Random r = new Random();
            private final List<Integer> data = new ArrayList<>(N);
    
            @Override
            protected List<Integer> doInBackground() throws Exception {
                while (!isCancelled()) {
                    data.clear();
                    for (int i = 0; i < N; i++) {
                        data.add(Math.abs(r.nextInt()));
                    }
                    publish(data);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException ie) {
                        ie.printStackTrace(System.err);
                    }
                }
                return data;
            }
    
            @Override
            protected void process(List<List<Integer>> chunks) {
                for (List<Integer> chunk : chunks) {
                    model.update(chunk);
                }
            }
        }
    
        private class ATableModel extends AbstractTableModel {
    
            private List<Integer> data = new ArrayList<>();
    
            public void update(List<Integer> data) {
                this.data = data;
                fireTableDataChanged();
            }
    
            @Override
            public int getColumnCount() {
                return 1;
            }
    
            @Override
            public int getRowCount() {
                return data.size();
            }
    
            @Override
            public Object getValueAt(int rowIndex, int columnIndex) {
                return data.get(rowIndex);
            }
    
            @Override
            public Class getColumnClass(int columnIndex) {
                return Integer.class;
            }
    
            @Override
            public String getColumnName(int col) {
                return "Column";
            }
        }
    
        public static final void main(String[] args) {
            SwingUtilities.invokeLater(() -> {
                new TableSortTest();
            });
        }
    }
    
    0 讨论(0)
  • 2020-12-21 11:53

    Thanks for your quick answer trashgod. You're right, I meant fireTableRowsUpdated () but I made a mistake when I wrote the code, sorry. The point is that fireTableRowsUpdated (rowIndex, rowIndex) and fireTableCellUpdated (rowIndex, columnIndex) both fail to sort the column correctly. In the real program most of the table rows do change from one iteration to the next so calling fireTableDataChanged () makes perfect sense. But I didn't want to use it because if I select one or more rows to send a signal to the processes or whatever the selection is lost on every update. I have explored this way and found two forms of preserving the selection but it's a bit annoying and one of them breaks the selection with the keyboard. I show the necessary additions to the original code next.

    The first form saves the selection before modifying the model and restores it after every update:

    ...
    private class Worker extends SwingWorker <Void, Pair>
    {
        private int [] selectedRows;
    
        @Override
        protected Void doInBackground ()
        {
            while (!isCancelled ())
            {
                // Save the selection before modifying the model
                int x = table.getSelectedRowCount ();
                if (x > 0)
                {
                    selectedRows = new int [x];
                    int [] tableSelection = table.getSelectedRows ();
    
                    for (int i = 0; i < x; i++)
                    {
                        selectedRows [i] = table.convertRowIndexToModel (tableSelection [i]);
                    }
                }
    
                Random r = new Random ();
                for (int i = 0; i < table.getRowCount (); i++)
                {
                    int indice = getIndexInRange (0, table.getRowCount () - 1);
                    Pair p = new Pair ();
                    p.index = indice;
                    p.value = Math.abs (r.nextInt ());
                    publish (p);
                }
    
                // If I put the code to restore the selection here, it doesn't work...
                try
                {
                    Thread.sleep (1000);
                }
                catch (InterruptedException ie)
                {
                    ie.printStackTrace ();
                }
            }
    
            return null;
        }
    
        @Override
        public void process (List <Pair> items)
        {
           for (Pair p : items)
           {
               model.setValueAt (p.value, p.index, 1);
           }
    
           // Restore the selection on every update
           if (selectedRows != null && selectedRows.length > 0)
           {
               for (int i = 0; i < selectedRows.length; i++)
               {
                   table.addRowSelectionInterval (table.convertRowIndexToView (selectedRows [i]), table.convertRowIndexToView (selectedRows [i]));
               }
           }
        }
    }
    ...
    

    The second form uses a ListSelectionListener, a KeyListener, and a flag. Selection with the keyboard doesn't work. To be honest, I don't know how did I come to get this solution. It probably was by chance:

    public class TableSortTestSolucionConSelectionListener extends JFrame implements KeyListener
    {
        ...
        private boolean ctrlOrShiftDown = false;
        private int [] selectedRows;
    
        @Override
        public void keyPressed (KeyEvent e)
        {
            ctrlOrShiftDown = e.isControlDown () || e.isShiftDown ();
        }
    
        @Override
        public void keyReleased (KeyEvent e)
        {
           ctrlOrShiftDown = e.isControlDown () || e.isShiftDown ();
        }
    
        @Override
        public void keyTyped (KeyEvent e)
        {
           ctrlOrShiftDown = e.isControlDown () || e.isShiftDown ();
        }
    
        public TableSortTestSolucionConSelectionListener ()
        {
            ...
            ListSelectionListener lsl = new ListSelectionListener ()
            {
                @Override
                public void valueChanged (ListSelectionEvent e)
                {
                    if (!e.getValueIsAdjusting ())
                    {
                        if (!ctrlOrShiftDown)
                        {
                            int x = table.getSelectedRowCount ();
                            if (x > 0)
                            {
                                selectedRows = new int [x];
                                int [] tableSelection = table.getSelectedRows ();
    
                                for (int i = 0; i < x; i++)
                                {
                                    selectedRows [i] = table.convertRowIndexToModel (tableSelection [i]);
                                }
                            }
                        }
    
                        // Disable the listener to avoid infinite recursion
                        table.getSelectionModel ().removeListSelectionListener (this);
    
                        if (selectedRows != null && selectedRows.length > 0)
                        {
                            for (int i = 0; i < selectedRows.length; i++)
                            {
                                table.addRowSelectionInterval (table.convertRowIndexToView (selectedRows [i]), table.convertRowIndexToView (selectedRows [i]));
                            }
                        }
    
                        table.getSelectionModel ().addListSelectionListener (this);
                    }
                }
            };
    
            table.getSelectionModel ().addListSelectionListener (lsl);
            ...      
         }
    

    Fortunately today I have found a simple way to get the column sorted correctly and keep the current selection. You only have to add the following to your code:

    TableRowSorter trs = (TableRowSorter) table.getRowSorter ();
    trs.setSortsOnUpdates (true);
    

    With this both fireTableCellUpdated () and fireTableRowsUpdated () work as I expected. To my understanding, setAutoCreateRowSorter () is only used to sort the rows when you click on the table header.

    Greetings.

    0 讨论(0)
提交回复
热议问题