Question

I am writing a class to track threads and display the status/progress in a JTable. What I had in mind was a JTable where all the necessary status/buttons/etc. were laid out in one column, one thread per row. I used a cell editor to get clickable buttons in the table, but the issue I can't work around is that items in the selected cell do not update unless I click on another cell. Is there a way to have the selected cell still update? The code below demonstrates the issue. Clicking the start button in a row will start the thread, but the progress in a row will not update while that row is selected.

import javax.swing.*;
import javax.swing.table.*;
import java.util.Random;
import java.lang.Thread;
import java.lang.Math;
import java.beans.*;
import java.util.concurrent.*;
import java.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.border.*;

/*
 * Program tracks some threads' progress, updating status in
 * cells in a JTable.
 *
 * Inner classes are MyDefTM, ThreadOB and ThreadCell which are
 * the table model, the object representing the thread's data and
 * the renderer/editor "stamp", respectively.
 */
public class ThreadManager {
    public JFrame jFrame;
    public JTable jTable;
    public MyDefTM tm;
    public JScrollPane jsp;

    public ThreadManager() {
        tm = new MyDefTM();
        tm.addColumn("Threads");
        jFrame = new JFrame("Thread List");
        jTable = new JTable();
        jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    public void createAndShowGUI() {
        /*
         * JTable in JScrollPane in JFrame.
         */
        jTable.setModel(tm);
        jTable.setRowHeight(60);
        jsp = new JScrollPane(jTable);
        jFrame.getContentPane().add(jsp);
        jFrame.pack();

        jTable.setDefaultRenderer(Object.class, new ThreadCell());
        jTable.setDefaultEditor(Object.class, new ThreadCell());
        jTable.setShowHorizontalLines(true);

        /*
         * Add some test threads.
         */
        for (int ii = 0; ii < 5; ii++) {
            ThreadOb to = new ThreadOb(ii, jTable);
            Vector v = new Vector();
            v.add(to);
            tm.addRow(v);
        }

        jFrame.setSize(640, 480);
        jFrame.setVisible(true);
        return; 
    }

    public static void main(String[] args) {
        ThreadManager threadManager = new ThreadManager();
        threadManager.createAndShowGUI();
    }

    /*
     * Use DefaultTableModel but make every cell editable.
     */
    public class MyDefTM extends DefaultTableModel {
        public boolean isCellEditable(int row, int column) {
            return true;
        }
    }

    /*
     * Represents a thread as stored in the table.  Stores
     * an ID for the thread, its progress and the result.
     */
    public class ThreadOb {
        public int threadID;
        public int threadProgress;
        public JTable jTable;
        public SwingWorker workerThread;
        public String threadResult;

        public ThreadOb(int id, JTable t) {
            jTable = t;
            threadID = id;
            threadProgress = 0;
        }

        public void buttonAction() {
            /*
             * Perform a task that takes just a little while to finish.
             */
            workerThread = new SwingWorker<String,Object>() {
                @Override
                public String doInBackground() throws InterruptedException {
                    int prog = 0;
                    Random rand = new Random(42);
                    while (prog < 100) {
                        prog += Math.abs(rand.nextInt() % 5);
                        setProgress(prog);
                        Thread.sleep(1000);
                        setProgress(Math.min(prog, 100));
                    }
                    return "42";
                }
            };

            workerThread.addPropertyChangeListener(new PropertyChangeListener() {
                @Override
                public void propertyChange(PropertyChangeEvent e) {
                    if (e.getPropertyName() == "state" && 
                                                    e.getNewValue() == "DONE") {
                        try {
                            threadResult = (String) workerThread.get();
                        } catch (Exception ignore) { }
                    }

                    if (e.getPropertyName() == "progress") {
                        threadProgress = ( (Integer) e.getNewValue()).intValue();
                        /*
                         * Couple the model and view together.  The table cells will
                         * not update without this line.
                         */
                        ((MyDefTM) jTable.getModel()).fireTableDataChanged();
                    }
                }

            });
            workerThread.execute();
        }
    }

    /*
     * Represents the graphical "stamp" for the renderer and editor.
     */
    public class ThreadCell extends AbstractCellEditor 
                            implements TableCellRenderer, TableCellEditor {

        private JLabel threadIDLabel;
        private JLabel threadProgressLabel;
        private JPanel threadPane;
        private JButton but;
        private JPanel statuspane;
        private JProgressBar jpb;
        private JPanel pane;
        private Border offBorder;
        private Border onBorder;
        private ThreadOb val;

        /*
         * Establish the layout of the cells in the JTable.
         * The selected cell has a red border.
         */
        public ThreadCell() {
            val = null;
            threadIDLabel = new JLabel();
            threadProgressLabel = new JLabel();
            threadPane = new JPanel();
            threadPane.setLayout(new BoxLayout(threadPane, BoxLayout.X_AXIS));
            threadPane.add(threadIDLabel);
            threadPane.add(threadProgressLabel);
            statuspane = new JPanel();
            statuspane.setLayout(new BoxLayout(statuspane, BoxLayout.X_AXIS));
            statuspane.add(threadPane);
            statuspane.add(Box.createHorizontalGlue());
            but = new JButton("Start");
            statuspane.add(but);
            jpb = new JProgressBar(0, 100);
            jpb.setStringPainted(true);
            pane = new JPanel();
            pane.setLayout(new BoxLayout(pane, BoxLayout.Y_AXIS));
            pane.add(statuspane);
            pane.add(jpb);
            offBorder = BorderFactory.createEmptyBorder(2,2,2,2);
            onBorder = BorderFactory.createLineBorder(java.awt.Color.RED, 2);
            pane.setBorder(offBorder);

            but.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    val.buttonAction();
                    /*
                     * Uncomment to deselect the cell after clicking.  
                     */
                    //fireEditingStopped();
                }
            });
        }

        /*
         * Populate the cell with the correct values.
         */
        public void update(JTable table, 
                           Object value, 
                           boolean isSelected, 
                           boolean hasFocus, 
                           int row, 
                           int column) {
            if (value == null) {
                return;
            }

            val = (ThreadOb) value;
            threadIDLabel.setText("ID: " + ((ThreadOb) value).threadID + "   ");
            threadProgressLabel.setText("Progress:  " +
                                    ((ThreadOb) value).threadProgress + "%");
            jpb.setValue(((ThreadOb) value).threadProgress);

            if (hasFocus) {
                pane.setBorder(onBorder);
            } else {
                pane.setBorder(offBorder);
            }
        }

        public Component getTableCellRendererComponent(JTable table, 
                                                       Object value, 
                                                       boolean isSelected, 
                                                       boolean hasFocus, 
                                                       int row, 
                                                       int column) {
            update(table, value, isSelected, hasFocus, row, column);
            return pane;
        }

        public Component getTableCellEditorComponent(JTable table, 
                                                     Object value, 
                                                     boolean isSelected, 
                                                     int row, 
                                                     int column) {
            update(table, value, isSelected, true, row, column);
            return pane;
        }

        public Object getCellEditorValue() {
            return val;
        }
    }
}
Was it helpful?

