11

I have two JScrollPanes in the same window. The one on the left is large enough to display the contents of the contained panel. The one on the right is not large enough to display its contents and thus it needs to create a vertical scrollbar.

JScrollPane Issue

But as you can see, the problem is that when the vertical scrollbar appears, the scrollbar appears on the inside of the JScrollPane. It covers up content contained inside and thus a horizontal scrollbar is necessary to show everything. I want that fixed.

I realize that I can turn the vertical scrollbar on all the time, but for aesthetic reasons, I only want for it to appear when necessary, without making it necessary for a horizontal scrollpane to appear.

EDIT: My code for starting this is as simple as can be:

JScrollPane groupPanelScroller = new JScrollPane(groupPanel);
this.add(groupPanelScroller, "align center");

I am using MigLayout (MigLayout.com), but this problem seems to appear no matter what layout manager I am using. Also, if I shrink the window so that the left panel is no longer large enough to display everything, the same behavior as the right panel occurs.

Thunderforge
  • 19,637
  • 18
  • 83
  • 130
  • I tried calling setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)), but it just made a really big border around the contents that did nothing to prevent the horizontal scrollbar from needing to appear. – Thunderforge Jul 20 '12 at 21:57
  • One thing you have got to find out is how you want it to behave when vertical scrollbar appears. Either the content needs to shrink (then work on the LayoutManager of the content) or the container needs to grow (and then work on the LayoutManager of the Container. – Guillaume Polet Jul 20 '12 at 22:57
  • I think I'd prefer for the container to grow. – Thunderforge Jul 21 '12 at 01:17

6 Answers6

12

First: never-ever tweak the sizing hints on the component level. Especially not so when you do have a powerful LayoutManager such as MigLayout which supports tweaking on the manager level.

In code, adjusting the pref size of whatever:

// calculate some width to add to pref, f.i. to take the scrollbar width into account
final JScrollPane pane = new JScrollPane(comp);
int prefBarWidth = pane.getVerticalScrollBar().getPreferredSize().width;
// **do not**  
comp.setPreferredSize(new Dimension(comp.getPreferredSize().width + prefBarWidth, ...);
// **do**
String pref = "(pref+" + prefBarWidth + "px)"; 
content.add(pane, "width " + pref);

That said: basically, you hit a (arguable) bug in ScrollPaneLayout. While it looks like taking the scrollbar width into account, it actually doesn't in all cases. The relevant snippet from preferredLayoutSize

// filling the sizes used for calculating the pref
Dimension extentSize = null;
Dimension viewSize = null;
Component view = null;

if (viewport != null) {
    extentSize = viewport.getPreferredSize();
    view = viewport.getView();
    if (view != null) {
        viewSize = view.getPreferredSize();
    } else {
        viewSize = new Dimension(0, 0);
    }
}

....

// the part trying to take the scrollbar width into account

if ((vsb != null) && (vsbPolicy != VERTICAL_SCROLLBAR_NEVER)) {
    if (vsbPolicy == VERTICAL_SCROLLBAR_ALWAYS) {
        prefWidth += vsb.getPreferredSize().width;
    }
    else if ((viewSize != null) && (extentSize != null)) {
        boolean canScroll = true;
        if (view instanceof Scrollable) {
            canScroll = !((Scrollable)view).getScrollableTracksViewportHeight();
        }
        if (canScroll && 
            // following condition is the **culprit** 
            (viewSize.height > extentSize.height)) {
            prefWidth += vsb.getPreferredSize().width;
        }
    }
}

it's the culprit, because

  • it's comparing the view pref against the viewport pref
  • they are the same most of the time

The result is what you are seeing: the scrollbar overlaps (in the sense of cutting off some width) the view.

A hack around is a custom ScrollPaneLayout which adds the scrollbar width if the view's height is less than the actual viewport height, a crude example (beware: not production quality) to play with

public static class MyScrollPaneLayout extends ScrollPaneLayout {

    @Override
    public Dimension preferredLayoutSize(Container parent) {
        Dimension dim =  super.preferredLayoutSize(parent);
        JScrollPane pane = (JScrollPane) parent;
        Component comp = pane.getViewport().getView();
        Dimension viewPref = comp.getPreferredSize();
        Dimension port = pane.getViewport().getExtentSize();
        // **Edit 2** changed condition to <= to prevent jumping
        if (port.height < viewPref.height) {
            dim.width += pane.getVerticalScrollBar().getPreferredSize().width;
        }
        return dim;
    }

}

Edit

hmm ... see the jumping (between showing vs. not showing the vertical scrollbar, as described in the comment): when I replace the textfield in my example with another scrollPane, then resizing "near" its pref width exhibits the problem. So the hack isn't good enough, could be that the time of asking the viewport for its extent is incorrect (in the middle of the layout process). Currently no idea, how to do better.

Edit 2

tentative tracking: when doing a pixel-by-pixel width change, it feels like a one-off error. Changing the condition from < to <= seems to fix the jumping - at the price of always adding the the scrollbar width. So on the whole, this leads to step one with a broader trailing inset ;-) Meanwhile believing that the whole logic of the scollLlayout needs to be improved ...

To summarize your options:

  • adjust the pref width in a (MigLayout) componentConstraint. It's the simplest, drawback is an addional trailing white space in case the scrollbar is not showing
  • fix the scrollPaneLayout. Requires some effort and tests (see the code of core ScrollPaneLayout what needs to be done), the advantage is a consistent padding w/out the scrollbar
  • not an option manually set the pref width on the component

Below are code examples to play with:

// adjust the pref width in the component constraint
MigLayout layout = new MigLayout("wrap 2", "[][]");
final JComponent comp = new JPanel(layout);
for (int i = 0; i < 10; i++) {
    comp.add(new JLabel("some item: "));
    comp.add(new JTextField(i + 5));
}

MigLayout outer = new MigLayout("wrap 2", 
        "[][grow, fill]");
JComponent content = new JPanel(outer);
final JScrollPane pane = new JScrollPane(comp);
int prefBarWidth = pane.getVerticalScrollBar().getPreferredSize().width;
String pref = "(pref+" + prefBarWidth + "px)";
content.add(pane, "width " + pref);
content.add(new JTextField("some dummy") );
Action action = new AbstractAction("add row") {

    @Override
    public void actionPerformed(ActionEvent e) {
        int count = (comp.getComponentCount() +1)/ 2;
        comp.add(new JLabel("some Item: "));
        comp.add(new JTextField(count + 5));
        pane.getParent().revalidate();
    }
};
frame.add(new JButton(action), BorderLayout.SOUTH);
frame.add(content);
frame.pack();
frame.setSize(frame.getWidth()*2, frame.getHeight());
frame.setVisible(true);

// use a custom ScrollPaneLayout
MigLayout layout = new MigLayout("wrap 2", "[][]");
final JComponent comp = new JPanel(layout);
for (int i = 0; i < 10; i++) {
    comp.add(new JLabel("some item: "));
    comp.add(new JTextField(i + 5));
}

MigLayout outer = new MigLayout("wrap 2", 
        "[][grow, fill]");
JComponent content = new JPanel(outer);
final JScrollPane pane = new JScrollPane(comp);
pane.setLayout(new MyScrollPaneLayout());
content.add(pane);
content.add(new JTextField("some dummy") );
Action action = new AbstractAction("add row") {

    @Override
    public void actionPerformed(ActionEvent e) {
        int count = (comp.getComponentCount() +1)/ 2;
        comp.add(new JLabel("some Item: "));
        comp.add(new JTextField(count + 5));
        pane.getParent().revalidate();
    }
};
frame.add(new JButton(action), BorderLayout.SOUTH);
frame.add(content);
frame.pack();
frame.setSize(frame.getWidth()*2, frame.getHeight());
frame.setVisible(true);
Community
  • 1
  • 1
kleopatra
  • 51,061
  • 28
  • 99
  • 211
  • Thank you for your very detailed solution. Aside from having to disable the horizontal scrollbar, I'm not seeing any difference between using insets or prefs to create enough space to accommodate an overlapping scrollbar, so perhaps it's just a matter of personal preference. I like the idea of fixing this "bug" in JScrollPane, so I'll be sure to play with that. Thanks again for your detailed response! – Thunderforge Jul 22 '12 at 01:30
  • Just tried creating my own JScrollPane with an overridden ScrollPaneLayout preferredLayoutSize method using your code. It works some of the time. When I try resizing the window, any JScrollPanes that don't have a visible VerticalScrollBar quickly alternate between the right size we are looking for and a wrong size. I can play with it some more, but if you have any ideas, I'd love to hear it. – Thunderforge Jul 22 '12 at 02:02
  • After some debugging, I found out that the results of pane.getViewport().getExtentSize() change pretty frequently, which appears to result in the behavior I described. Looking into how to fix that. – Thunderforge Jul 22 '12 at 02:37
3

I figured it out. The problem is that the JScrollPane conforms to the size of the content within and overlaps it when the scroll bar becomes visible. So I decided to just make the content wider off the bat to make it accommodate the scrollbar if it becomes visible. This can also be done by making the insets be the width of the Scrollbar, which is probably the more orthodox way to do it.

int scrollBarWidth = new JScrollPane().getVerticalScrollBar().getPreferredSize();
groupPanel.setLayout(new MigLayout("insets 1 " + scrollBarWidth + " 1 " + scrollBarWidth*1.5); //Adjust multipliers and widths to suit
//...
//Add everything in
//...
JScrollPane groupPanelScroller = new JScrollPane(groupPanel);
groupPanelScroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
this.add(groupPanelScroller, "align center");

EDIT: The answer above is probably the better answer, but here's my original answer for the curious, which actually resizes the panel.

JScrollPane groupPanelScroller = new JScrollPane(groupPanel);
groupPanel.setPreferredSize(new Dimension(groupPanel.getPreferredSize().width + groupPanelScroller.getVerticalScrollBar().getVisibleAmount()*2, 
        groupPanel.getPreferredSize().height));
groupPanelScroller.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
this.add(groupPanelScroller, "align center");

So what this does it make the content its same size plus twice the width of the scrollbar. Since everything is centered, it all looks the same and then some of the extra space on one side is filled in by the scrollbar if it appears.

Thunderforge
  • 19,637
  • 18
  • 83
  • 130
  • +1 good idea to send here your solution, please you can accepting your post (2days???) – mKorbel Jul 21 '12 at 06:41
  • and no - visibleAmount is not the property you are looking for to adjust the width .. and no: there is no need to disable the showing of the horizontal scrollBar if the pref width is adjusted by either of the options I suggested – kleopatra Jul 21 '12 at 22:10
  • 1
    Ah, I see that getPreferredSize() on the vertical scrollbar is probably the better way to get the width than getVisibleAmount(). I've changed that. You're right, this method does result in disabling the horizontal scrollbar, which may not be right for everyone. – Thunderforge Jul 22 '12 at 00:56
2

I've had the same problem, and arrived here after hours of trying to find the cause.. I ended up implementing my own ViewportLayout and wanted to share that solution too.

The underlying problem is, that the ScrollPaneLayout does not know that one dimension (in your case the height of the view) will be constrained to a max value, so it can't pre-determine whether scroll bars will be needed. Therefore it can't adjust the other, flexible dimension (in your case the width).

A custom ViewportLayout can take this into consideration, asking its parent for the allowed height, and thereby allowing the ScrollPaneLayout to adjust the preferred width accordingly:

public class ConstrainedViewPortLayout extends ViewportLayout {

    @Override
    public Dimension preferredLayoutSize(Container parent) {

        Dimension preferredViewSize = super.preferredLayoutSize(parent);

        Container viewportContainer = parent.getParent();
        if (viewportContainer != null) {
            Dimension parentSize = viewportContainer.getSize();
            preferredViewSize.height = parentSize.height;
        }

        return preferredViewSize;
    }
}

The ViewportLayout is set like this:

scrollPane.getViewport().setLayout(new ConstrainedViewPortLayout());
meyertee
  • 2,211
  • 17
  • 16
1

This bug is now several years old but still unfixed, so I'll add my contribution.

You can also work around the bug by implementing javax.swing.Scrollable on the view compoment of your JScrollPane. The ScrollPaneLayout checks the methods of this interface to determine whether to display a horizontal scrollbar first, before falling back to its buggy calculation.

You can implement the Scrollable interface so that the view component is treated exactly as it was before implementing Scrollable, except for the method determining whether a horizontal scrollbar is displayed.

A concrete example to demonstrate this which I haven't tested in isolation, but have extracted from our codebase. Change:

JPanel panel = new JPanel();
JScrollPane pane = new JScrollPane(panel);

to

class ScrollableJPanel extends JPanel implements Scrollable {
    @Override public Dimension getPreferredScrollableViewportSize() {
        return getPreferredSize();
    }

    @Override public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
        JScrollPane scrollPane = (JScrollPane)getParent();
        return orientation == SwingConstants.VERTICAL ? scrollPane.getVerticalScrollBar().getUnitIncrement() : scrollPane.getHorizontalScrollBar().getUnitIncrement();
    }

    @Override public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
        JScrollPane scrollPane = (JScrollPane)getParent();
        return orientation == SwingConstants.VERTICAL ? scrollPane.getVerticalScrollBar().getBlockIncrement() : scrollPane.getHorizontalScrollBar().getBlockIncrement();
    }

    @Override public boolean getScrollableTracksViewportHeight() {
        return false;
    }

    @Override public boolean getScrollableTracksViewportWidth() {
        Dimension sz = getSize();
        Dimension vsz = getViewportSize();
        return sz.width - 1 <= vsz.width; // This is the bodge
    }
}

