【问题标题】:JTable model listener detects inserted rows too soon (before they are drawn)JTable 模型侦听器过早检测到插入的行(在绘制之前)
【发布时间】:2013-01-03 22:52:22
【问题描述】:

我有一个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);
    }
}

编辑:根据trashgod 的要求更新了下面的SSCCE。这个版本仍然不起作用,但是,如果我像他一样将滚动逻辑从表格模型侦听器移动到按钮侦听器,那么它确实有效!

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

【问题讨论】:

    标签: java swing user-interface jtable listener


    【解决方案1】:

    这个example 使用scrollRectToVisible() (有条件地)滚动到最后一个单元格矩形。作为一项功能,您可以单击拇指暂停滚动并释放以恢复。

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

    附录:我在我的 SSCCE 中尝试过scrollRectToVisible ,但仍然出现同样的问题。

    这个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();
        }
    }
    

    【讨论】:

    • 谢谢,不过好像不能直接解决我的问题,不知道是不是不可能?在您的示例中,您不断地每秒检查一次是否应该滚动到最后。但是,我希望在单击“添加行”后立即发生这种情况。如果我在您的应用程序中单击一堆按钮,它会遇到与我相同的问题(但在不到 1 秒的时间内“修复”自身)。用户单击按钮后是否不可能发生滚动,因为我猜模型和 GUI 位于不同的线程上?
    • 我应该补充一点,我在我的 SSCCE 中尝试了scrollRectToVisible,但它仍然出现同样的问题,这可能比我上面写的更相关。
    • 我已经在上面详细说明了;请用您当前的代码更新您的问题。
    • 感谢您的详细说明。这种技术(将滚动逻辑添加到按钮侦听器而不是表模型侦听器)似乎确实有效。这不是我想要的代码结构,但我想我可以适应。我用我正在尝试的第二个版本的代码更新了我的 OP,它仍然没有工作(仍然试图完成滚动以响应表模型插入事件)。我仍然很好奇为什么这不起作用。我现在最好的猜测是模型监听器触发得太早(一旦插入事件开始),而将该逻辑移动到按钮监听器......
    • 我已经更新了示例以便于测试。排序事件的一种方法是将滚动操作包装在 EventQueue.invokeLater() 中。
    【解决方案2】:

    但是,这种检测似乎“为时过早”

    为了强调(@trashgod 已经在评论中提到它):这是真的 - 并且是 Swing 中一个相当普遍的问题 :-)

    表格 - 以及任何其他具有任何模型的视图,不仅是数据,还有选择、调整…… - 正在监听模型以更新自身。因此,自定义侦听器只是该行中的另一个侦听器(服务序列未定义)。如果它想要做一些依赖于视图状态的事情,它必须确保在所有内部更新都准备好之后这样做(在这个具体的上下文中,内部更新包括调整模型的更新)垂直滚动条)推迟自定义处理直到内部完成,通过包装 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);
    

    【讨论】:

    • 感谢您的回复。我认为它可能帮助我理解了@trashgod 说它时我没有得到的东西。他提到像你一样将我的滚动调用包装在invokeLater 中,我认为这不会完成任何事情,因为我的整个程序已经在这样的包装器中。我现在注意到包装器创建了一个 new Runnable,我猜这就是实现他所说的“排序”的原因,保证这个新的可运行文件直到它后面的行才会运行另一个可运行文件已完成。这完全正确吗?
    猜你喜欢
    • 2016-10-30
    • 1970-01-01
    • 2012-09-04
    • 2013-12-25
    • 1970-01-01
    • 2012-10-30
    • 1970-01-01
    • 2014-09-14
    • 1970-01-01
    相关资源
    最近更新 更多