Слушатель модели JTable обнаруживает вставленные строки слишком рано (до их отрисовки)

У меня есть JTable которые могут иметь строки, динамически добавляемые пользователем. Он сидит в JScrollPane, так как количество строк становится достаточно большим, скроллер становится активным. Мое желание состоит в том, чтобы, когда пользователь добавляет новую строку, скроллер перемещался полностью вниз, так что новая строка видна в области прокрутки. В настоящее время (SSCCE ниже) я пытаюсь использовать слушатель модели таблицы, чтобы определить, когда вставлена ​​строка, и полностью переместить полосу прокрутки вниз, когда будет выполнено обнаружение. Тем не менее, кажется, что это обнаружение "слишком рано", так как модель обновилась, но новая строка на самом деле еще не нарисована, поэтому происходит то, что скроллер перемещается полностью до самого дна непосредственно перед вставкой новой строки, и затем новая строка вставляется чуть ниже конца панели (вне видимости).

Очевидно, что этот подход как-то не так. Какой правильный подход?

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.DefaultTableModel;

public class TableListenerTest {

    private JFrame frame;
    private JScrollPane scrollPane;
    private JTable table;
    private DefaultTableModel tableModel;

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            public void run() {
                try {
                    TableListenerTest window = new TableListenerTest();
                    window.frame.setVisible(true);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    public TableListenerTest() {
        initialize();
    }

    private void initialize() {
        frame = new JFrame();
        frame.setBounds(100, 100, 450, 200);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().setLayout(new BoxLayout(frame.getContentPane(), BoxLayout.Y_AXIS));

        JSplitPane splitPane = new JSplitPane();
        frame.getContentPane().add(splitPane);

        scrollPane = new JScrollPane();
        scrollPane.setPreferredSize(new Dimension(100, 2));
        splitPane.setLeftComponent(scrollPane);

        tableModel = new DefaultTableModel(new Object[]{"Stuff"},0);
        table = new JTable(tableModel);
        scrollPane.setViewportView(table);
        table.getModel().addTableModelListener(new TableModelListener() {
            public void tableChanged(TableModelEvent e) {
                if (e.getType() == TableModelEvent.INSERT) {
                    JScrollBar scrollBar = scrollPane.getVerticalScrollBar();
                    scrollBar.setValue(scrollBar.getMaximum());
                }
            }
        });

        JButton btnAddRow = new JButton("Add Row");
        btnAddRow.addMouseListener(new MouseAdapter() {
            @Override
            public void mouseClicked(MouseEvent e) {
                tableModel.addRow(new Object[]{"new row"});
            }
        });
        splitPane.setRightComponent(btnAddRow);
    }
}

РЕДАКТИРОВАТЬ: Обновлен SSCCE ниже на основе запроса trashgod. Эта версия по-прежнему не работает, однако, если я переместил логику прокрутки от слушателя модели таблицы к слушателю кнопки, как он это сделал, то это работает!

import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Rectangle;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JTable;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.DefaultTableModel;

    public class TableListenerTest {

        private JFrame frame;
        private JScrollPane scrollPane;
        private JTable table;
        private DefaultTableModel tableModel;

        public static void main(String[] args) {
            EventQueue.invokeLater(new Runnable() {
                public void run() {
                    try {
                        TableListenerTest window = new TableListenerTest();
                        window.frame.setVisible(true);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            });
        }

        public TableListenerTest() {
            initialize();
        }

        private void initialize() {
            frame = new JFrame();
            frame.setBounds(100, 100, 450, 200);
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.getContentPane().setLayout(new BoxLayout(frame.getContentPane(), BoxLayout.Y_AXIS));

            JSplitPane splitPane = new JSplitPane();
            frame.getContentPane().add(splitPane);

            scrollPane = new JScrollPane();
            scrollPane.setPreferredSize(new Dimension(100, 2));
            splitPane.setLeftComponent(scrollPane);

            tableModel = new DefaultTableModel(new Object[]{"Stuff"},0);
            table = new JTable(tableModel);
            scrollPane.setViewportView(table);
            table.getModel().addTableModelListener(new TableModelListener() {
                public void tableChanged(TableModelEvent e) {
                    if (e.getType() == TableModelEvent.INSERT) {
                        int last = table.getModel().getRowCount() - 1;
                        Rectangle r = table.getCellRect(last, 0, true);
                        table.scrollRectToVisible(r);
                    }
                }
            });

            JButton btnAddRow = new JButton("Add Row");
            btnAddRow.addMouseListener(new MouseAdapter() {
                @Override
                public void mouseClicked(MouseEvent e) {
                    tableModel.addRow(new Object[]{"new row"});
                }
            });
            splitPane.setRightComponent(btnAddRow);
        }
    }

2 ответа

Решение

Этот пример использует scrollRectToVisible() для (условно) прокрутки до последнего прямоугольника ячейки. В качестве функции вы можете нажать на большой палец, чтобы приостановить прокрутку и отпустите, чтобы возобновить.

private void scrollToLast() {
    if (isAutoScroll) {
        int last = table.getModel().getRowCount() - 1;
        Rectangle r = table.getCellRect(last, 0, true);
        table.scrollRectToVisible(r);
    }
}

Приложение: я пытался scrollRectToVisible в моем SSCCE, и это все еще показывает ту же проблему.

это Action обеспечивает управление мышью и клавиатурой:

JButton btnAddRow = new JButton(new AbstractAction("Add Row") {

    @Override
    public void actionPerformed(ActionEvent e) {
        tableModel.addRow(new Object[]{"new row"});
        int last = table.getModel().getRowCount() - 1;
        Rectangle r = table.getCellRect(last, 0, true);
        table.scrollRectToVisible(r);
    }
});

Приложение: Вот вариант вашего примера, который иллюстрирует пересмотренную стратегию компоновки.

/** @see https://stackru.com/a/14429388/230513 */
public class TableListenerTest {

    private static final int N = 8;
    private JFrame frame;
    private JScrollPane scrollPane;
    private JTable table;
    private DefaultTableModel tableModel;

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

            @Override
            public void run() {
                TableListenerTest window = new TableListenerTest();
                window.frame.setVisible(true);
            }
        });
    }

    public TableListenerTest() {
        initialize();
    }

    private void initialize() {
        frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLayout(new BoxLayout(frame.getContentPane(), BoxLayout.X_AXIS));
        tableModel = new DefaultTableModel(new Object[]{"Stuff"}, 0);
        for (int i = 0; i < N; i++) {
            tableModel.addRow(new Object[]{"new row"});
        }
        table = new JTable(tableModel) {

            @Override
            public Dimension getPreferredScrollableViewportSize() {
                return new Dimension(200, table.getRowHeight() * N);
            }
        };
        scrollPane = new JScrollPane();
        scrollPane.setViewportView(table);
        JButton btnAddRow = new JButton(new AbstractAction("Add Row") {

            @Override
            public void actionPerformed(ActionEvent e) {
                tableModel.addRow(new Object[]{"new row"});
                int last = table.getModel().getRowCount() - 1;
                Rectangle r = table.getCellRect(last, 0, true);
                table.scrollRectToVisible(r);
            }
        });
        frame.add(scrollPane);
        frame.add(btnAddRow);
        frame.pack();
    }
}

Тем не менее, кажется, что это обнаружение "слишком рано"

Для акцента (@trashgod уже упоминал об этом в комментарии): это правда - и ожидается, и довольно общая проблема в Swing:-)

Таблица - и любое другое представление с любой моделью, не только с данными, но и с выбором, настройкой, ... - прослушивает модель для обновления. Таким образом, пользовательский слушатель - это просто еще один слушатель в очереди (с неопределенной последовательностью обслуживания). Если он хочет сделать что-то, зависящее от состояния представления, он должен сделать это после того, как все внутреннее обновление будет готово (в этом конкретном контексте внутреннее обновление включает в себя обновление AdjustmentModel вертикальной полосы прокрутки) Откладывание пользовательской обработки до тех пор, пока внутреннее устройство не будет выполнено, достигается с помощью SwingUtilities.invokeLater:

TableModelListener l = new TableModelListener() {
    @Override
    public void tableChanged(TableModelEvent e) {
        if (e.getType() == TableModelEvent.INSERT) {
            invokeScroll();
        }
    }

    protected void invokeScroll() {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                int last = table.getModel().getRowCount() - 1;
                Rectangle r = table.getCellRect(last, 0, true);
                table.scrollRectToVisible(r);
            }
        });
    }
};
table.getModel().addTableModelListener(l);
Другие вопросы по тегам