Solution

There are so many things wrong (sorry), it's kind of scary...

  • This e.getPropertyName() == "state" is not how String comparisons are done. Your if statement will NEVER be true. Use something like "state".equals(e.getPropertyName()) instead...

  • The UI isn't begin updated because the cell is still in edit mode.

  • In your cell editors, actionPerformed method, add a stopCellEditing statement, this will dismiss the cell editor and allow the cell renderer to do its job

  • This not, really, the right way to use a JTable

Updated with a POSSIBLE example

This is just an example of a possible solution.

JTable is suppose to show rows and columns of data. The way you have it setup currently, it would be simpler to just layout your monitor panels on another panel using something like a GridBagLayout - IMHO

enter image description here

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.EventQueue;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.EventObject;
import java.util.List;
import java.util.Random;
import javax.swing.AbstractCellEditor;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;

public class ThreadMonitorExample {

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

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

                ThreadTableModel model = new ThreadTableModel();
                model.add(new Task(0, model));
                model.add(new Task(1, model));
                model.add(new Task(2, model));
                model.add(new Task(3, model));
                model.add(new Task(4, model));
                JTable table = new JTable(model);

                TaskProgressRenderer progressRenderer = new TaskProgressRenderer();
                TaskStatusRenderer statusRenderer = new TaskStatusRenderer();
                table.getColumnModel().getColumn(1).setCellRenderer(progressRenderer);
                table.getColumnModel().getColumn(2).setCellRenderer(statusRenderer);
                table.getColumnModel().getColumn(2).setCellEditor(new TaskStatusEditor());

                table.setRowHeight(
                                Math.max(getCellRendererHeight(table, 0, 1, progressRenderer), 
                                getCellRendererHeight(table, 0, 2, statusRenderer)));

                JFrame frame = new JFrame("Test");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
                frame.add(new JScrollPane(table));
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    protected int getCellRendererHeight(JTable table, int row, int column, TableCellRenderer renderer) {
        return renderer.getTableCellRendererComponent(table, table.getValueAt(row, column), true, true, row, column).getPreferredSize().height;
    }

    public class ThreadTableModel extends AbstractTableModel {

        private String[] headers = {"ID", "Progress", "Action"};
        private List<Task> tasks;

        public ThreadTableModel() {
            tasks = new ArrayList<>(25);
        }

