Почему обновление представления JTable блокирует весь графический интерфейс?

У меня есть простой JTable, отображающий много данных (~ 1000 строк) со сложным настраиваемым средством визуализации ячеек. Для рендеринга этих ячеек требуется немного времени. В это время весь графический интерфейс кажется замороженным. Это видно, потому что пользователю показывается индикатор прогресса. Во время генерации данных работает нормально. Но если JTable запускается через fireTableDataChanged для обновления своего представления, индикатор выполнения замораживается.

В следующем небольшом примере вы должны нажать на кнопку и увидеть эффект после генерации данных. Статические блоки ожидания (до Thread.sleep()) представляют собой сложный код реального мира для генерации данных и генерации представления.

Теперь мой вопрос, где моя вина. Потому что я не верю, что нет возможности решить эту проблему.

Пояснение:

  • В моем приложении каждая ячейка отображает данные в две строки с разными цветами и значками. Чтобы показать простой пример, я добавил сон только для того, чтобы показать, что для создания и отображения всех этих ячеек требуется много времени. Обратите внимание, что мое приложение показывает около 1000 или, может быть, 2000 из этих двух строковых ячеек с разными цветами и значками. Это требует много времени для рендеринга. И из-за того, что этот простой пример не может показать эту сложность, я добавил сон (который должен отражать сложную работу).

  • Вы спросили: «Почему так долго?». Как я объяснял ранее, существуют тысячи ячеек, и они вызывают действие для каждого отдельного объекта в каждой ячейке и настройках отображения. На самом деле это может быть не так долго, но чтобы показать проблему, я установил на этот раз конкретные 42 мс.

  • Я знаю, что графический интерфейс зависает, когда я блокирую EDT. Но я не знаю, что я блокирую EDT в любом месте в моей программе. Операторы сна используются для представления большого количества строк кода, которые либо генерируют данные, либо настраивают каждую ячейку.

  • "... нет причин что-то создавать, ни JComponents, как JLabel...". Мне нужна многострочная ячейка с возможностью окрашивания каждой строки и добавления значка к каждой строке. Лучший способ, который я нашел, - это два JLabels на JPanel в качестве ячейки. Если есть более простой способ, буду рад услышать. Я уже пробовал JList с многострочным HTML. Проблема в том, что он имеет тенденцию отображаться неправильно, если есть много разных строк.

  • "... Какова ваша цель..." - это должно работать просто. Можно ли изменить этот пример, чтобы отображать «работающий» неопределенный индикатор выполнения до тех пор, пока таблица не будет полностью обновлена ​​без удаления операторов Thread.sleep()?


package example;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.Box;
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.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;

public class Example extends DefaultTableModel {

    static List<String> data = new ArrayList<>();

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

    @Override
    public Object getValueAt(int row, int column) {
        return data.get(row);
    }

    @Override
    public String getColumnName(int column) {
        return "Column 0";
    }

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

    @Override
    public boolean isCellEditable(int row, int column) {
        return false;
    }

