2

I'm experimenting with a custom LayoutManager and I don't understand the subtleties of using it within a component that itself has an upper-level layout manager.

enter image description here

Below is a test program that makes two frames with pair of JPanels. Each of the two JPanels has a thin black border and uses my WeirdGridLayout to force its child components into squares in a grid, with the JPanel height being computed from the width. Both JPanels are in another JPanel with a thin red border that uses BorderLayout.

In one frame, the JPanels with WeirdGridLayout are arranged EAST and WEST, the other arranged NORTH and SOUTH.

The problem is that in the north/south case, if I change the width/height of the frame, the two JPanels with WeirdGridLayout have the right size, but not the right position (they either have a gap or overlap vertically).

enter image description here

In the east/west case, it just ends up wrong.

enter image description here

What I have to do to get my layout manager to play well with outer layout managers?

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.LayoutManager2;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

/**
 */
public class WeirdGridLayout implements LayoutManager2
{


    static final private int GRIDGAP = 10; 
    static final private int COMPONENT_SIZE = 30;
    static final private int GRIDSPACING = COMPONENT_SIZE + GRIDGAP;

    final private List<Component> components
        = new ArrayList<Component>();

    @Override public void addLayoutComponent(Component comp, Object constraints) {
        this.components.add(comp);
    }
    @Override public void addLayoutComponent(String name, Component comp) {
        this.components.add(comp);
    }
    @Override public void removeLayoutComponent(Component comp) {
        this.components.remove(comp);
    }   

    @Override public float getLayoutAlignmentX(Container target) {
        return Component.LEFT_ALIGNMENT;
    }
    @Override public float getLayoutAlignmentY(Container target) {
        return Component.TOP_ALIGNMENT;
    }

    @Override public void invalidateLayout(Container target) {}
    @Override public Dimension maximumLayoutSize(Container target) {
        return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
    }
    @Override public Dimension minimumLayoutSize(Container parent) {
        return new Dimension(0,0);
    }

    @Override public void layoutContainer(Container parent) {
        int x = GRIDGAP;
        int y = GRIDGAP;

        Dimension d = preferredLayoutSize(parent);
        parent.setSize(d);
        for (Component component : this.components)
        {
            component.setBounds(x, y, COMPONENT_SIZE, COMPONENT_SIZE);

            x += GRIDSPACING;
            if (x >= d.getWidth())
            {
                x = GRIDGAP;
                y += GRIDSPACING;
            }
        }
    }

    @Override public Dimension preferredLayoutSize(Container parent) {
        // how many blocks wide can we fit?
        int n = this.components.size();
        int nblockwidth = (parent.getWidth() - GRIDGAP) / GRIDSPACING;
        int nblockheight = (nblockwidth == 0) ? 0 
                : ((n-1)/nblockwidth) + 1;      
        return new Dimension(
                nblockwidth*GRIDSPACING+GRIDGAP,
                nblockheight*GRIDSPACING+GRIDGAP);
    }

    /* ---- test methods ---- */

