Cagepi
Cagepi

Reputation: 199

Avoiding GridBagLayout, still ensuring vertical fill

I need two horizontally positioned components in a container, both of them should fill all of their container's vertical space.

Do I now have to use the heavy-weight GridBagLayout, because of the fill requirement? (and if I want it to flow to boot, bad luck, I'm out of options)

component layout

Java 8, Swing.

Upvotes: 0

Views: 72

Answers (1)

gthanop
gthanop

Reputation: 3366

As an alternative to GridBagLayout for this scenario, you can use a BoxLayout like so:

import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class BoxLayoutMain {
    
    /**
     * Specifies minimum and maximum sizes for each child {@code Component} of the
     * {@linkplain BoxLayout#getTarget() target} of the given {@code BoxLayout}, such that aspect
     * ratios of their preferred size are maintained throughout parent resizing. Assumes all
     * children of the target are already added to it. Assumes the layout is horizontal.
     * @param boxLayout
     */
    private static void assignHorizontalBoxLayoutChildWeights(final BoxLayout boxLayout) {
        final int maxValue = Short.MAX_VALUE;
        final Container parent = boxLayout.getTarget();
        final int totalWidth = parent.getPreferredSize().width;
        final Dimension commonMinimumSize = new Dimension();
        for (final Component child: parent.getComponents()) {
            child.setMinimumSize(commonMinimumSize);
            final Dimension prefSize = child.getPreferredSize();
            child.setMaximumSize(new Dimension((int) Math.round(maxValue * (prefSize.width / (double) totalWidth)), maxValue));
        }
    }
    
    public static void main(final String[] args) {
        SwingUtilities.invokeLater(() -> {
            
            final JLabel component1 = new JLabel("Component 1", JLabel.CENTER);
            component1.setBorder(BorderFactory.createLineBorder(Color.RED, 5));
            
            final JLabel component2 = new JLabel("Component 2 (which is a little longer)", JLabel.CENTER);
            component2.setBorder(BorderFactory.createLineBorder(Color.GREEN, 5));
            
            final JPanel parent = new JPanel();
            parent.setBorder(BorderFactory.createLineBorder(Color.BLUE, 5));
            final BoxLayout layout = new BoxLayout(parent, BoxLayout.X_AXIS);
            parent.setLayout(layout);
            parent.add(component1);
            parent.add(component2);
            
            assignHorizontalBoxLayoutChildWeights(layout);
            
            final JFrame frame = new JFrame("Box for no gridbag");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.add(parent);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }
}

This produces the following result:

Candidate solution with BoxLayout

When the parent Container is resized the children maintain their width aspect ratios, while always filling their whole height.

Nevertheless, sadly, a minor gap can be observed between green and blue borders on the right side of the parent when it is resized. This is shown in the brown circle in the following image:

BoxLayout solution problem, with gap between borders

This may not be a problem though depending on the situation (eg in case you are not using any borders for the children in the above code).


Update:

SpringLayout also comes to mind for this scenario:

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import javax.swing.BorderFactory;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Spring;
import javax.swing.SpringLayout;
import javax.swing.SwingUtilities;

public class SpringLayoutMainWithoutFixedSizes {
    
    private static Component createComponent(final String label,
                                             final Color borderColor) {
        final JPanel panel = new JPanel(new BorderLayout());
        panel.setBorder(BorderFactory.createLineBorder(borderColor, 5));
        if (label != null) {
            final JLabel c = new JLabel(label, JLabel.CENTER);
            panel.add(c, BorderLayout.CENTER);
        }
        return panel;
    }
    
    public static void main(final String[] args) {
        SwingUtilities.invokeLater(() -> {
            
            final Component component1 = createComponent("Component 1", Color.RED),
                            component2 = createComponent("Component 2 (which is a little longer)", Color.GREEN);
            
            final SpringLayout layout = new SpringLayout();
            
            final JPanel parent = new JPanel(layout);
            parent.setBorder(BorderFactory.createLineBorder(Color.BLUE, 5));
            parent.add(component1);
            parent.add(component2);
            
            final SpringLayout.Constraints constraints1 = layout.getConstraints(component1),
                                           constraints2 = layout.getConstraints(component2),
                                           constraintsParent = layout.getConstraints(parent);
            
            //Component 2 x will be equal to component 1 width:
            constraints2.setX(constraints1.getWidth());
            
            //The width of the container will be equal to the summary of the width of the two children:
            constraintsParent.setWidth(Spring.sum(constraints1.getWidth(), constraints2.getWidth()));
            
            //The height of the container will be equal to the maximum height between the two children:
            constraintsParent.setHeight(Spring.max(constraints1.getHeight(), constraints2.getHeight()));
            
            final JFrame frame = new JFrame("Spring for no gridbag");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.add(parent);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }
}

Which is not explicitly setting minimum/maximum/preferred sizes per child.

Notice however here that the width ratio of the preferred sizes of the children is not preserved intact as we resize the window. This is because of the values of minimum and maximum sizes per child, which are also taken into account. This is true for both SpringLayout and BoxLayout. For example, because the children have equal maximum sizes, their width ratio tends to 1 as we enlarge the window. In other words they tend to occupy equal space horizontally for sizes greater than the preferred ones.

I also wrap the JLabel children into a JPanel each, because JLabels by default have minimum and maximum sizes equal to their preferred size, thus the width ratio change could not be observed by JLabels without manually setting their minimum and maximum sizes.

Follows a BoxLayout approach equivalent to the posted SpringLayout one:

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class BoxLayoutMainWithoutFixedSizes {
    
    private static Component createComponent(final String label,
                                             final Color borderColor) {
        final JPanel panel = new JPanel(new BorderLayout());
        panel.setBorder(BorderFactory.createLineBorder(borderColor, 5));
        if (label != null) {
            final JLabel c = new JLabel(label, JLabel.CENTER);
            panel.add(c, BorderLayout.CENTER);
        }
        return panel;
    }
    
    public static void main(final String[] args) {
        SwingUtilities.invokeLater(() -> {
            
            final Component component1 = createComponent("Component 1", Color.RED),
                            component2 = createComponent("Component 2 (which is a little longer)", Color.GREEN);
            
            final JPanel parent = new JPanel();
            parent.setBorder(BorderFactory.createLineBorder(Color.BLUE, 5));
            final BoxLayout layout = new BoxLayout(parent, BoxLayout.X_AXIS);
            parent.setLayout(layout);
            parent.add(component1);
            parent.add(component2);
            
            final JFrame frame = new JFrame("Box for no gridbag");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.add(parent);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });
    }
}

The above (last) code does not explicitly set minimum nor maximum sizes per child, but on the other hand the width ratio is not preserved for the same reason as for the SpringLayout code (ie exactly because we are not settings sizes, which are taken into account by these two LayoutManagers).


Note, I now understand that you want an equivalent solution to GridBagLayout without explicitly setting minimum/maximum/preferred sizes, but I leave the above ideas as close alternatives.

In case there is a need for the user to be able to determine manually the width of each Component then you could use a JSplitPane instead.

Upvotes: 1

Related Questions