JPanel panel = new ScrollableJPanel();
JScrollPane pane = new JScrollPane();

Whlie testing our application against a Java 1.8 rt.jar modified to print lots of debug output, we found that the value was only out by one pixel - so that's the constant we're using in the above bodge.

Incidentally this has been filed with Oracle https://bugs.openjdk.java.net/browse/JDK-8042020, but it's helpfully marked as "Won't Fix".

Mike B
  • 1,600
  • 1
  • 12
  • 8
0
  • this image talking about GridLayout, both parts have got the same size

  • use proper LayoutManager that accepting different PreferredSize, BoxLayout or GridBagLayout

mKorbel
  • 109,525
  • 20
  • 134
  • 319
  • I'm actually using MigLayout (http://miglayout.com). Sorry, should have mentioned that first. However, it seems to occur no matter what layout I'm using. – Thunderforge Jul 20 '12 at 22:04
  • I'm sorry, I'm not entirely sure what you are asking. Are you asking if I have posted this question to SSCCE? What is that? – Thunderforge Jul 20 '12 at 22:08
  • Ah okay. I edited the original post a little while ago to include the code. Again though, this behavior happens whether I am using MigLayout or not. And if i resize the window so it's too small to show the contents in the left panel, then the same behavior occurs as in the right panel. – Thunderforge Jul 20 '12 at 22:37
  • -1 for suggesting less powerful - compared to Mig - LayoutManagers which don't solve the problem. Actually, it's not any application code manager that's misbehaving, it's the ScrollPaneLayout ;-) – kleopatra Jul 21 '12 at 15:16
