Search code examples
javaswingswingworkerjprogressbartablecellrenderer

GUI not updating with current information even though the correct Events are firing


I am having a slight problem with a GUI where the ProgressCellRenderer is not updating until I either click on it or highlight it or even resize the window. I have slimmed down the code to just use generic data and using sleeps to emulate a long running task.

What have I missed that is causing it to not update until I cause an update on the gui itself?

This is minimum working code that will run.

 public class Test
    {

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

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

                    UpdatableTableModel model = new UpdatableTableModel();

                    JTable table = new JTable();
                    table.setModel(model);

                    table.getColumn("Status").setCellRenderer(new ProgressCellRender());

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

                    StringFinderWorker worker = new StringFinderWorker(model);
                    worker.execute();

                }
            });
        }

        public class ProgressCellRender 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)
            {
                int progress = 0;
                if (value instanceof Float)
                {
                    progress = Math.round(((Float) value) * 100f);
                }
                else if (value instanceof Integer)
                {
                    progress = (int) value;
                }
                setValue(progress);
                return this;
            }
        }

        public class RowData
        {
            private String string;
            private float status;

            public RowData(String string)
            {
                this.string = string;
                this.status = 0f;
            }

            public String getString()
            {
                return string;
            }

            public float getStatus()
            {
                return status;
            }

            public void setStatus(float status)
            {
                this.status = status;
            }
        }

        public class UpdatableTableModel extends AbstractTableModel
        {
            private static final long serialVersionUID = 1L;

            private List<RowData> rows;
            private Map<String, RowData> mapLookup;

            public UpdatableTableModel()
            {
                rows = new ArrayList<>(25);
                mapLookup = new HashMap<>(25);
            }

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

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

            @Override
            public String getColumnName(int column)
            {
                String name = "??";
                switch (column)
                {
                    case 0:
                        name = "The String";
                        break;
                    case 1:
                        name = "Status";
                        break;
                }
                return name;
            }

            @Override
            public Object getValueAt(int rowIndex, int columnIndex)
            {
                RowData rowData = rows.get(rowIndex);
                Object value = null;
                switch (columnIndex)
                {
                    case 0:
                        value = rowData.getString();
                        break;
                    case 1:
                        value = rowData.getStatus();
                        break;
                }
                return value;
            }

            @Override
            public void setValueAt(Object aValue, int rowIndex, int columnIndex)
            {
                RowData rowData = rows.get(rowIndex);
                switch (columnIndex)
                {
                    case 3:
                        if (aValue instanceof Float)
                        {
                            rowData.setStatus((float) aValue);
                        }
                        break;
                }
            }

            public void addString(String string)
            {
                RowData rowData = new RowData(string);
                mapLookup.put(string, rowData);
                rows.add(rowData);
                fireTableRowsInserted(rows.size() - 1, rows.size() - 1);
            }

            protected void updateStatus(String string, int progress)
            {
                RowData rowData = mapLookup.get(string);
                if (rowData != null)
                {
                    int row = rows.indexOf(rowData);
                    float p = (float) progress / 100f;
                    setValueAt(p, row, 3);
                    fireTableCellUpdated(row, 3);
                }
            }
        }

        public class StringFinderWorker extends SwingWorker<List<String>, String>
        {

            private UpdatableTableModel model;

            public StringFinderWorker(UpdatableTableModel model)
            {
                this.model = model;
            }

            @Override
            protected void process(List<String> chunks)
            {
                for (String string : chunks)
                {
                    model.addString(string);
                }
            }

            protected ArrayList<String> getStrings()
            {
                ArrayList<String> strings = new ArrayList<String>();
                for (int i = 0; i < 24; i++)
                {
                    strings.add("test: " + i);
                }
                return strings;
            }

            @Override
            protected List<String> doInBackground() throws Exception
            {
                List<String> strings = getStrings();

                for (int i = 0; i < strings.size(); i++)
                {
                    publish(strings.get(i));
                }
                return strings;
            }

            @Override
            protected void done()
            {
                try
                {
                    List<String> strings = get();
                    for (String string : strings)
                    {
                        new StringDownloaderWorker(model, string).execute();
                    }
                }
                catch (Exception exp)
                {
                    exp.printStackTrace();
                }
            }
        }

        public class StringDownloaderWorker extends SwingWorker<String, String>
        {

            private String currentString;
            private UpdatableTableModel model;

            public StringDownloaderWorker(UpdatableTableModel model, String string)
            {
                this.currentString = string;
                this.model = model;

                addPropertyChangeListener(new PropertyChangeListener()
                {
                    @Override
                    public void propertyChange(PropertyChangeEvent evt)
                    {
                        if (evt.getPropertyName().equals("progress"))
                        {
                            StringDownloaderWorker.this.model.updateStatus(currentString, (int) evt.getNewValue());
                        }
                    }
                });

            }

            @Override
            protected String doInBackground() throws Exception
            {
                for (int i = 0; i <= 100; i++)
                {
                    setProgress(i);
                    try
                    {
                        Thread.sleep(1000);
                    }
                    catch (InterruptedException ie)
                    {
                        //
                    }
                }

                return currentString;
            }
        }
    }

Solution

  • You're going to kick yourself when you see this...

    The column indexes that you are using to register the fact that the cell has changed are wrong...

    You are using setValueAt(p, row, 3);, when the progress column is actually 1, so you should actually be using setValueAt(p, row, 1);

    This also means that the switch statement in your setValueAt method will need to change.

    @Override
    public void setValueAt(Object aValue, int rowIndex, int columnIndex) {
        RowData rowData = rows.get(rowIndex);
        switch (columnIndex) {
            case 1:
                if (aValue instanceof Float) {
                    rowData.setStatus((float) aValue);
                    fireTableCellUpdated(rowIndex, columnIndex);
                }
                break;
        }
    }
    

    You will also note that I moved the fireTableCellUpdated call to this method, as you never know when setValueAt might be called.