Java JTable column loses TableCellRenderer when AbstractTableModel calls fireTableStructureChanged

I have a JTable where I would like to use a JProgressBar to render data in one of the columns. I can do this by calling setCellRenderer().

However, I also have a SwingWorker that can modify my AbstractTableModel to add a column to the table, in which case I call fireTableStructureChanged(). Unfortunately, this causes my column to lose the custom TableCellRenderer.

I have tried setAutoCreateColumnsFromModel(false), but this prevents my AbstractTableModel from adding a new column. Here is my code:

package components;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingWorker;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellRenderer;

public class LostRenderer extends JPanel {

    private static final long serialVersionUID = 1L;

    // Make the JTable a field, so inner class TableCellTask can access it
    private final JTable table;

    public class ProgressBarCellRenderer extends JProgressBar implements TableCellRenderer {

        private static final long serialVersionUID = 1L;

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
            setValue((int) value);
            return this;
        }
    }
    
    public LostRenderer() {
        super(new GridLayout(1,0));

        MyTableModel myTableModel = new MyTableModel();
        table = new JTable(myTableModel);
        table.setPreferredScrollableViewportSize(new Dimension(500, 70));
        table.setFillsViewportHeight(true);

        table.getColumn("Progress").setCellRenderer(new ProgressBarCellRenderer());
        /*
         * NOTE: table.setAutoCreateColumnsFromModel(true); (or commenting out the call)
         * results in the column renderer getting lost when table structure is changes.
         * Setting it to false maintains column renderer, but no new columns get added.
         */
        table.setAutoCreateColumnsFromModel(true);
        
        //Create the scroll pane and add the table to it.
        JScrollPane scrollPane = new JScrollPane(table);

        //Add the scroll pane to this panel.
        add(scrollPane);

        new TableCellTask(myTableModel).execute();
    }

    private class TableCellTask extends SwingWorker<Void, Integer> {
        
        private final MyTableModel myTableModel;
        
        public TableCellTask(MyTableModel myTableModel) {
            this.myTableModel = myTableModel;
        }
        
        @Override
        protected Void doInBackground() {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
            }
            publish(new Integer( 0));
            return null;
        }

        @Override
        protected void process(List<Integer> tableCells) {
            for (int tableCell : tableCells) {
                myTableModel.process(tableCell);
            }
        }
    }

    class MyTableModel extends AbstractTableModel {

        private static final long serialVersionUID = 1L;
        
        private List<Integer> data = new ArrayList<Integer>();

        public int getColumnCount() {
            return data.size() + 1;
        }

        public int getRowCount() {
            return 1;
        }

        public String getColumnName(int col) {
            if (col == 0) {
                return "Progress";
            } else {
                return "Data" + col;
            }
        }

        public Object getValueAt(int row, int col) {
            if (col == 0) {
                return 50;
            } else {
                return data.get(col - 1);
            }
        }

        public Class<?> getColumnClass(int c) {
            return getValueAt(0, c).getClass();
        }

        public void process(int value) {
            data.add(value);
            fireTableStructureChanged();
        }

    }

    /**
     * Create the GUI and show it.  For thread safety,
     * this method should be invoked from the
     * event-dispatching thread.
     * 
     */
    private static void createAndShowGUI() {
        //Create and set up the window.
        JFrame frame = new JFrame("LostRenderer");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        //Create and set up the content pane.
        LostRenderer newContentPane = new LostRenderer();
        newContentPane.setOpaque(true); //content panes must be opaque
        frame.setContentPane(newContentPane);

        //Display the window.
        frame.pack();
        frame.setVisible(true);
    }

    public static void main(String[] args) {

        //Schedule a job for the event-dispatching thread:
        //creating and showing this application's GUI.
        javax.swing.SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                createAndShowGUI();
            }
        });

    }
}

This is what the table looks like at the start (with correct JProgressBar rendering): Before AbstractTableModel calls fireTableStructureChanged()

And this is what it looks like after I add a column (column has lost JProgressBar rendering): After AbstractTableModel calls fireTableStructureChanged()

Any suggestions would be appreciated.

Answer

I had to rearrange your code, but I came up with the following GUI.

JTable Renderer 1

After 2 seconds.

JTable Renderer 2

The main change I made was to give the first column a unique class value in your TableModel. Then, I could set the JProgressBar renderer to that column as the default renderer.

It makes it much easier for the people reading your code to separate your classes and to organize code so that the important parts are first, and the more detailed parts follow.

There was no need to extend JPanel or JProgressBar. The only time you should extend a Swing component, or any Java class, is when you want to override one or more of the class methods.

Here’s the complete runnable code.

import java.awt.BorderLayout;
import java.awt.Component;
import java.util.ArrayList;
import java.util.List;

import javax.swing.JFrame;
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.table.AbstractTableModel;
import javax.swing.table.TableCellRenderer;

public class JTableRendererGUI implements Runnable {

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new JTableRendererGUI());
    }
    
    private JTable table;
    
    private MyTableModel myTableModel;
    
    public JTableRendererGUI() {
        this.myTableModel = new MyTableModel();
    }
    
    @Override
    public void run() {
        JFrame frame = new JFrame("JTable Renderer");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        
        frame.add(createMainPanel(), BorderLayout.CENTER);

        frame.pack();
        frame.setLocationByPlatform(true);
        frame.setVisible(true);
        
        new TableCellTask(myTableModel).execute();
    }
    
    private JPanel createMainPanel() {
        JPanel panel = new JPanel(new BorderLayout());
        
        table = new JTable(myTableModel);
        table.setDefaultRenderer(MyTableModel.class, 
                new ProgressBarCellRenderer());
        table.setAutoCreateColumnsFromModel(true);
        table.setFillsViewportHeight(true);
        JScrollPane scrollPane = new JScrollPane(table);
        
        panel.add(scrollPane, BorderLayout.CENTER);
        
        return panel;
    }
    
    public class TableCellTask extends SwingWorker<Void, Integer> {

        private final MyTableModel myTableModel;

        public TableCellTask(MyTableModel myTableModel) {
            this.myTableModel = myTableModel;
        }

        @Override
        protected Void doInBackground() {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            publish(0);
            return null;
        }

        @Override
        protected void process(List<Integer> tableCells) {
            for (int tableCell : tableCells) {
                myTableModel.process(tableCell);
            }
        }
    }
    
    public class MyTableModel extends AbstractTableModel {

        private static final long serialVersionUID = 1L;

        private List<Integer> data = new ArrayList<Integer>();

        public int getColumnCount() {
            return data.size() + 1;
        }

        public int getRowCount() {
            return 1;
        }

        public String getColumnName(int col) {
            if (col == 0) {
                return "Progress";
            } else {
                return "Data" + col;
            }
        }

        public Object getValueAt(int row, int col) {
            if (col == 0) {
                return 50;
            } else {
                return data.get(col - 1);
            }
        }

        public Class<?> getColumnClass(int c) {
            if (c == 0) {
                return MyTableModel.class;
            } else {
                return getValueAt(0, c).getClass();
            }
        }

        public void process(int value) {
            data.add(value);
            fireTableStructureChanged();
        }

    }
    
    public class ProgressBarCellRenderer implements TableCellRenderer {
        
        private JProgressBar progressBar;
        
        public ProgressBarCellRenderer() {
            this.progressBar = new JProgressBar();
        }

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, 
                boolean isSelected, boolean hasFocus, int row, int column) {
           progressBar.setValue((int) value);
           return progressBar;
        }

    }

}

Leave a Reply

Your email address will not be published. Required fields are marked *