4

I'm trying to build a panel with components laid out horizontally, with auto-wrap if not enough place and a vertical scrollbar.

Something like this:

+-----------------+
|[1][2][3][4][5]  |
|                 | 
+-----------------+

reducing the width:

+-----------+
|[1][2][3]  |
|[4][5]     |
+-----------+

reducing the width again, the scrollbar appears:

+---------+
|[1][2]  ^|
|[3][4]  v|
+---------+

I'm not far from a solution:

public class TestFlow extends JFrame {

    public TestFlow() {

        getContentPane().setLayout(new BorderLayout());

        JPanel panel = new JPanel(new FlowLayout());
        JScrollPane scroll = new JScrollPane(panel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);

        getContentPane().add(scroll, BorderLayout.CENTER);

        panel.add(new MyComponent("A"));
        panel.add(new MyComponent("B"));
        panel.add(new MyComponent("C"));
        panel.add(new MyComponent("D"));
        panel.add(new MyComponent("E"));
        panel.add(new MyComponent("F"));
        panel.add(new MyComponent("G"));
        panel.add(new MyComponent("H"));
        panel.add(new MyComponent("I"));
        panel.add(new MyComponent("J"));
        panel.add(new MyComponent("K"));
        panel.add(new MyComponent("L"));
        panel.add(new MyComponent("M"));
        panel.add(new MyComponent("N"));
        panel.add(new MyComponent("O"));

        scroll.addComponentListener(new ComponentAdapter() {

            @Override
            public void componentResized(ComponentEvent e) {
                Dimension max=((JScrollPane)e.getComponent()).getViewport().getExtentSize();
//                panel.setMaximumSize(new Dimension(max.width,Integer.MAX_VALUE));
                panel.setPreferredSize(new Dimension(max.width,Integer.MAX_VALUE));
//                panel.setPreferredSize(max);
                panel.revalidate();
//                panel.repaint();
//                System.out.println(panel.getSize().width+"--"+max.width);
            }

        });

        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setSize(500, 200);
        setLocationRelativeTo(null);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> new TestFlow().setVisible(true));
    }

    private static class MyComponent extends JLabel {

        public MyComponent(String text) {
            super(String.join("", Collections.nCopies((int)(Math.round(Math.random()*4)+4), text)));
            setOpaque(true);
            setBackground(Color.YELLOW);
        }

    }

}

, but have still strange behaviours:

With the solution panel.setPreferredSize(new Dimension(max.width,Integer.MAX_VALUE));

  • after a resize of the window, the panel gets empty. I must manually move the scrollbar to have the content appearing
  • the scrollbar is always visible, while it shouldn't

With the solution panel.setPreferredSize(max);

  • after a resize of the window, the panel is not re-laid out. I must manually move a second time the window to have the content re-laid out.
  • the scrollbar is never visible, while it should.

Any suggestion in that code ?

[EDIT] I've complicated the original code, and applied the suggestions that were provided until now.

For design purpose, I would like to use a MigLayout on top of my Panel. At start, everything is well laid out. When enlarging the window, it works too. But not on reducing the window. The addComponentListenerdoes not bring any addedvalue.

public class TestMigFlow extends JFrame {

    public TestMigFlow() {

        getContentPane().setLayout(new BorderLayout());
        JPanel panel = new JPanel(new MigLayout("debug, fill, flowy", "[fill]"));
        JScrollPane scroll = new JScrollPane(panel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);

        getContentPane().add(scroll, BorderLayout.CENTER);

        panel.add(new JLabel("A title as spearator "), "growy 0");

        JPanel sub = new JPanel(new WrapLayout());
//        panel.add(sub, "growx 0"); // Works well on shrink but not on grow
        panel.add(sub); // Works well on grow but not on shrink

        sub.add(new MyComponent("A"));
        sub.add(new MyComponent("B"));
        sub.add(new MyComponent("C"));
        sub.add(new MyComponent("D"));
        sub.add(new MyComponent("E"));
        sub.add(new MyComponent("F"));
        sub.add(new MyComponent("G"));
        sub.add(new MyComponent("H"));
        sub.add(new MyComponent("I"));
        sub.add(new MyComponent("J"));
        sub.add(new MyComponent("K"));
        sub.add(new MyComponent("L"));
        sub.add(new MyComponent("M"));
        sub.add(new MyComponent("N"));
        sub.add(new MyComponent("O"));

        addComponentListener(new ComponentAdapter() {

            @Override
            public void componentResized(ComponentEvent e) {
                Dimension max = new Dimension(scroll.getWidth(), Short.MAX_VALUE);
                panel.setMaximumSize(max);
                panel.repaint();
            }

        });

        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setSize(200, 500);
        setLocationRelativeTo(null);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> new TestMigFlow().setVisible(true));
    }

    private static class MyComponent extends JLabel {

        public MyComponent(String text) {
            super(String.join("", Collections.nCopies((int) (Math.round(Math.random() * 4) + 4), text)));
            setOpaque(true);
            setBackground(Color.YELLOW);
        }

    }
