Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Provide default component width, allow shrinking inside JScrollPane

Tags:

java

swing

Here's what I want.

  1. I want a component to have a default "preferred" width.
  2. (important) The component is fine with being shrunk to some degree, it's just it shouldn't be displayed in a shrunk state initially.
  3. I also want it to be inside a 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;
    }
}

container exceeds the component's preferred width

container is less than the component's preferred width, the component's cropped

like image 399
Sergey Zolotarev Avatar asked Dec 19 '25 15:12

Sergey Zolotarev


2 Answers

I implemented VGR's idea. It works.

Scenario #1: reference to JScrollPane available at instantiation

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

the component's shown in its preferred size

the component's shrunk beyond its preferred width as the viewport's shrunk

the viewport's shrunk beyond the component's minimum width, the component's cropped

Scenario #2: reference to 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();
        }
    }

Caveat

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 Scrollables recursively:

// javax.swing.ScrollPaneLayout#layoutContainer
        if (!isEmpty && view instanceof Scrollable) {
            sv = (Scrollable)view;
            viewTracksViewportWidth = sv.getScrollableTracksViewportWidth();
            viewTracksViewportHeight = sv.getScrollableTracksViewportHeight();
        }
like image 127
Sergey Zolotarev Avatar answered Dec 21 '25 05:12

Sergey Zolotarev


If I understand the requirement you use the ScrollablePanel with the "FIT" parameter:

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.*;
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();
        ScrollablePanel panel = new ScrollablePanel();
        panel.setScrollableWidth( ScrollablePanel.ScrollableSizeHint.FIT );
        panel.setBorder(BorderFactory.createLineBorder(Color.RED, 3));
        panel.setPreferredSize(new Dimension(250, 150));
        return panel;
    }
}

If none of the parameters do exactly what you require, then yes, you will need to do a custom implementation the implements the Scrollable interface.

like image 31
camickr Avatar answered Dec 21 '25 04:12

camickr



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!