ProgressBar inside JTable

2019-09-17 10:00发布

问题:

I have a problem that I can't solve using JTable with editor and renderer containing JProgressBar inside. I have a JButton that is use to start a thread that increment the progress bar value. The problem is when I click on the JTable's cell, the progress bar is not refreshing itself anymore. I tried to add a ChangeListener to the progress bar that terminate edits, but then other cells are not editable as well.
Here is a SSCCE:

public class TableTest {

    final static MyObjectTableModel model = new MyObjectTableModel();
    final static JTable table = new JTable(model);
    private static Map<Integer, Future> mapSubmittedReadProgress = new HashMap<Integer, Future>();
    final static StartProgressActionListener progressActionListener = new StartProgressActionListener();
    final static CloseActionListener closeActionListener = new CloseActionListener();
    final static ProgressChangeListener progressChangeListener = new ProgressChangeListener();

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                new TableTest().createGUI();
            }
        });
    }

    public static class MyObjectTableModel extends AbstractTableModel {

        private LinkedList<MyObject> myList;

        public MyObjectTableModel() {
            super();
            myList = new LinkedList<MyObject>();
        }

        public MyObjectTableModel(SortedSet<MyObject> myObjects) {
            super();
            this.myList = new LinkedList<MyObject>(myObjects);
        }

        public void addRow(MyObject myObject) {
            myList.add(myObject);
            fireTableRowsInserted(myList.size() - 1, myList.size() - 1);
        }

        public void removeRow(int row) {
            myList.remove(row);
            fireTableRowsDeleted(row, row);
        }

        @Override
        public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
            myList.set(rowIndex, (MyObject) aValue);
            fireTableCellUpdated(rowIndex, 0);
        }

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

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

        @Override
        public Class<?> getColumnClass(int columnIndex) {
            switch (columnIndex) {
                case 0:
                    return MyObject.class;
                default:
                    throw new IllegalArgumentException("invalid column: " + columnIndex);
            }
        }

        @Override
        public Object getValueAt(int rowIndex, int columnIndex) {
            switch (columnIndex) {
                case 0:
                    return myList.get(rowIndex);
                default:
                    throw new IllegalArgumentException("invalid column: " + columnIndex);
            }
        }

        public MyObject getMyObjectAt(int row) {
            return myList.get(row);
        }

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

        public int getIndexOf(MyObject myObject) {
            return myList.indexOf(myObject);
        }
    }

    private static void createGUI() {
        JFrame f = new JFrame("TableTest");
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        table.getModel().addTableModelListener(new TableModelListener() {
            @Override
            public void tableChanged(TableModelEvent e) {
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        setPreferredRowHeights();
                    }
                });
            }
        });

        for (int i = 0; i < 16; i++) {
            MyObject myObject = new MyObject();
            myObject.setText1("" + i);
            model.addRow(myObject);
        }
        table.setOpaque(false);
        table.setShowGrid(false);
        table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
        table.setDefaultRenderer(MyObject.class, new MyTableCellRenderer());
        table.setDefaultEditor(MyObject.class, new MyTableCellEditor());
        table.setFillsViewportHeight(true);
        f.add(new JScrollPane(table));

        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    private static void setPreferredRowHeights() {
        for (int row = 0; row < table.getRowCount(); row++) {
            setPreferredRowHeight(row);
        }
    }

    private static void setPreferredRowHeight(int row) {
        int prefHeight = getPreferredRowHeight(row);
        table.setRowHeight(row, prefHeight);
    }

    public static int getPreferredRowHeight(int row) {
        int pref = 0;
        for (int column = 0; column < table.getColumnCount(); column++) {
            TableCellRenderer renderer = table.getCellRenderer(row, column);
            Component comp = table.prepareRenderer(renderer, row, column);
            pref = Math.max(pref, comp.getPreferredSize().height);
        }
        return pref > 0 ? pref : table.getRowHeight();
    }

    private static class MyTableCellEditor extends AbstractCellEditor implements TableCellEditor {

        private MyObjectPanel myObjectPanel = new MyObjectPanel();
        private transient List<CellEditorListener> listeners;

        public MyTableCellEditor() {
            myObjectPanel.addStartProgressActionListener(progressActionListener);
            myObjectPanel.addCloseActionListener(closeActionListener);
//            myObjectPanel.addProgressChangeListener(progressChangeListener);
            listeners = new ArrayList<>();
        }

        @Override
        public boolean isCellEditable(EventObject e) {
            return true;
        }

        @Override
        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
            MyObject myObject = (MyObject) value;
            myObjectPanel.setMyObject(myObject);
            return myObjectPanel;
        }

        @Override
        public Object getCellEditorValue() {
            MyObject myObject = myObjectPanel.getMyObject();
            return myObject;
        }

        @Override
        public void addCellEditorListener(CellEditorListener l) {
            listeners.add(l);
        }

        @Override
        public void removeCellEditorListener(CellEditorListener l) {
            listeners.remove(l);
        }

        @Override
        protected void fireEditingStopped() {
            ChangeEvent ce = new ChangeEvent(this);
            for (int i = listeners.size() - 1; i >= 0; i--) {
                ((CellEditorListener) listeners.get(i)).editingStopped(ce);
            }
        }
    }

    private static class MyTableCellRenderer implements TableCellRenderer {

        private MyObjectPanel myObjectPanel = new MyObjectPanel();

        public MyTableCellRenderer() {
            myObjectPanel.addStartProgressActionListener(progressActionListener);
            myObjectPanel.addCloseActionListener(closeActionListener);
//            myObjectPanel.addProgressChangeListener(progressChangeListener);
        }

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
            MyObject myObject = (MyObject) value;
            myObjectPanel.setMyObject(myObject);
            return myObjectPanel;
        }
    }

    private static class ProgressChangeListener implements ChangeListener {

        @Override
        public void stateChanged(ChangeEvent e) {
            if (table.isEditing()) {
                table.getCellEditor().stopCellEditing();
            }
        }
    }

    private static class CloseActionListener implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent e) {
            if (table.isEditing()) {
                table.getCellEditor().stopCellEditing();
            }
            model.removeRow(table.getSelectedRow());
        }
    }

    private static class StartProgressActionListener implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent e) {
            if (table.isEditing()) {
                table.getCellEditor().stopCellEditing();
            }
            final ExecutorService executor = Executors.newFixedThreadPool(1);
            int row = table.getSelectedRow();
            MyObject myObject = (MyObject) table.getValueAt(row, 0);
            myObject.setStartEnable(false);
            myObject.setText1Enable(false);
            myObject.setText2Enable(false);
            Runnable progressRunnable = new ProgressRunnable(table.getSelectedRow(), myObject);
            final Future<?> submit = executor.submit(progressRunnable);
            mapSubmittedReadProgress.put(table.getSelectedRow(), submit);
        }
    }

    private static class ProgressRunnable implements Runnable {

        private ExecutorService executor;
        private long beT;
        private int dur = 30; // s
        private int progress = 0;
        private int row;
        private MyObject myObject;

        public ProgressRunnable(int row) {
        }

        private ProgressRunnable(int selectedRow, MyObject myObject) {
            this.row = selectedRow;
            this.myObject = myObject;
            beT = System.currentTimeMillis();
        }

        @Override
        public void run() {
            boolean abort = false;
            int i = 0;
            while (i <= dur && !abort) {
                final long curT = System.currentTimeMillis();
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    abort = true;
                    executor.shutdown();
                }
                if (Thread.currentThread().isInterrupted()) {
                    abort = true;
                    executor.shutdown();
                }
                progress = (int) Math.round(100 * ((double) (curT - beT) / 1000) / dur);
                myObject.setProgress(progress);
                table.setValueAt(myObject, row, 0);
                i++;
            }
        }
    }

    // My object
    static class MyObject {

        private String text1;
        private String text2;
        private int progress;
        private boolean startEnable = true;
        private boolean abortEnable = true;
        private boolean text1Enable = true;
        private boolean text2Enable = true;
        private boolean closeEnable = true;

        public String getText1() {
            return text1;
        }

        public void setText1(String text1) {
            this.text1 = text1;
        }

        public String getText2() {
            return text2;
        }

        public void setText2(String text2) {
            this.text2 = text2;
        }

        public int getProgress() {
            return progress;
        }

        public void setProgress(int progress) {
            this.progress = progress;
        }

        public boolean isStartEnable() {
            return startEnable;
        }

        public void setStartEnable(boolean startEnable) {
            this.startEnable = startEnable;
        }

        public boolean isAbortEnable() {
            return abortEnable;
        }

        public void setAbortEnable(boolean abortEnable) {
            this.abortEnable = abortEnable;
        }

        public boolean isText1Enable() {
            return text1Enable;
        }

        public void setText1Enable(boolean text1Enable) {
            this.text1Enable = text1Enable;
        }

        public boolean isText2Enable() {
            return text2Enable;
        }

        public void setText2Enable(boolean text2Enable) {
            this.text2Enable = text2Enable;
        }

        public boolean isCloseEnable() {
            return closeEnable;
        }

        public void setCloseEnable(boolean closeEnable) {
            this.closeEnable = closeEnable;
        }
    }

    // MyObjectPanel
    static class MyObjectPanel extends javax.swing.JPanel {

        /**
         * Creates new form MyObjectPanel
         */
        public MyObjectPanel() {
            initComponents();
        }

        /**
         * This method is called from within the constructor to initialize the
         * form. WARNING: Do NOT modify this code. The content of this method is
         * always regenerated by the Form Editor.
         */
        @SuppressWarnings("unchecked")
        // <editor-fold defaultstate="collapsed" desc="Generated Code">
        private void initComponents() {

            jTextField1 = new javax.swing.JTextField();
            jTextField2 = new javax.swing.JTextField();
            jProgressBar1 = new javax.swing.JProgressBar();
            btnStart = new javax.swing.JButton();
            btnStop = new javax.swing.JButton();
            btnClose = new javax.swing.JButton();

            btnStart.setText("Start");

            btnStop.setText("Stop");

            btnClose.setText("Close");

            javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
            this.setLayout(layout);
            layout.setHorizontalGroup(
                    layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addGroup(layout.createSequentialGroup()
                    .addContainerGap()
                    .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(jTextField1)
                    .addComponent(jTextField2)
                    .addComponent(jProgressBar1, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addGroup(layout.createSequentialGroup()
                    .addComponent(btnStart)
                    .addGap(18, 18, 18)
                    .addComponent(btnStop)
                    .addGap(18, 18, 18)
                    .addComponent(btnClose)
                    .addGap(0, 199, Short.MAX_VALUE)))
                    .addContainerGap()));
            layout.setVerticalGroup(
                    layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addGroup(layout.createSequentialGroup()
                    .addContainerGap()
                    .addComponent(jTextField1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                    .addComponent(jTextField2, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addGap(18, 18, 18)
                    .addComponent(jProgressBar1, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                    .addGap(18, 18, 18)
                    .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE)
                    .addComponent(btnStart)
                    .addComponent(btnStop)
                    .addComponent(btnClose))
                    .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)));
        }// </editor-fold>
        // Variables declaration - do not modify
        private javax.swing.JButton btnClose;
        private javax.swing.JButton btnStart;
        private javax.swing.JButton btnStop;
        private javax.swing.JProgressBar jProgressBar1;
        private javax.swing.JTextField jTextField1;
        private javax.swing.JTextField jTextField2;
        // End of variables declaration

        void setMyObject(MyObject myObject) {
            jTextField1.setText(myObject.getText1());
            jTextField2.setText(myObject.getText2());
            jProgressBar1.setValue(myObject.getProgress());
            btnStart.setEnabled(myObject.isStartEnable());
            btnClose.setEnabled(myObject.isCloseEnable());
            btnStop.setEnabled(myObject.isAbortEnable());
            jTextField1.setEnabled(myObject.isText1Enable());
            jTextField2.setEnabled(myObject.isText2Enable());
        }

        MyObject getMyObject() {
            MyObject myObject = new MyObject();
            myObject.setText1(jTextField1.getText());
            myObject.setText2(jTextField2.getText());
            myObject.setProgress(jProgressBar1.getValue());
            myObject.setStartEnable(btnStart.isEnabled());
            myObject.setCloseEnable(btnClose.isEnabled());
            myObject.setAbortEnable(btnStop.isEnabled());
            myObject.setText1Enable(jTextField1.isEnabled());
            myObject.setText2Enable(jTextField2.isEnabled());
            return myObject;
        }

        void addStartProgressActionListener(ActionListener progressActionListener) {
            btnStart.addActionListener(progressActionListener);
        }

        void addCloseActionListener(ActionListener closeActionListener) {
            btnClose.addActionListener(closeActionListener);
        }

        void addProgressChangeListener(ChangeListener changeListener) {
            jProgressBar1.addChangeListener(changeListener);
        }
    }
}


Thanks for any help.

回答1:

There's a few things to change to make it work.

First, your MyObjectPanel class needs to have a MyObject variable and return that instead of a new one each time getMyObject is called. This variable is set when setMyObject is called on MyObjectPanel.

// End of variables declaration

private MyObject object;
void setMyObject(MyObject myObject) {
  object = myObject;
...
}

MyObject getMyObject() {
  return object;
}

Second, your MyObject class needs a int row variable to be able to know which row it represents. Make a getter too for this variable. Set this variable in the for loop in createGUI .

Last, in the while of your ProgressRunnable in run, you need to stop editing the cell if that's the one your progress is running. To do that, you check with JTable.getEditingRow, if the table isEditing that this row is the same as myObject.getRow that the progress myObject variable. Something like this :

myObject.setProgress(progress);
          if (table.isEditing()) {
            if (table.getEditingRow() == myObject.getRow()) {
              table.getCellEditor().stopCellEditing();
            }
          }
          model.fireTableRowsUpdated(row, row);

Also note that I changed the setValue there for fireTableRowsUpdated instead.

This should make it work as you want.

You can remove your TableModelListener in createGUI and only call setPreferredRowHeights once before pack.