        public void add(Task task) {
            int row = getRowCount();
            tasks.add(task);
            fireTableRowsInserted(row, getRowCount() - 1);
        }

        @Override
        public boolean isCellEditable(int rowIndex, int columnIndex) {
            return columnIndex == 2 && !tasks.get(rowIndex).isRunning() && !tasks.get(rowIndex).isDone();
        }

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

        @Override
        public int getColumnCount() {
            return headers.length;
        }

        @Override
        public Class<?> getColumnClass(int columnIndex) {
            Class clazz = Object.class;
            switch (columnIndex) {
                case 0:
                    clazz = String.class;
                    break;
                case 1:
                    clazz = Integer.class;
                    break;
            }
            return clazz;
        }

        @Override
        public String getColumnName(int column) {
            return headers[column];
        }

        @Override
        public Object getValueAt(int rowIndex, int columnIndex) {
            Task task = tasks.get(rowIndex);
            Object value = null;
            switch (columnIndex) {
                case 0:
                    value = task.getID();
                    break;
                case 1:
                    value = task.getProgress();
                    break;
                case 2:
                    value = task;
                    break;
            }
            return value;
        }

        @Override
        public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
            System.out.println("setValueAt " + rowIndex + "x" + columnIndex);
            if (columnIndex == 2) {
                Task task = tasks.get(rowIndex);
                if (!task.isRunning() && !task.isDone()) {
                    task.execute();
                    fireTableCellUpdated(rowIndex, columnIndex);
                }
            }
        }

        public void updated(Task task) {
            int row = tasks.indexOf(task);
            System.out.println("Row updated " + row);
            fireTableRowsUpdated(row, row);
        }
    }

    public class TaskProgressRenderer extends JProgressBar implements TableCellRenderer {

        public TaskProgressRenderer() {
            setOpaque(false);
        }

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
            if (value instanceof Integer) {
                int progress = (int) value;
                System.out.println("cellProgress = " + progress);
                setValue(progress);
            }
            if (isSelected) {
                setBackground(table.getSelectionBackground());
                setOpaque(true);
            } else {
                setBackground(table.getBackground());
                setOpaque(false);
            }
            return this;
        }

    }

    public class TaskStatusEditor extends AbstractCellEditor implements TableCellEditor {

        private JPanel editor;

        public TaskStatusEditor() {
            editor = new JPanel();
            editor.add(new JButton("Start"));
        }

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

        @Override
        public Object getCellEditorValue() {
            return null;
        }

        @Override
        public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    stopCellEditing();
                }
            });
            return editor;
        }

    }

    public class TaskStatusRenderer extends JPanel implements TableCellRenderer {

        private JButton start;
        private JLabel label;

        public TaskStatusRenderer() {
            setOpaque(false);
            start = new JButton("Start");
            label = new JLabel();
        }

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
            removeAll();
            if (value instanceof Task) {
                Task task = (Task) value;
                if (task.isDone()) {
                    try {
                        label.setText(task.get());
                    } catch (Exception ex) {
                        label.setText(ex.getMessage());
                    }
                    add(label);
                } else if (task.isRunning()) {
                    label.setText("Working");
                    add(label);
                } else {
                    add(start);
                }
            }
            if (isSelected) {
                setBackground(table.getSelectionBackground());
                setOpaque(true);
            } else {
                setBackground(table.getBackground());
                setOpaque(false);
            }
            return this;
        }

    }

    public class Task extends SwingWorker<String, Object> {

        private int id;
        private String threadResult;
        private ThreadTableModel tableModel;
        private boolean running;

        public Task(int id, ThreadTableModel tableModel) {
            this.tableModel = tableModel;
            this.id = id;
            addPropertyChangeListener(new PropertyChangeListener() {
                @Override
                public void propertyChange(PropertyChangeEvent e) {
                    System.out.println(e.getPropertyName());
                    if ("state".equalsIgnoreCase(e.getPropertyName())
                                    && "DONE".equalsIgnoreCase(e.getNewValue().toString())) {
                        try {
                            threadResult = (String)get();
                        } catch (Exception ignore) {
                            ignore.printStackTrace();
                        }
                    }

                    if ("progress".equalsIgnoreCase(e.getPropertyName())) {
                        System.out.println("task.getProgress = " + getProgress());
                        Task.this.tableModel.updated(Task.this);
                    }
                }
            });
        }

        public boolean isRunning() {
            return running;
        }

        public int getID() {
            return id;
        }

        @Override
        protected String doInBackground() throws Exception {
            running = true;
            setProgress(0);
            int prog = 0;
            Random rand = new Random(42);
            while (prog < 100) {
                prog += Math.abs(rand.nextInt() % 5);
                Thread.sleep(250);
                setProgress(Math.min(prog, 100));
            }
            return "42";
        }
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top