2

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



}

}
Daniele Testa
  • 1,538
  • 3
  • 16
  • 34
  • 2
    *"I have seen this question a couple of times, but the answers I found are a bit "bad" in my opinion."* Where specifically? Link to them. For better help sooner, post a [MCTaRE](http://stackoverflow.com/help/mcve) (Minimal Complete Tested and Readable Example). – Andrew Thompson Mar 20 '14 at 10:27
  • 1
    hard to believe that you never came across panel.scrollRectToVisible in your search ;-) – kleopatra Mar 20 '14 at 10:34
  • You mean that scrollRectToVisible will work even if I set it before the GUI had time to update the height? – Daniele Testa Mar 20 '14 at 10:36
  • Tip: Add @kleopatra (or whoever, the `@` is important) to *notify* someone of a new comment. – Andrew Thompson Mar 20 '14 at 10:48
  • @Andrew Thompson: Ah, thanks. I will keep that in mind. I assumed people that already commented will automatically be notified if there was additional comments added (like at Facebook and others). – Daniele Testa Mar 20 '14 at 10:53
  • wrap it into a invokeLater – kleopatra Mar 20 '14 at 10:53
  • @kleopatra Ok, but that was the thing I explained in my question. Isn't that a "bad" way of doing it? Just delaying execution a "unknown" amout of time sounds like a hack to me. – Daniele Testa Mar 20 '14 at 10:56
  • *"I assumed people that already commented will automatically be notified"* (Unfortunately) the SO rules for when people get notified are more complicated. We always get notified of any comment on any question or answer **we** make. If **only one** person has commented and we post a comment, they are automatically notified. If **two or more** people have made comments, they are not notified without the `@` notation. – Andrew Thompson Mar 20 '14 at 10:56
  • the _amount of time_ is irrelevant, what's relevant is its relation to other events, and that's it's not _unknown_ - it's guaranteed to happen _after_ all pending events, that it until all internally triggered state changes are processed (hint: the api doc is your friend :) – kleopatra Mar 20 '14 at 10:58
  • feels like all of you are kind of deaf, so trying to shout **DO USE API THAT'S AVAILABLE** – kleopatra Mar 20 '14 at 12:20
  • @Daniele Testa 1. here are a few code examples in SSCCE/MCVE/MCTRE form - how to implements scrollable to JPanel (e.i.), 2. (wild shot into dark, I'm bet that this is your real issue) you have to call JPanel.revalidate & JPanel.repaint in the case that JComponents are added to the already visible Swing GUI, otherwise JPanel never returns proper size and JScrollPane don't know this derived size too – mKorbel Mar 20 '14 at 12:22

1 Answers1

5

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.

kleopatra
  • 51,061
  • 28
  • 99
  • 211
  • +1 (flamewar) but whats happends in the case that there isn't empty space for new JComponent, (explanation to my flamewar) still sounds me that JComponent is added off screen without revalidate & repaint, then scrollRectToVisible can see last accepted JComponent by used LayoutManager, because max value for JScrollBar can do that too, same/similair way (I'm hope that are used correct terms) – mKorbel Mar 20 '14 at 13:14
  • @mKorbel If you have a question, please post a ... [question](http://stackoverflow.com/questions/ask) :-) Seriously, no idea which problem you expect, whatever it might be, looks unrelated to the question here – kleopatra Mar 20 '14 at 13:29
  • (copy - paste from OPs question) `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.` - 1. OPs code doesn't corresponding with this (allmost) sentence(s), 2. scrollRectToVisible and scrollBar.getMaximum() returns the same int value (for JComponents laid in one single column), btw not explained too – mKorbel Mar 20 '14 at 13:47
  • looks like as whatever [(have to check OPs)](http://stackoverflow.com/questions/22513032/how-to-force-refresh-repaint-a-jscrollpane) – mKorbel Mar 20 '14 at 13:51
  • @kleopatra Btw, do you have a good way to check if the scrollpane is already on the bottom? – Daniele Testa Mar 20 '14 at 15:44
  • @DanieleTesta curious - why would you want to know? – kleopatra Mar 20 '14 at 16:16
  • @kleopatra Because I only want the scrollpane to scroll if it was already at the bottom before the component was added. Else it should just stay where it is. This is common practise in most cases. – Daniele Testa Mar 20 '14 at 17:58