Here's what I want.
JScrollPane
.Can I square the three together?
The problem is any component's preferredSize
effectively becomes its minimumSize
if it's placed inside a JScrollPane
, it never shrinks — but it is cropped. I can't meet the second requirement while also meeting the other two. As shown in the second screenshot, the component's cropped (not shrunk) once I shrink the container beyond the component's preferred width.
I think another way to put it is this: I expect the scroller to "kick in" (e.g. show the scrollbar, if it's "as needed") only once its components' minimum (not preferred) sizes can no longer be honored.
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.WindowConstants;
import java.awt.Color;
import java.awt.Dimension;
public class ScrollPaneDemo {
public static void main(String[] args) {
SwingUtilities.invokeLater(ScrollPaneDemo::launch);
}
private static void launch() {
JFrame frame = new JFrame("ScrollPane Demo");
frame.setContentPane(createScrollPane());
frame.setLocationRelativeTo(null);
frame.pack();
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setVisible(true);
}
private static JScrollPane createScrollPane() {
JScrollPane scrollPane = new JScrollPane(createScrollablePanel());
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
return scrollPane;
}
private static JPanel createScrollablePanel() {
JPanel panel = new JPanel();
panel.setBorder(BorderFactory.createLineBorder(Color.RED, 3));
panel.setPreferredSize(new Dimension(250, 150));
return panel;
}
}
I implemented VGR's idea. It works.
JScrollPane
available at instantiationprivate static JScrollPane createScrollPane() {
JScrollPane scrollPane = new JScrollPane();
scrollPane.setViewportView(createScrollablePanel(scrollPane));
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
return scrollPane;
}
private static JPanel createScrollablePanel(JScrollPane scrollPane) {
JPanel panel = new ScrollablePanel(scrollPane);
panel.setBorder(BorderFactory.createLineBorder(Color.RED, 3));
panel.setPreferredSize(new Dimension(350, 250)); // not honored
panel.setMinimumSize(new Dimension(200, 250)); // honored
return panel;
}
private static class ScrollablePanel extends JPanel implements Scrollable {
private final JViewport viewport;
private ScrollablePanel(JScrollPane scrollPane) {
this(scrollPane.getViewport());
}
private ScrollablePanel(JViewport viewport) {
this.viewport = viewport;
}
@Override
public Dimension getPreferredScrollableViewportSize() {
return getPreferredSize();
}
@Override
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
return 5; // arbitrary
}
@Override
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
return 10; // arbitrary
}
@Override
public boolean getScrollableTracksViewportWidth() {
return getMinimumSize().getWidth() <= viewport.getWidth();
}
@Override
public boolean getScrollableTracksViewportHeight() {
return getMinimumSize().getHeight() <= viewport.getHeight();
}
}
JScrollPane
not available at instantiation private static JPanel createScrollablePanel() {
JPanel panel = new ScrollablePanel();
panel.setBorder(BorderFactory.createLineBorder(Color.RED, 3));
panel.setPreferredSize(new Dimension(250, 150)); // not honored
panel.setMinimumSize(new Dimension(150, 150)); // honored
return panel;
}
private static class ScrollablePanel extends JPanel implements Scrollable {
private JScrollPane scrollPane;
@Override
public Dimension getPreferredScrollableViewportSize() {
return getPreferredSize();
}
@Override
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
if (scrollPaneNotFound()) return 5; // arbitrary
JScrollBar scrollBar = scrollPane.getVerticalScrollBar();
return scrollBar.getUnitIncrement();
}
private boolean scrollPaneNotFound() {
return !scrollPaneFound();
}
private boolean scrollPaneFound() {
if (scrollPane != null) return true;
scrollPane = findScrollPane().orElse(null);
return scrollPane != null;
}
public Optional<JScrollPane> findScrollPane() {
JScrollPane scrollPane = (JScrollPane) SwingUtilities.getAncestorOfClass(JScrollPane.class, this);
return Optional.ofNullable(scrollPane);
}
@Override
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
if (scrollPaneNotFound()) return 10; // arbitrary
JScrollBar scrollBar = scrollPane.getVerticalScrollBar();
return scrollBar.getBlockIncrement();
}
@Override
public boolean getScrollableTracksViewportWidth() {
if (scrollPaneNotFound()) return false;
return getMinimumSize().getWidth() <= scrollPane.getViewport().getWidth();
}
@Override
public boolean getScrollableTracksViewportHeight() {
if (scrollPaneNotFound()) return false;
return getMinimumSize().getHeight() <= scrollPane.getViewport().getHeight();
}
}
It won't work if the component is not directly set as the scroller's view.
private static JScrollPane createScrollPane() {
JScrollPane scrollPane = new JScrollPane();
JPanel panel = new JPanel(new BorderLayout()); // this will break everything
panel.add(createScrollablePanel());
scrollPane.setViewportView(panel);
scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
return scrollPane;
}
It's because ScrollPaneLayout
doesn't search for Scrollable
s recursively:
// javax.swing.ScrollPaneLayout#layoutContainer
if (!isEmpty && view instanceof Scrollable) {
sv = (Scrollable)view;
viewTracksViewportWidth = sv.getScrollableTracksViewportWidth();
viewTracksViewportHeight = sv.getScrollableTracksViewportHeight();
}