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:
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.