lvr123
  • 524
  • 6
  • 24
  • 1
    Possible duplicate of [How do I make this FlowLayout wrap within its JSplitPane?](https://stackoverflow.com/questions/5709690/how-do-i-make-this-flowlayout-wrap-within-its-jsplitpane) – Sergiy Medvynskyy Sep 26 '19 at 10:04
  • 1
    The `WrapLayout` in the linked question could really do it. But a side note: Using `Integer.MAX_VALUE` in **any** size computation will badly, badly mess up things. Any computation like `MAX_VALUE + 1` will cause the result to be a *negative* value. The closest thing to an "infinitely large" component that you should ever use is `Short.MAX_VALUE` (but in >20 years of Swing programming, I only used this *once*, in a very specific context - usually, the size of a component is known more precisely...) – Marco13 Sep 26 '19 at 12:23
  • Thanks. WrapLayout is the solution for that example. Integrated as a component in a Panel laid out by a MigLayout, it behaves less well. I will update the example. – lvr123 Sep 26 '19 at 20:44
  • I've updated the example to a more complicated one. – lvr123 Oct 07 '19 at 09:31

3 Answers3

0

The code below works for me.

I have split the code into a component that does all the layout and the test code to verify that it works (in the static main) method.

Instead of hooking to resize events on the JScrollPane, I do that on the whole TestFlow (TF) component - I believe this is equivalent, but it seems safer. Whenever the TF is resized, I update the preferred size of the panel, just as you did - but there are two steps that you were not taking:

  • the width of the inner panel will be whatever width the outer panel has been asigned, but should also leave enough space for the vertical scrollbar, if it is needed.
  • the height of the inner panel should be that of the lowest component that it is displaying. In my case, I am taking the shortcut of assuming that all components in the lowest row are of equal height, but this need not be always true.

Diving into how Swing handles layout and painting, and which of the several similar-looking methods get called when by whom always makes me think of dark magic.

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;

public class TestFlow extends JPanel {

    JPanel panel;
    JScrollPane scroll;

    public TestFlow() {
        panel = new JPanel(new FlowLayout());
        scroll = new JScrollPane(panel, 
            ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, 
            ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        setLayout(new BorderLayout());
        add(scroll, BorderLayout.CENTER);
        addComponentListener(new ComponentAdapter(){
            @Override
            public void componentResized(ComponentEvent e){
                Rectangle lastChildBounds = panel
                    .getComponent(panel.getComponentCount() - 1).getBounds();
                Dimension preferred = new Dimension(
                    scroll.getWidth(), 
                    lastChildBounds.y + lastChildBounds.height);
                if (scroll.getVerticalScrollBar().isVisible()) {
                    preferred.width -= scroll.getVerticalScrollBar().getWidth();
                }
                // System.err.println("setting inner panel size to " + preferred);
                panel.setPreferredSize(preferred);
                panel.repaint();
            }
        });
    }

    public JPanel getInnerPanel() { return panel; }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            // get a TestFlow instance and fill it with random labels
            TestFlow tf = new TestFlow();
            Random r = new Random();
            for (String s : "A B C D E F G H I J K L M N O".split(" ")) {
                String labelText = String.join("", Collections.nCopies(4+r.nextInt(3)*4, s));
                JLabel label = new JLabel(labelText);
                label.setOpaque(true);
                label.setBackground(Color.YELLOW);
                tf.getInnerPanel().add(label);
            }

            // display it in a window
            JFrame jf = new JFrame("test");
            jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);            
            jf.add(tf, BorderLayout.CENTER);
            jf.setSize(500, 200);
            jf.setLocationRelativeTo(null);
            jf.setVisible(true);
        });
    }
}
tucuxi
  • 17,561
  • 2
  • 43
  • 74
  • Thanks. However, for the original case, the WrapLayout suggestion works fine and is easier. I applied your suggestion in the more complicated case, but I didn't bring an added-value. – lvr123 Oct 04 '19 at 06:49
