JTable模型监听器过早检测插入的行(在绘制之前)

我有一个JTable ,可以有用户dynamic添加的行。 它位于一个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。 这个版本仍然不起作用,但是,如果我像从前那样将滚动逻辑从表模型监听器移动到button监听器,那么它的确行得通!

 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); } } 

此示例使用scrollRectToVisible()来(有条件地)滚动到最后一个单元格矩形。 作为一个function,你可以点击拇指暂停滚动和释放恢复。

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

附录: 在我的SSCCE中 尝试了 scrollRectToVisible ,它仍然performance出同样的问题。

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://stackoverflow.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 🙂

表格 – 以及其他任何模型的视图,不仅包括数据,还包括select,调整等 – 正在倾听模型以更新自身。 所以一个自定义监听器只是另一个监听器(服务序列未定义)。 如果它想做一些依赖于视图状态的东西,那么所有内部更新都准备好之后(在这个具体的上下文中,内部更新包括垂直scrollBar的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);