Search code examples
javaimageswingjframegridbaglayout

Tiny bit of extra space around the JPanel when it should fill the JFrame


I've made an MRE which you can find at the bottom, as well as the image file I'm using. Not sure if it would have been better to just upload my src folder, but someone can let me know if that's okay to do. First off, it does fill the JFrame. However there is a catch. It only fills properly when the contentpane dimensions are, I believe a multiple of 54. I have a picture attached, because it's a lot easier to see then it is to explain.

enter image description here

Now if I resize the JFrame. If I expand the width by one more pixel the JPanel will fill horizontally. Same goes for the Height if I expand the JFrame Vertically by one more pixel the JPanel will fill vertically Like the image below.

enter image description here

The only difference between the two images is that I expanded the height/width of the JFrame by 1 more pixel in the second image

import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;


class UIPanel extends JPanel {
    private BufferedImage[] textures;
    GridBagConstraints c = new GridBagConstraints();
    JLayeredPane jp = new JLayeredPane();
    BufferedImage PewterCityMap;

    UIPanel() {
        this.setLayout(new BorderLayout());
        jp.setLayout(new GridBagLayout());
        c.fill = 1;
        c.weightx = 1;
        c.weighty = 1;
        c.gridwidth = 1;
        c.gridheight = 1;
        try {
            PewterCityMap = ImageIO.read(new File("src/imagesUI/PewterCityMap.jpg"));
            final int width = 16;
            final int height = 16;
            final int rows = 55;
            final int cols = 53;
            textures = new BufferedImage[rows * cols];

            for (int i = 0; i < rows; i++) {
                for (int j = 0; j < cols; j++) {
                    textures[(i * cols) + j] = PewterCityMap.getSubimage(
                            j * width, i * height,
                            width, height
                    );
                }
            }
        } catch (
                IOException ex) {
                  ex.printStackTrace();
        }
        int count = 0;
        for (int i = 0; i < 55; i++) {
            c.gridy = i;
            c.gridx = 0;
            for (int j = 0; j < 53; j++) {
                c.gridx = j;
                    CanEnterLbl canEnterLbl = new CanEnterLbl(new ImageIcon(textures[count]));
                    jp.add(canEnterLbl, c);
                    jp.setLayer(canEnterLbl, 0);
                count++;
            }
        }
        this.add(jp, BorderLayout.CENTER);
    }
    public static void main(String[] args) {
        JFrame jf = new JFrame();
        jf.setLayout(new BorderLayout());
        UIPanel ui = new UIPanel();
        jf.add(ui, BorderLayout.CENTER);
        jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        jf.pack();
        jf.setVisible(true);
    }
}

class CanEnterLbl extends JLabel{
    private Image img;
    CanEnterLbl(ImageIcon imageIcon){
        img = imageIcon.getImage();
        this.setIcon(imageIcon);
    }
    @Override
    protected void paintComponent(Graphics g){
        //In reply to @Frakcool. 
        //Firstly, thank you! Are the next two lines what you were thinking? I 
        doesn't fill correctly when using just this line though 
        //super.paintComponent(g);
        //What I was doing before
        g.drawImage(img, 0, 0, getWidth(), getHeight(), this);
    }
}

Here is the Image file I'm using. I would have used a publicly available image but the image's height & width need to be multiples of 16.
enter image description here


Solution

  • I need the images to resize

    Then you should not use a JLabel to display the image. You don't need all the extra overhead of the JLabel UI.

    Instead you can create a custom component by extending JComponent. Then you customize it's paintComponent() method to scale the image as it is drawn.

    It only fills properly when the contentpane dimensions are, I believe a multiple of 54.

    I would guess a multiple of 53 horizontally and 55 vertically.

    The GridBagLayout will first allocate space to each component at its preferred size.

    Then if there is extra space it will allocate the space to each component based on the weightx/y constraint.

    You can't allocate a fraction of a pixel to a component. So the best that can be done is to allocate a single pixel, which is why you need either 53 or 55 depending on the sizing direction.

    I don't know of any layout manager in the JDK that can prevent the extra space around the panel edges

    A possible solution is to use the Relative Layout. The Relative Layout allows components to be sized relative to the space available. Once components are given there relative size it allows you to specify how "extra" pixels should be allocated.

    To use this approach you would need a main panel that uses the RelativeLayout vertically, followed by child panels for each row that uses the RelativeLayout horizontally.

    So the logic might be something like:

    RelativeLayout verticalLayout = new RelativeLayout(RelativeLayout.Y_AXIS);
    verticalLayout.setRoundingPolicy( RelativeLayout.EQUAL );
    verticalLayout.setFill(true);
    JPanel mapPanel = new JPanel( vertcalLayout );
    
    for (int i = 0; i < rows; i++) 
    {
        RelativeLayout rowLayout = new RelativeLayout(RelativeLayout.X_AXIS);
        rowLayout.setRoundingPolicy( RelativeLayout.EQUAL );
        rowLayout.setFill(true);
    
        JPanel row = new JPanel( rowLayout );
    
        for (int j = 0; j < cols; j++) 
        {
            CanEnterLbl canEnterLbl = new CanEnterLbl(…);
            row.add(canEnterLbl, new Float(1));
        }
    
        mapPanel.add(row, new Float(1));
    }
    

    Note, the RelativeLayout does not calculate an initial preferred size, so you would need to set that so the frame.pack() method works properly.