javaswingjtablejprogressbar

Why does adding an indeterminate JProgressBar to JTable kill the animation?


I'm reverting a JavaFX UI to Swing. In the UI, I have a table of tasks with progress bars. When a task is of indeterminate length, I'd like to use an indeterminate progress bar. Unfortunately, I cannot figure out how to get an indeterminate progress bar to animate while inside the table.

GPT-4 has been very "helpful" in scouring this website to provide me all kinds of examples that don't work.

I've constructed an example below. It draws a UI with an animating indeterminate progress bar. Every 5 seconds, it adds a bar to the table -- alternating between a new bar and the one animating at the top.

Nothing animates in the table. What's more, if I add the bar from the top to the table, it stops animating at the top as well.

What is going on?

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.UIManager;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumnModel;

@SuppressWarnings("serial")
public class TestSwing extends JPanel implements TableCellRenderer
{
    String[] header = {"Progress"};
    DefaultTableModel tableModel = new DefaultTableModel(null, header) {
        @Override
        public Class<?> getColumnClass(int col) {
            return getValueAt(0, col).getClass();
        }
    };
    JTable table = new JTable(tableModel);
    JProgressBar bar = new JProgressBar();

    public TestSwing()
    {
        bar.setIndeterminate(true);
        TableColumnModel columnModel = table.getColumnModel();
        columnModel.getColumn(0).setCellRenderer(this);
        
        setLayout(new BorderLayout());
        add(bar, BorderLayout.NORTH);
        
        JScrollPane scroll = new JScrollPane(table);
        scroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
        add(scroll, BorderLayout.CENTER);
    }
    
    @Override
    public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column)
    {
        return bar;
    }
    
    protected void update()
    {
        while (true) {
            try {
                Thread.sleep(5000);
            } catch (Exception e) {};
            EventQueue.invokeLater(() -> {
                tableModel.addRow(new Object[] {Boolean.TRUE});
            });
        }
    }

    public static void main(String[] args) throws Exception {
        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                JFrame f = new JFrame();
                f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                TestSwing list = new TestSwing();
                f.add(list);
                f.setSize(new Dimension(300, 500));
                f.setLocationRelativeTo(null);
                f.setVisible(true);
                
                Thread updater = new Thread(() -> list.update(), "List Updater");
                updater.setDaemon(true);
                updater.start();
            }
        });
    }
}

This question has appeared a few times on this site: I tried updating the table model as suggested by one accepted answer. I tried redrawing the table as suggested by another. I even tried having the TableCellRenderer return a different bar for indeterminate rows as the accepted answer of a third.

Finally, in constructing this toy problem, I realized that once added to a table, even a functioning indeterminate bar would stop animating.

If this works on your VM, do let me know what you're using. It's not working on any of mine.


Solution

  • The BasicProgressBarUI employed by the JProgressBar has a handler that will stop the animation timer every time the component changes place in the hierarchy. Since this is many layers of private classes and functions, and the hierarchy will be changed constantly by the TableCellRenderer, the easiest thing to do is override the behavior of the bar itself:

    JProgressBar bar = new JProgressBar() {
        @Override
        public boolean isDisplayable() {
            return table.isDisplayable() || super.isDisplayable();
        }
    };