5
\$\begingroup\$

I have created a simple component in Swing that lets me place other components such as buttons on top of each other.

They fill their parent horizontally but not vertically, which is exactly as I want it. I called this parent component VerticalPanel and this is how it looks:

verticalPanel

I can't shake the feeling though that there could be a more elegant solution. Especially the need for my VerticalPanel to know the number of components it will hold in advance is something I'm not entirely happy with.

Here's the VerticalPanel:

public class VerticalPanel extends JPanel {
    private static final long serialVersionUID = 1L;

    public VerticalPanel(int nrOfComps) {
        LayoutManager layout = getVerticalLayout(nrOfComps);
        setLayout(layout);
    }

    @Override
    public Component add(Component comp) {
        return add(comp, getComponentCount());
    }

    @Override
    public Component add(Component comp, int index) {
        final GridBagConstraints gbc = new GridBagConstraints();
        gbc.fill = GridBagConstraints.HORIZONTAL;
        // Allocate extra width to the buttons
        gbc.weightx = 1.0;
        gbc.anchor = GridBagConstraints.NORTH;
        gbc.gridx = 0;
        gbc.gridy = index;
        add(comp, gbc);
        return comp;
    }

    private LayoutManager getVerticalLayout(int nrOfComps) {
        GridBagLayout layout = new GridBagLayout();

        double[] rowWeights = new double[nrOfComps];
        rowWeights[nrOfComps - 1] = Double.MIN_VALUE;
        layout.rowWeights = rowWeights;
        return layout;
    }
}

This creates the example from the screenshot above:

public class VerticalButtons {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager
                            .getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException
                        | IllegalAccessException
                        | UnsupportedLookAndFeelException e) {
                    e.printStackTrace();
                }
                createAndShowGUI();
            }
        });
    }

    private static void createAndShowGUI() {
        JFrame frame = new JFrame();

        frame.getContentPane().add(createVerticalButtons());
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        frame.pack();
        frame.setVisible(true);
    }

    private static JComponent createVerticalButtons() {

        VerticalPanel buttons = new VerticalPanel(2);

        JButton b1 = new JButton("1");
        JButton b2 = new JButton("2");
        buttons.add(b1);
        buttons.add(b2);

        return buttons;
    }
}
\$\endgroup\$
3
  • 2
    \$\begingroup\$ Pretty cool. Looks almost like a WPF StackPanel - would be nice to make it a StackPanel and be able to specify an Orientation to stack Horizontal or Vertical \$\endgroup\$ – Mathieu Guindon Sep 29 '15 at 18:40
  • \$\begingroup\$ @Mat'sMug: That's a great idea! Once I don't need to know the number of children in advance, I'll generalize this to the StackPanel you described. \$\endgroup\$ – Matthias Braun Sep 29 '15 at 18:50
  • \$\begingroup\$ have a look at implementing a layout manager. I started playing with that this morning and I don't think it requires knowledge of how many components you are going to have. It starts calling methods after all the compents are added and it is trying to decide the width and height \$\endgroup\$ – Robert Snyder Sep 30 '15 at 13:36
2
\$\begingroup\$

as I mentioned in my comment I was playing with a LayoutManager. Based off of what you are hoping to achieve I would suggest going down this path as you are trying to control how the components are laid out which is what the LayoutManager does. Here is a version that works, but could stand to be cleaned up a bit more, and adapted a tad more to cover more scenario's (padding, margins, isVisible...etc)

public class VerticalLayout implements LayoutManager {
    private final Dimension minimumSize = new Dimension();

    @Override
    public void addLayoutComponent(String name, Component comp) {
    }

    @Override
    public void removeLayoutComponent(Component comp) {
    }

    @Override
    public Dimension preferredLayoutSize(Container parent) {
        Dimension dimension = new Dimension(getTotalPaddingWidth(parent), getTotalPaddingHeight(parent));
        minimumSize.height = dimension.height;
        for(Component c :parent.getComponents()){
            if(!c.isVisible()){
                continue;
            }

            Dimension preferredSize = c.getPreferredSize();
            dimension.height += preferredSize.getHeight();
            if(preferredSize.width > dimension.width){
                dimension.width = preferredSize.width;
            }
        }
        minimumSize.width = dimension.width;
        return dimension;
    }

    private int getTotalPaddingWidth(Container container) {
        Insets padding = container.getInsets();
        return padding.left + padding.right;
    }
    private int getTotalPaddingHeight(Container container){
        Insets padding = container.getInsets();
        return padding.top + padding.bottom;
    }
    @Override
    public Dimension minimumLayoutSize(Container parent) {
        return minimumSize;
    }

    @Override
    public void layoutContainer(Container parent) {
        Insets padding = parent.getInsets();
        int y = padding.top;
        for(Component c : parent.getComponents()){
            if(c.isVisible()) {
                c.setBounds(padding.left, y, parent.getWidth(), c.getPreferredSize().height);
                y += c.getHeight();
           }
        }
    }
}
\$\endgroup\$
1
\$\begingroup\$

What you've built here is a wrapper around GridBagLayout. This means you're significantly restricting flexibility when implementing special constraints, because you cannot get into the guts of your LayoutManager by injecting custom GridBagConstraints.

Overall this looks like a badly thought out solution to me...

Especially the reliance on knowing the number of components ahead of time smells to me. Instead of relying on the layout's row-weight you can instead modify your code to give a weighty to only the last element you add.

To give an added component the next y-coordinate you should use:

constraints.gridy = GridBagConstraints.RELATIVE;

See also the relevant javadoc:

Specifies that this component is the next-to-last component in its column or row (gridwidth, gridheight), or that this component be placed next to the previously added component (gridx, gridy).

In the end I achieved the same result as you did by using following layout code:

private static JComponent createVerticalButtons() {
    JPanel pane = new JPanel(new GridBagLayout());

    GridBagConstraints constraints = new GridBagConstraints();
    constraints.gridy = GridBagConstraints.RELATIVE;
    constraints.gridx = 0;
    constraints.fill = GridBagConstraints.HORIZONTAL;
    constraints.weightx = 1.0;
    constraints.anchor = GridBagConstraints.NORTH;

    JButton b1 = new JButton("1");
    JButton b2 = new JButton("2");
    pane.add(b1, constraints);
    constraints.weighty = 1.0;
    pane.add(b2, constraints);

    return pane;
}
\$\endgroup\$
2
  • \$\begingroup\$ Thanks for your suggestions. I tried adding a third button to the pane you defined: constraints.weighty = 2.0; pane.add(new JButton("3"), constraints); But this caused an offset between the second and the third button. \$\endgroup\$ – Matthias Braun Sep 30 '15 at 16:06
  • \$\begingroup\$ I should have been clearer there... for this to work out like you want it to, you may only add a weighty to the last Component you add to the panel \$\endgroup\$ – Vogel612 Sep 30 '15 at 16:08

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.