0

Probably I misunderstood your question, but when I add revalidate call, all works fine also when the panel is shrinking.

Dimension max = new Dimension(scroll.getWidth(), Short.MAX_VALUE);
panel.setMaximumSize(max);
// trigger layout recalculation
panel.revalidate();
panel.repaint();

Here is a little bit sophisticated improvement (to provide better size consideration for scroll bar and layout), when it's required ;)

public class TestMigFlow extends JFrame {
    private Integer layoutDecr;

    public TestMigFlow() {

        getContentPane().setLayout(new BorderLayout());
        JPanel panel = new JPanel(new MigLayout("debug, fill, flowy", "[fill]"));
        JScrollPane scroll =
                new JScrollPane(panel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);

        getContentPane().add(scroll, BorderLayout.CENTER);

        panel.add(new JLabel("A title as spearator "), "growy 0");

        JPanel sub = new JPanel(new WrapLayout());
        // panel.add(sub, "growx 0"); // Works well on shrink but not on grow
        panel.add(sub); // Works well on grow but not on shrink

        sub.add(new MyComponent("A"));
        sub.add(new MyComponent("B"));
        sub.add(new MyComponent("C"));
        sub.add(new MyComponent("D"));
        sub.add(new MyComponent("E"));
        sub.add(new MyComponent("F"));
        sub.add(new MyComponent("G"));
        sub.add(new MyComponent("H"));
        sub.add(new MyComponent("I"));
        sub.add(new MyComponent("J"));
        sub.add(new MyComponent("K"));
        sub.add(new MyComponent("L"));
        sub.add(new MyComponent("M"));
        sub.add(new MyComponent("N"));
        sub.add(new MyComponent("O"));

        addComponentListener(new ComponentAdapter() {

            @Override
            public void componentResized(ComponentEvent e) {
                if (layoutDecr == null && panel.getWidth() > 0) {
                    layoutDecr = panel.getWidth() - sub.getWidth();
                }
                JScrollBar vBar = scroll.getVerticalScrollBar();
                int decr = vBar.isVisible() ? vBar.getPreferredSize().width : 0;
                decr += layoutDecr == null ? 0 : layoutDecr;
                Dimension max = new Dimension(scroll.getWidth() - decr, Short.MAX_VALUE);
                sub.setMaximumSize(max);
                sub.revalidate();
                sub.repaint();
            }

        });

        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setSize(200, 500);
        setLocationRelativeTo(null);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> new TestMigFlow().setVisible(true));
    }

    private static class MyComponent extends JLabel {

        public MyComponent(String text) {
            super(String.join("", Collections.nCopies((int) (Math.round(Math.random() * 4) + 4), text)));
            setOpaque(true);
            setBackground(Color.YELLOW);
        }

    }
}
Sergiy Medvynskyy
  • 11,160
  • 1
  • 32
  • 48
  • The content is well laid out at start-up and on enlarging the window but not on reducing the width of the window. To have the window reduced correctly, first reduce the window followed by a small enlargement of it. Then you have it right. So this is not working as expected. Something to do the Miglayout options ? – lvr123 Nov 12 '19 at 18:04
  • What about getting rid off of the MigLayout ? My goal is to have a stack of elements : a label-separator that never grows nor shrink vertically, followed by a panel containing different images (the "AAA", "BBB" of the example) that can grow and shrink vertically depending of the place needed to display all the elements, depending of the available width. This combination label-separator/images-container can be repeated multiple times. A Miglayout seemed a good approach... – lvr123 Nov 12 '19 at 18:43
  • The `addComponentListener` at the Dialog level was the wrong choice. Because that moment of that event, the panel has not received its full width and all the computation is in correct. The trick is also to use the `preferredLayoutSize` of the WrapLayout to have everything computed how should be before revalidating. See my solution. – lvr123 Nov 28 '19 at 22:04
0

The solution is

  1. to wait until the panel's container has been resized. To have thus the addComponentListener at the panel level and not the frame level
  2. to use the WrapLayout
  3. to rely on the WrapLayout's preferredLayoutSize the compute the adequate size of the panel
  4. to use a "fillx" option in the MigLayout

