javaswingjscrollpanemiglayoutflowlayout

Layout with auto-wrap within JScrollBar


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));

With the solution panel.setPreferredSize(max);

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

    }

Solution

  • 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);
      }