JPanels don't completely stretch to occupy the available space
Asked Answered
B

3

6

I have a panel where I place several mini-panels, side-by-side, with different sizes and colors, and they should occupy the entire parent panel (horizontally).

For this I use BorderLayout (for the parent panel), and BoxLayout for a sub-panel where I place all the mini-panels (see code below). It does work and behave correctly uppon resizing and everything. However, as the number of mini-panels becomes larger, a strange behaviour occurs: empty space appears at the end of the parent panel.

enter image description here

I think I found that this is a streching bug in the layout managers, because in order to strech the panels, the layout manager tries to add a single pixel to each mini-panel. However, when the number of mini-panels is large, adding a single pixel to every one will result in adding many pixels and going beyond the size of the parent. Thus, the layout manager ends up not adding any pixels to any mini-panel, resulting in the empty space.

Here is my SSCCE: (try running, and streching the window, to understand the problem)

package com.myPackage;

import java.awt.*;
import java.util.Vector;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JPanel;

public class ColoredPanels extends JPanel
{
    /* Content information. */
    private Vector<Integer> partitions;
    private Vector<Color> colors;

    /* Panel where the content panels will go. */
    private JPanel contentHolder;

    private final int defaultHeight = 20;

    public ColoredPanels(Vector<Integer> partitions, Vector<Color> colors)
    {
        assert partitions != null;
        assert !partitions.isEmpty();
        assert colors != null;
        assert !colors.isEmpty();
        assert colors.size() == partitions.size();

        this.partitions = partitions;
        this.colors = colors;

        /* Set layout manager. */
        setLayout(new BorderLayout());

        /* Create the content holder. */
        contentHolder = new JPanel();
        contentHolder.setLayout(new BoxLayout(contentHolder, BoxLayout.X_AXIS));
        this.add(contentHolder, BorderLayout.NORTH);

        /* Fill content holder with colored panels. */
        createPanels();
    }

    private void createPanels()
    {
        assert partitions != null;
        assert !partitions.isEmpty();
        assert colors != null;
        assert !colors.isEmpty();
        assert colors.size() == partitions.size();

        for (int i = 0; i < partitions.size(); i++)
        {
            JPanel newPanel = new JPanel();
            newPanel.setBackground(colors.get(i));
            newPanel.setPreferredSize(new Dimension(partitions.get(i), defaultHeight));
            newPanel.setMinimumSize(new Dimension(1, defaultHeight));
            contentHolder.add(newPanel);
        }
    }

    public static void main(String[] in)
    {
        Vector<Integer> sizes = new Vector<Integer>();
        Vector<Color> cols = new Vector<Color>();

        /* Make 100 random sizes, and use two colors. */
        for (int i = 0; i < 100; i++)
        {
            int size = (int)Math.round(1 + Math.random() * 10);
            sizes.add(size);
            cols.add((i%2 == 0)? Color.red : Color.green);
        }

        ColoredPanels panels = new ColoredPanels(sizes, cols);
        panels.setBorder(BorderFactory.createLineBorder(Color.yellow, 1));

        JFrame newFrame = new JFrame();
        newFrame.getContentPane().add(panels);
        newFrame.pack();
        newFrame.setVisible(true);
    }
}

How do I avoid this behaviour? I want my panels to occupy the whole container.

EDIT: The mini-panels are intended to have (once this is resolved) mouse listeners. Thus, painting solutions are unfortunatelly avoidable.

Basuto answered 9/2, 2012 at 14:48 Comment(2)
interesting ... what do you want to happen? The easiest (most probably not the best :-) way out would be to give all the rounding space to the last ...Ries
I want the mini-panels to completely fill the available space. Giving the remaining to the last is not an option, since partition size-ratios must be respected.Basuto
R
4

Layout problems are solved by ... LayoutManagers :-) If one doesn't what you want it to do, implement the behaviour you want.

So if the core BoxLayout simply ignores pixels due to rounding errors, subclass and make it distribute those pixels as needed. A very raw quick example:

public static class XBoxLayout extends BoxLayout {

    enum Strategy {
        NONE,
        STRETCH_LAST,
        DISTRUBUTE
    }

    private Strategy strategy;

    public XBoxLayout(Container target, int axis, Strategy strategy) {
        super(target, axis);
        this.strategy = strategy;
    }


    @Override
    public void layoutContainer(Container target) {
        super.layoutContainer(target);
        if (Strategy.NONE == strategy) return;
        Insets targetInsets = target.getInsets();
        int targetSize = target.getWidth() - targetInsets.left - targetInsets.right;
        int childSum = 0;
        for (Component child : target.getComponents()) {
            childSum += child.getWidth();
        }
        if (targetSize > childSum) {
            int excess = targetSize - childSum;
            distribute(target, excess);
        }
    }


    private void distribute(Container target, int excess) {
        System.out.println("childCount/rounding excess " + target.getComponentCount() + "/" + excess);
        if (Strategy.STRETCH_LAST == strategy) {
            Component lastChild = target.getComponent(target
                    .getComponentCount() - 1);
            lastChild.setSize(lastChild.getWidth() + excess,
                    lastChild.getHeight());
        } else {
            int firstToDistribute = target.getComponentCount() - excess;
            int summedOffset = 0;
            for(int index = firstToDistribute; index < target.getComponentCount(); index++) {
                Component child = target.getComponent(index);
                Rectangle bounds = child.getBounds();
                bounds.x += summedOffset++;
                bounds.width += 1;
                child.setBounds(bounds);
            }
        }
    }

}
Ries answered 13/2, 2012 at 15:47 Comment(3)
Thank you for your help. That does look like a viable solution, and I was looking for something involving the layout managers. The code doesn't look like it respects component size proportions (in relation with each other), even with the DISTRIBUTION strategy, but I got the point, and will try to implement something in those lines.Basuto
I am going to second this - when you need such custom behavior, doing your own layout manager isn't that painful and you can optimize it for your specific use-case.Cudbear
@Basuto distributing single pixels is .. a matter of strategy :-) Implement whatever you need, at the end of the day it won't be exactly proportional to actual/pref/whatever sizeRies
C
2

Two options I can think of:

  1. Have a custom component that has a paintComponent(...) method that paints all the regions, and when the window is resized it would repaint the regions whilst scaling them according to the new width information.

  2. Paint the coloured regions once to an image, and then paint that, and when the window is resized rescale the image to fit.

Canada answered 9/2, 2012 at 15:0 Comment(3)
Thank you for the help. I intend on adding mouse listeners to the mini-panels. If I use the painting strategy, how could I do that?Basuto
It would be necessary to add the listener to the underlying component (e.g. a panel), then scale the co-ordinates back from the scaled size of the panel.Oshiro
Isn't there an easier solution with layouts ?Basuto
V
1

I once ran into this problem when using GridLayout for a chessboard. My solution was to extend GridLayout and calculate the layout with floating point precision.

Valle answered 9/2, 2012 at 15:41 Comment(1)
Could you give additional insight for that solution?Basuto

© 2022 - 2024 — McMap. All rights reserved.