The negative-side of the solution is a bit of flickering when resizing the window.

public class TestMigFlow2 extends JFrame {

    public TestMigFlow2() {

        getContentPane().setLayout(new BorderLayout());
        JPanel panel = new JPanel(new MigLayout("fillx, flowy", "[fill]"));
        JScrollPane scroll
                = new JScrollPane(panel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);

        getContentPane().add(scroll, BorderLayout.CENTER);

        panel.add(new MySeparator("sep1"), "growy 0, shrinky 100");

        JPanel sub = new JPanel(new WrapLayout());
        panel.add(sub, "shrinky 100"); // Works well on grow but not on shrink

        sub.add(new MyComponent("A"));
        sub.add(new MyComponent("B"));
        sub.add(new MyComponent("C"));
        sub.add(new MyComponent("D"));
        sub.add(new MyComponent("E"));
        sub.add(new MyComponent("F"));
        sub.add(new MyComponent("G"));
        sub.add(new MyComponent("H"));
        sub.add(new MyComponent("I"));
        sub.add(new MyComponent("J"));
        sub.add(new MyComponent("K"));
        sub.add(new MyComponent("L"));
        sub.add(new MyComponent("M"));
        sub.add(new MyComponent("N"));
        sub.add(new MyComponent("O"));

        panel.add(new MySeparator("sep2"), "growy 0, shrinky 100");

        panel.addComponentListener(new ComponentAdapter() {
            @Override
            public void componentResized(ComponentEvent e) {

                WrapLayout wl = (WrapLayout) sub.getLayout();
                Dimension prefdim = wl.preferredLayoutSize(sub);
                sub.setPreferredSize(prefdim);
                panel.revalidate();
                panel.repaint();
            }

        });

        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setSize(200, 500);
        setLocationRelativeTo(null);
    }

The same also works with a BoxLayout instead of a MigLayout with 2 additions:

  1. add VerticulGlue as last element
  2. giving the panel a max height along to pref height to prevent the BoxLayout to share the extra space between the panel and the Vertical glue.

    public TestBoxLayout() {

        getContentPane().setLayout(new BorderLayout());
        JPanel panel = new JPanel();
        BoxLayout layout = new BoxLayout(panel, BoxLayout.PAGE_AXIS);
        panel.setLayout(layout);
        JScrollPane scroll
                = new JScrollPane(panel, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
    
        getContentPane().add(scroll, BorderLayout.CENTER);
    
        JLabel separator;
        separator = new MySeparator("Sep1");
        panel.add(separator);
    
        JPanel sub = new MyPanel(new WrapLayout());
        sub.setAlignmentX(0f);
        panel.add(sub);
    
        sub.add(new MyComponent("A"));
        sub.add(new MyComponent("B"));
        sub.add(new MyComponent("C"));
        sub.add(new MyComponent("D"));
        sub.add(new MyComponent("E"));
        sub.add(new MyComponent("F"));
        sub.add(new MyComponent("G"));
        sub.add(new MyComponent("H"));
        sub.add(new MyComponent("I"));
        sub.add(new MyComponent("J"));
        sub.add(new MyComponent("K"));
        sub.add(new MyComponent("L"));
        sub.add(new MyComponent("M"));
        sub.add(new MyComponent("N"));
        sub.add(new MyComponent("O"));
    
        separator = new MySeparator("Sep2");
        panel.add(separator);
    
        // -- Un filler --
        panel.add(Box.createVerticalGlue());
    
    
        panel.addComponentListener(new ComponentAdapter() {
            @Override
            public void componentResized(ComponentEvent e) {
    
                WrapLayout wl=(WrapLayout) sub.getLayout();
                Dimension prefdim=wl.preferredLayoutSize(sub);
                sub.setPreferredSize(prefdim);
                // Force the max height = pref height to prevent the BoxLayout dispatching the remaining height between the panel and the glue.
                Dimension maxdim=new Dimension(Short.MAX_VALUE,prefdim.height);
                sub.setMaximumSize(maxdim);
                panel.revalidate();
                panel.repaint();
            }
    
        });
    
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setSize(200, 500);
        setLocationRelativeTo(null);
    }
    
lvr123
  • 524
  • 6
  • 24