    static public class ColorPanel extends JPanel {
        final private Color color;
        final private String label;
        public ColorPanel(String label, Color color) { 
            this.label = label;
            this.color = color; 
        }
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            g.setColor(this.color);
            g.fillRect(0,0,getWidth(),getHeight());
            g.setColor(Color.WHITE);
            FontMetrics fm = g.getFontMetrics();
            int w = fm.stringWidth(this.label);
            g.drawString(this.label, (getWidth()-w)/2,
                    (getHeight()+fm.getAscent())/2);
        } 
    }

    public static void main(String[] args) {
        showFrame(true);
        showFrame(false);
    }
    private static void showFrame(boolean eastWest) {
        JFrame frame = new JFrame("WeirdGridLayout test: eastWest="+eastWest);
        JPanel framePanel = new JPanel(new BorderLayout());
        framePanel.setPreferredSize(new Dimension(400,200));

        JPanel panel[] = new JPanel[2];
        for (int i = 0; i < 2; ++i)
        {
            panel[i] = new JPanel(new WeirdGridLayout());
            panel[i].setBorder(BorderFactory.createLineBorder(Color.BLACK));
            final Random r = new Random();
            for (int j = 0; j < 24; ++j)
            {
                Color c = new Color(
                            r.nextFloat(),
                            r.nextFloat(),
                            r.nextFloat());
                JPanel subpanel = new ColorPanel(Integer.toString(j), c);
                panel[i].add(subpanel);
            }
        }

        framePanel.add(new JButton("test"), BorderLayout.NORTH);
        JPanel bottomPanel = new JPanel(new BorderLayout());
        framePanel.add(bottomPanel, BorderLayout.SOUTH);

        if (eastWest)
        {
            bottomPanel.add(panel[0], BorderLayout.WEST);
            bottomPanel.add(panel[1], BorderLayout.EAST);
        }
        else
        {
            bottomPanel.add(panel[0], BorderLayout.NORTH);
            bottomPanel.add(panel[1], BorderLayout.SOUTH);          
        }
        bottomPanel.setBorder(BorderFactory.createLineBorder(Color.RED));


        frame.setContentPane(framePanel);
        frame.pack();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }
}
Jason S
  • 184,598
  • 164
  • 608
  • 970
  • 1
    Your WeirdGridLayout looks like the ModifiedFlowLayout I posted here: http://stackoverflow.com/questions/3679886/how-can-i-let-jtoolbars-wrap-to-the-next-line-flowlayout-without-them-being-hid – jzd Jun 13 '11 at 19:07
  • Can it be guaranteed that the 2 areas have the same number of boxes? If so, using 2 x `FlowLayout` in a `GridLayout` that is a single column should do the trick. – Andrew Thompson Jun 13 '11 at 19:10
  • @Andrew: thanks, but I'm trying to understand how to implement `LayoutManager`, so even if there's a predefined class to do what I want, it doesn't help me. – Jason S Jun 13 '11 at 19:12
  • Fair enough. Good question, BTW. – Andrew Thompson Jun 13 '11 at 19:15
  • ARGH: I'm going crazy! I've now tried BoxLayout and MigLayout as the outer layouts, and they still have this problem. :-( – Jason S Jun 13 '11 at 21:18

2 Answers2

2

The key is to understand what the top level layout manager is doing and making sure the lower level has a preferred size or other attribute set to have it render correctly.

From the API (http://download.oracle.com/javase/7/docs/api/java/awt/BorderLayout.html):

The NORTH and SOUTH components may be stretched horizontally; the EAST and WEST components may be stretched vertically; the CENTER component may stretch both horizontally and vertically to fill any space left over.

jzd
  • 23,473
  • 9
  • 54
  • 76
  • interesting... Whose responsibility is this? The programmer of a custom LayoutManager, or the programmer of the application that uses a custom LayoutManager? Where can/should setPreferredSize() be called from within a LayoutManager? – Jason S Jun 13 '11 at 19:08
  • The programmer of the application would have to understand how each layout manager works. You can make some ugly GUIs if you combine standard FlowLayout and BorderLayouts, without care. – jzd Jun 13 '11 at 19:10
  • OK, well, in my east/west case, I'd like to split the red-border JPanel horizontally, and let each of my WeirdLayoutManager JPanels have half the total size and determine their appropriate vertical size. Is there a way I can do this using a different top-level manager from BorderLayout? – Jason S Jun 13 '11 at 19:19
  • ...and in my north/south case, it looks like it's splitting the vertical space based on outdated information. – Jason S Jun 13 '11 at 19:20
  • @Jason, I think just a normal two grid GridLayout (either vertical or horizontal) would work for your 2 cases. – jzd Jun 13 '11 at 19:46
  • I just tried it, and it makes the east/west case work as well as the north/south case, but both still have the same problem as with BorderLayout: that it looks like the outer layout is making decisions based on outdated sizing information from the inner container. I can't seem to figure out how to ensure the outer container takes into account inner container changes in size. (I've tried having my layoutmanager call `parent.getParent().invalidate()` on the outer container, no luck) – Jason S Jun 13 '11 at 20:11
  • ...and for north/south GridLayout works for the case where the two are the same size, doesn't work if they have different #s of squares; I can look into GridBagLayout or something but I need to solve the overall issue. :-( – Jason S Jun 13 '11 at 20:12
  • 1
    @Jason - never-ever call setPreferredSize, not even in normal application code. Doing so in a LayoutManager is pure _EVIL_ - these are sizing hints _produced_ by the components and _used_ in the layout – kleopatra Jun 14 '11 at 08:02
2

Answering the subject: none :-) LayoutManager is not designed for interaction with other LayoutManagers, they act in isolation on the target they are responsible for.

  • LayoutManager is responsible for sizing and positioning the children of container, not the container itself. So WeirdLayoutManager is misbehaving in setting the size of the parent to its pref.
  • preferredLayoutSize must return something reasonable always: regard it as kind-of detached, something like "dear container, given you had all the space of the world, what size exactly would you like to have" Or the other way round: don't rely on the parent size to answer the question. It would be like a dog trying to bite into its own tail. For a grid-like structure, that probably requires some kind of prefColumns/-Rows property
  • layoutContainer must size and position the direct children inside the current bounds of the container, don't touch the container itself. It can do so in any way it likes, in as many rows/columns as needed
kleopatra
  • 51,061
  • 28
  • 99
  • 211
  • @mKorbel: If you have a useful answer, why not post it yourself rather than as a comment? – Jason S Jun 14 '11 at 10:22
  • very useful for explaining philosophy. OK, then if the container knows what size it wants, and this has recently changed, how does it pass this on to its own parent? – Jason S Jun 14 '11 at 10:24
  • It sounds like I should venture beyond just using a layout manager and instead use a custom JPanel (with my custom layout manager) which can assert its own min/max/pref size based on the contents.... – Jason S Jun 14 '11 at 10:28
  • @Jason if anything makes the container (inside its responsibility sphere) wanting a different size it should invalidate itself – kleopatra Jun 14 '11 at 12:39