0

Ok I had exactly the same problem:

  • dynamically add (sub)panels into a (main)panel, which is put into a jscrollpane (I want to create a vertical list of panels)
  • the vertical scrollbar of the jscrollpane overlap my content (ie my subpanels).

I've tried to follow lot of workarounds found on internet (and stackoverflow) without a real solution!

Here is what I understand, and my working solution:

  • It seems there is a bug in scrollpanelayout (the layout manager used by the jscrollpane) which does not compute correctly the size of component in it.

  • If you put mainpanel.setPreferedSize(new Dimension(1, 1)), there is no more overlap bug, but the vertical scrollbar (event if put as VERTICAL_SCROLLBAR_ALWAYS) will not scroll! It's because the scrollpane think your content is 1 pixel height.

  • So just iterate over your subpanel and compute the appropriate height:

//into your constructor or somewhere else
JPanel mainpanel = new JPanel();
mainpanel.setLayout(new BoxLayout(mainpanel, BoxLayout.Y_AXIS));
JScrollPane scrollPane = new JScrollPane(mainpanel, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);


//into another function to create dynamic content
int height = 0;
for (int i = 0; i < nb_subpanel_to_create; ++i) {
    JPanel subpanel = create_your_sub_panel();
    height += subpanel.getPreferedSize().height;
    mainpanel.add(subpanel);
}
mainpanel.setPreferedSize(new Dimension(1, height);

Bonus:

If you want that the content into the "list" is stack to the top:

List<JComponent> cmps = new LinkedList<JComponent>();
int height = 0;
for (int i = 0; i < nb_subpanel_to_create; ++i) {
    JPanel subpanel = create_your_sub_panel();
    height += subpanel.getPreferedSize().height;
    cmps.add(subpanel);
}
mainpanel.add(stackNorth(subpanel));
mainpanel.setPreferedSize(new Dimension(1, height);

and the stackNorth function:

public static JPanel stackNorth(List<JComponent> components) {
     JPanel last = new JPanel(new BorderLayout());
     JPanel first = last;
     for (JComponent jComponent : components) {
          last.add(jComponent, BorderLayout.NORTH);
          JPanel tmp = new JPanel(new BorderLayout());
          last.add(tmp, BorderLayout.CENTER);
          last = tmp;
     }
     return first;
} 
Alexxx
  • 766
  • 1
  • 11
  • 19