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.
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.
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());
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
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:
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);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With