    public void updateView() {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                fireTableDataChanged();
            }
        });
    }

    static class CustomCellRenderer extends JPanel implements TableCellRenderer {

        private JLabel line1, line2;

        public CustomCellRenderer() {
            super();
            setOpaque(true);
            setLayout(new BorderLayout());

            this.line1 = new JLabel();
            this.line1.setOpaque(false);
            add(this.line1, BorderLayout.CENTER);
            this.line2 = new JLabel();
            this.line2.setOpaque(false);
            add(this.line2, BorderLayout.SOUTH);
        }

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, 
                boolean isSelected, boolean hasFocus, int row, int column) {
            if (isSelected) {
                setForeground(table.getSelectionForeground());
                this.line1.setForeground(table.getSelectionForeground());
                this.line2.setForeground(table.getSelectionForeground());
                setBackground(table.getSelectionBackground());
            } else {
                setForeground(table.getForeground());
                this.line1.setForeground(table.getForeground());
                this.line2.setForeground(table.getForeground());
                setBackground(table.getBackground());
            }

            // This wait represents other complex layout preparations
            // for this cell
            try {
                Thread.sleep(42);
            } catch (Exception e) {
            }

            line1.setText("Value: " + value);
            line2.setText("isSelected? " + isSelected + ", hasFocus?" + hasFocus);
            return this;
        }

    }

    static JPanel overlay;

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                JFrame frame = new JFrame("Example");
                frame.setLayout(new BorderLayout(4, 4));

                // Add JTable
                final Example model = new Example();
                JTable table = new JTable(model) {

                    @Override
                    public void doLayout() {
                        TableColumn col = getColumnModel().getColumn(0);
                        for (int row = 0; row < getRowCount(); row++) {
                            Component c = prepareRenderer(col.getCellRenderer(), row, 0);
                            setRowHeight(row, c.getPreferredSize().height);
                        }
                        super.doLayout();
                    }
                };
                TableColumn col = table.getColumnModel().getColumn(0);
                col.setCellRenderer(new CustomCellRenderer());
                frame.add(new JScrollPane(table), BorderLayout.CENTER);

                // Add button
                Box hBox = Box.createHorizontalBox();
                hBox.add(new JButton(new AbstractAction("Load data") {

                    @Override
                    public void actionPerformed(ActionEvent e) {
                        new Thread(new Runnable() {

                            @Override
                            public void run() {
                                overlay.setVisible(true);
                                data.clear();

                                System.out.println("Generating data ...");

                                for (int i = 0; i < 42; i++) {
                                    data.add("String no. " + (i + 1));
                                    try {
                                        Thread.sleep(42);
                                    } catch (Exception e) {
                                    }
                                }

                                System.out.println("Updating view ...");

                                model.updateView();
                                overlay.setVisible(false);

                                System.out.println("Finished.");
                            }
                        }).start();
                    }
                }));
                hBox.add(Box.createHorizontalGlue());
                frame.add(hBox, BorderLayout.NORTH);

                // Create loading overlay
                overlay = new JPanel(new FlowLayout(FlowLayout.CENTER)) {

                    @Override
                    protected void paintComponent(Graphics g) {
                        g.setColor(new Color(0, 0, 0, 125));
                        g.fillRect(0, 0, getWidth(), getHeight());
                        super.paintComponent(g);
                    }
                };
                overlay.setOpaque(false);
                overlay.setBackground(new Color(0, 0, 0, 125));
                JProgressBar bar = new JProgressBar();
                bar.setIndeterminate(true);
                overlay.add(bar);

                frame.setGlassPane(overlay);
                frame.getGlassPane().setVisible(false);

                // Create frame
                frame.setSize(600, 400);
                frame.setVisible(true);
            }
        });
    }

}

person mythbu    schedule 09.06.2014    source источник
comment
вы должны выполнять длительные запросы в отдельном контексте. объекты, которые не синхронизировались, блокируя потоки, которые не смогли обработать события, поступающие в окно, имеющее запутанный графический интерфейс с неисправимыми ошибками, которые вы не смогли воспроизвести.   -  person Roman C    schedule 09.06.2014
comment
// This wait represents other complex layout preparations.. for this cell 42 мс на ячейку? Почему это занимает так много времени? Тем не менее, поскольку это только видимые ячейки, их не должно быть много. Но в целом: не блокируйте EDT (поток отправки событий) — когда это произойдет, графический интерфейс «зависнет». Вместо вызова Thread.sleep(n) реализуйте Swing Timer для повторяющихся задач или a SwingWorker для длительных задач. См. Параллелизм в Swing для получения дополнительной информации.   -  person Andrew Thompson    schedule 09.06.2014
comment
FireTableDataChanged(); реализован в DefaultTableModel и корректно,   -  person mKorbel    schedule 09.06.2014
comment
Thread.sleep(42); должен быть удален или удален внутри Renderer, весь Renderer выглядит так, как будто он делает неправильную работу   -  person mKorbel    schedule 09.06.2014
comment
приведение JLabel из общедоступного компонента getTableCellRendererComponent (возвращает Component, JComponent, JLabel по умолчанию), нет причин создавать что-либо или JComponents, поскольку JLabel   -  person mKorbel    schedule 09.06.2014
comment
все из main(String[] args) { должно быть перемещено в конструктор, super.doLayout(); должен быть внутри рендерера, собрать все части вместе, важный вопрос: какова ваша цель,   -  person mKorbel    schedule 09.06.2014
comment
Я добавил некоторые детали, чтобы объяснить.   -  person mythbu    schedule 09.06.2014
comment
используйте JLabel (посредством приведения из общедоступного компонента getTableCellRendererComponent), добавьте LayoutManager (GridLayout), по-прежнему не используйте doLayout, используйте высоту строки для JTable в случае, если это значение одинаково для всех строк, эти два пункта указывают на сохранение важного количество времени (рендерер инициализируется от всех событий мыши и клавиш и от модели к событиям просмотра, реализованным в API)   -  person mKorbel    schedule 09.06.2014
comment
Вы загружаете свои значки в средство визуализации ячеек, метод getTableCellRendererComponent?   -  person MadProgrammer    schedule 09.06.2014
comment
Возможный дубликат. См. также этот ответ.   -  person trashgod    schedule 09.06.2014