JScrollPane 和自动滚动:令人不安的滚动行为

JScrollPane and autoscroll: Disturbing scrolling behavior

我正在编写一个使用 JScrollPane 的应用程序。在此 JScrollPane 中,我想自动显示搜索结果,这意味着我必须在 JScrollPane 中动态添加和删除结果。结果实现为 JTextArea,它嵌入在 GridBagLayout 中。

当有大量搜索结果时,JScrollPane 会自动滚动到底部(应该在顶部)。我已经用我在这里找到的解决方案解决了它。这里的问题是,你可以看到,它是如何滚动回到顶部的。是否可以消除此行为?

我发现了以下内容:

最好是禁用自动滚动。我创建了一个可执行示例来演示此行为。单击按钮 "Add Row" 时,将添加 500 行。多点几次就很清楚了

非常感谢您的帮助!

import java.awt.GridBagConstraints;
import javax.swing.JTextArea;

public class ScrollPaneTest extends javax.swing.JFrame {
    private javax.swing.JButton jButton1;
    private javax.swing.JPanel jPanel1;
    private javax.swing.JPanel jPanel2;
    private javax.swing.JScrollPane jScrollPane1;


    /**
     * Creates new form ScrollPaneTest
     */
    public ScrollPaneTest() {
        initComponents();
    }

    /**
     * Adds a new row.
     * @param index The index of the new row.
     */
    private void addRow(int index) {
        JTextArea row = new JTextArea("Area " + index);
        row.setEditable(false);
        row.setBorder(null);

        GridBagConstraints gridBagConstraints = new GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = index;
        gridBagConstraints.fill = GridBagConstraints.BOTH;
        gridBagConstraints.weightx = 1.0;
        gridBagConstraints.insets = new java.awt.Insets(2, 0, 2, 0);
        jPanel2.add(row, gridBagConstraints);
    }


    /**
     * Initializes the components.
     */
    private void initComponents() {

        jScrollPane1 = new javax.swing.JScrollPane();
        jPanel1 = new javax.swing.JPanel();
        jPanel2 = new javax.swing.JPanel();
        jButton1 = new javax.swing.JButton();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

        jScrollPane1.setHorizontalScrollBarPolicy(javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        jScrollPane1.setVerticalScrollBarPolicy(javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
        jScrollPane1.setPreferredSize(new java.awt.Dimension(400, 400));

        jScrollPane1.setViewportView(jPanel1);

        jPanel1.setLayout(new java.awt.BorderLayout());

        jPanel2.setLayout(new java.awt.GridBagLayout());
        jPanel1.add(jPanel2, java.awt.BorderLayout.NORTH);

        jScrollPane1.setViewportView(jPanel1);

        jButton1.setText("Create Rows");
        jButton1.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                jButton1ActionPerformed(evt);
            }
        });

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
            .addGroup(layout.createSequentialGroup()
                .addGap(148, 148, 148)
                .addComponent(jButton1)
                .addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 245, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                .addComponent(jButton1)
                .addGap(0, 21, Short.MAX_VALUE))
        );

        pack();
    }                   

    /**
     * Creates 500 new rows.
     * @param evt
     */
    private void jButton1ActionPerformed(java.awt.event.ActionEvent evt) { 
        jPanel2.removeAll();

        for(int i = 0; i < 1000; i++) {
            addRow(i);
        }

         javax.swing.SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                jScrollPane1.getVerticalScrollBar().setValue(0);
            }

         });
         jPanel2.validate();
         jPanel2.repaint();
    }                                        

    public static void main(String args[]) {
        java.awt.EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() { 
                new ScrollPaneTest().setVisible(true);
            }
        });
    }             
}

更新 1

我删除了 lambda 表达式。希望它现在也可以用 < Java 8.

编译

更新 2

令人不安的滚动行为问题已通过替换

解决
jPanel2.validate();
jPanel2.repaint();

jScrollPane1.validate();
jScrollPane1.repaint();

尽管如此,这个问题的两个答案在其他一些情况下都可能非常有用,应该引起注意。

实现此目的的一种方法是为您的滚动窗格自定义 JViewPort。此自定义视口覆盖 setViewPosition 并使用标志来防止滚动或不滚动。

下面是这样的代码示例,在更改文本区域的内容之前,我们 "lock" 视口以防止滚动,稍后我们将其解锁:

import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JViewport;
import javax.swing.SwingUtilities;
import javax.swing.Timer;

public class TestNoScrolling {

    private int lineCount = 0;
    private LockableViewPort viewport;
    private JTextArea ta;

    private final class LockableViewPort extends JViewport {

        private boolean locked = false;

        @Override
        public void setViewPosition(Point p) {
            if (locked) {
                return;
            }
            super.setViewPosition(p);
        }

        public boolean isLocked() {
            return locked;
        }

        public void setLocked(boolean locked) {
            this.locked = locked;
        }
    }

    protected void initUI() {
        JFrame frame = new JFrame("test");
        ta = new JTextArea(5, 30);
        JScrollPane scrollpane = new JScrollPane();
        viewport = new LockableViewPort();
        viewport.setView(ta);
        scrollpane.setViewport(viewport);
        frame.add(scrollpane);
        frame.pack();
        frame.setVisible(true);
        Timer t = new Timer(1000, new ActionListener() {

            @Override
            public void actionPerformed(ActionEvent e) {
                viewport.setLocked(true);
                ta.append("Some new line " + lineCount++ + "\n");
                SwingUtilities.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        viewport.setLocked(false);
                    }
                });
            }
        });
        t.setRepeats(true);
        t.start();
    }

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

            @Override
            public void run() {
                new TestNoScrolling().initUI();
            }
        });
    }
}

您可以简单地将Caret位置设置为起始位置(0),例如...

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

public class ScrollNoMore {

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

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

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        private JTextArea ta;
        private JScrollPane sp;
        private Random rnd = new Random();

        private boolean initalised = false;

        public TestPane() {
            setLayout(new BorderLayout());
            ta = new JTextArea(20, 40);
            sp = new JScrollPane(ta);
            add(sp);
            Timer timer = new Timer(250, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    long value = rnd.nextLong();
                    ta.append(String.valueOf(value) + "\n");
                    if (!initalised) {
                        ta.setCaretPosition(0);
                        initalised = true;
                    }
                }
            });
            timer.start();
        }

    }

}

这只会在 Timer 第一次运行时设置它,这意味着如果您出于某种原因移动 Caret 或滚动位置,它不会 "flick" 返回.您可以设置一系列状态,如果用户移动当前视图,它不会影响滚动,但可以根据需要重置。