What is the correct way to make a JScrollPane scroll to the bottom after a component has been inserted?

StackOverflow https://stackoverflow.com/questions/22529802

Вопрос

I have seen this question a couple of times, but the answers I found are a bit "bad" in my opinion.

So, basically I have a JScrollPane that I insert components to. Each time I insert a component, I want the JScrollPane to scroll to the bottom. Simple enough.

Now, the logical thing to do would be to add a listener (componentAdded) to the container that I am inserting to.

That listener would then simply scroll to the bottom. However, this will not work, as the component height has not been finished calculating at this time, thus the scrolling fails.

The answers I have seen to this usually involves putting the scroll-row in one (or even several chained) "invokeLater" threads.

This seems to me like an "ugly hack". Surely there should be a better way to actually move the scroll once all the height calculations are done, instead of just "delaying" the scroll for a unknown amount of time?

I also read some answers that you should work with the SwingWorker, which I never really understood. Please enlighten me :)

Here is some code for you to modify (read "make work"):

JScrollPane scrollPane = new JScrollPane();
add(scrollPane);

JPanel container = new JPanel();
scrollPane.setViewportView(container);

container.addContainerListener(new ContainerAdapter() {
    public void componentAdded(ContainerEvent e) {
        JScrollPane scrollPane = (JScrollPane) value.getParent().getParent();
        JScrollBar scrollBar = scrollPane.getVerticalScrollBar();
        scrollBar.setValue(scrollBar.getMaximum());
    }
});

JPanel hugePanel = new JPanel();
hugePanel.setPreferredSize(new Dimension(10000, 10000);
container.add(hugePanel);

UPDATE: Added some code to test the theory. However, it seems to work fine, so I guess I have a problem somewhere else in my program :)

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ContainerAdapter;
import java.awt.event.ContainerEvent;

import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.LineBorder;


public class ScrollTest extends JFrame {

private static final long serialVersionUID = -8538440132657016395L;

public static void main(String[] args) {

    SwingUtilities.invokeLater(new Runnable() {
        public void run() {
            new ScrollTest().setVisible(true);
        }
    });

}

public ScrollTest() {

    UIManager.put("swing.boldMetal", Boolean.FALSE);

    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setTitle("Scroll Test");
    setSize(1000, 720);
    setLocationRelativeTo(null);

    JPanel container = new JPanel();
    container.setLayout(new BoxLayout(container, BoxLayout.X_AXIS));
    add(container);

    //Create 3 scollpanels
    final JScrollPane scrollPane1 = new JScrollPane();
    scrollPane1.setBorder(new LineBorder(Color.RED, 2));
    container.add(scrollPane1);

    final JScrollPane scrollPane2 = new JScrollPane();
    scrollPane2.setBorder(new LineBorder(Color.GREEN, 2));
    container.add(scrollPane2);

    final JScrollPane scrollPane3 = new JScrollPane();
    scrollPane3.setBorder(new LineBorder(Color.BLUE, 2));
    container.add(scrollPane3);

    //Create a jpanel inside each scrollpanel
    JPanel wrapper1 = new JPanel();
    wrapper1.setLayout(new BoxLayout(wrapper1, BoxLayout.Y_AXIS));
    scrollPane1.setViewportView(wrapper1);
    wrapper1.addContainerListener(new ContainerAdapter() {
        public void componentAdded(ContainerEvent e) {

            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    scrollPane1.getVerticalScrollBar().setValue(scrollPane1.getVerticalScrollBar().getMaximum());
                }
            });

        }
    });        

    JPanel wrapper2 = new JPanel();
    wrapper2.setLayout(new BoxLayout(wrapper2, BoxLayout.Y_AXIS));
    scrollPane2.setViewportView(wrapper2);
    wrapper2.addContainerListener(new ContainerAdapter() {
        public void componentAdded(ContainerEvent e) {

            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    scrollPane2.getVerticalScrollBar().setValue(scrollPane2.getVerticalScrollBar().getMaximum());
                }
            });

        }
    });        

    JPanel wrapper3 = new JPanel();
    wrapper3.setLayout(new BoxLayout(wrapper3, BoxLayout.Y_AXIS));
    scrollPane3.setViewportView(wrapper3);
    wrapper3.addContainerListener(new ContainerAdapter() {
        public void componentAdded(ContainerEvent e) {

            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    scrollPane3.getVerticalScrollBar().setValue(scrollPane3.getVerticalScrollBar().getMaximum());
                }
            });

        }
    });        

    //Add come stuff into each wrapper
    JPanel junk;
    for(int x = 1; x <= 1000; x++) {
        junk = new JPanel();
        junk.setBorder(new LineBorder(Color.BLACK, 2));
        junk.setPreferredSize(new Dimension(100, 40));
        junk.setMaximumSize(junk.getPreferredSize());
        wrapper1.add(junk);
    }

    for(int x = 1; x <= 1000; x++) {
        junk = new JPanel();
        junk.setBorder(new LineBorder(Color.BLACK, 2));
        junk.setPreferredSize(new Dimension(100, 40));
        junk.setMaximumSize(junk.getPreferredSize());
        wrapper2.add(junk);
    }

    for(int x = 1; x <= 1000; x++) {
        junk = new JPanel();
        junk.setBorder(new LineBorder(Color.BLACK, 2));
        junk.setPreferredSize(new Dimension(100, 40));
        junk.setMaximumSize(junk.getPreferredSize());
        wrapper3.add(junk);
    }



}

}
Это было полезно?

Решение

The correct - that is without wasting time in re-inventing the wheel - way to scroll a component anywhere you want it is something like:

wrapper1.addContainerListener(new ContainerAdapter() {
    @Override
    public void componentAdded(final ContainerEvent e) {

        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                JComponent comp = (JComponent) e.getChild();
                Rectangle bounds = new Rectangle(comp.getBounds());
                comp.scrollRectToVisible(bounds);
            }
        });

    }
});

The smells you are avoiding:

  • no reference to any enclosing parent (like the scrollPane)
  • no digging into the internals of any parent (like the scrollBar)
  • no need for a custom model

As to the invokeLater, citing its api doc (bolding added by me):

Causes doRun.run() to be executed asynchronously on the AWT event dispatching thread. This will happen after all pending AWT events have been processed

So you can be fairly certain that the internals are handled before your code is executed.

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top