1

I want to create a game with a fixed width/heigth ratio for the actual screen (so I can easily just scale the game elements and don't have to bother with complex layout). In order to do so I created a JFrame with a BorderLayout with the actual screen in the center and 4 spacing-panels on the sides. After every resize I recalculate the required spacing and set the preferred sizes accordingly so the screen gets the maximum ractangle of the given ratio that fits in the frame. The following code does that job, I replaced the actual screen with a simple blue panel and also added output to check on the calculation:

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

public class JFrameResize {
  private static JFrame frame;
  private static JPanel inside = new JPanel();
  private static JPanel spacingTop = new JPanel();
  private static JPanel spacingBottom = new JPanel();
  private static JPanel spacingLeft = new JPanel();
  private static JPanel spacingRight = new JPanel();
  private static JPanel compound = new JPanel();

  public static void main(String[] args) {
    SwingUtilities.invokeLater(() -> createAndShowGUI());
  }

  public static void createAndShowGUI() {
    frame = new JFrame();
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    inside.setBackground(Color.BLUE);
    compound.setLayout(new BorderLayout());
    compound.add(inside, BorderLayout.CENTER);
    compound.add(spacingTop, BorderLayout.NORTH);
    compound.add(spacingBottom, BorderLayout.SOUTH);
    compound.add(spacingLeft, BorderLayout.WEST);
    compound.add(spacingRight, BorderLayout.EAST);

    frame.add(compound);

    frame.addComponentListener(new ComponentAdapter() {
      @Override
      public void componentResized(ComponentEvent e) {
        calculateSize();
      }
    });

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

  public static void calculateSize() {
    double width = frame.getContentPane().getWidth();
    double height = frame.getContentPane().getHeight();
    double vSpacing = 0, hSpacing = 0;

    System.out.println("frame: " + width + ":" + height);

    // ratio is hardcoded to 3:4
    if ((width * 3 - height * 4) >= 0) {
      hSpacing = (width - (height * 4) / 3) / 2;
      width = width - 2 * hSpacing;
    } else {
      vSpacing = (height - (width * 3) / 4) / 2;
      height = height - 2 * vSpacing;
    }

    System.out.println("spacing: " + hSpacing + " + " + width + " + " + hSpacing + "  :  "
        + vSpacing + " + " + height + " + " + vSpacing);

    spacingBottom.setPreferredSize(new Dimension((int) width, (int) vSpacing));
    spacingTop.setPreferredSize(new Dimension((int) width, (int) vSpacing));
    spacingLeft.setPreferredSize(new Dimension((int) hSpacing, (int) height));
    spacingRight.setPreferredSize(new Dimension((int) hSpacing, (int) height));
    inside.setPreferredSize(new Dimension((int) width, (int) height));

    frame.revalidate();
    frame.repaint();

    System.out.println("inside: " + inside.getWidth() + ":" + inside.getHeight()
      + ", " + "border: " + spacingLeft.getWidth() + ":"
      + spacingTop.getHeight());
  }
}

This works well for the most time, I get outputs like

frame: 510.0:445.0
spacing: 0.0 + 510.0 + 0.0 : 31.25 + 382.5 + 31.25
inside: 510:385, border: 0:30

and the frame shows the correct rectangle. However if I set the frame to fullscreen I get this output:

frame: 1366.0:705.0
spacing: 213.0 + 940.0 + 213.0 : 0.0 + 705.0 + 0.0
inside: 1366:643, border: 0:31

That is incorrect since my formular calculated the inside to be 940:705 but it became 1312:705. The rectangle also doesn't show correctly anymore. Same goes for using windows + arrow keys or dragging the frame to the screen sides . The calculation input is correct but the repaint/revalidate somehow behaves differently from a normal resize. No different combination of revalidate() and repaint() or spamming them across the code seems to change anything.

How does this happen and how do I fix this? Or is the approach in general flawed and should be replaced?

MDK
  • 499
  • 3
  • 14
  • *"setting preferred sizes"* See [Should I avoid the use of set(Preferred|Maximum|Minimum)Size methods in Java Swing?](http://stackoverflow.com/q/7229226/418556) (Yes.) – Andrew Thompson Nov 22 '19 at 00:19

2 Answers2

3

Don't use a BorderLayout and try to add spacing components.

Instead I would suggest you can set the layout manager of the content panel to be a GridBagLayout. When using the default GridBagConstraints any component added to the GridBagLayout will automatically be centered.

Then for the "inside" panel that you add to the frame you need to override the getPreferredSize() method to calculate the size of the panel.

To determine its preferred size you would get the size of its parent container and then you determine its preferred size based on your rules.

There would be no need for the ComponentListener because the layout manager is automatically invoked whenever the frame is resized.

Simple example to get you started:

import java.awt.*;
import javax.swing.*;

public class JFrameResize
{
  public static void main(String[] args) {
    SwingUtilities.invokeLater(() -> createAndShowGUI());
  }

  public static void createAndShowGUI() {
    JFrame frame = new JFrame();
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    JPanel inside = new JPanel()
    {
        @Override
        public Dimension getPreferredSize()
        {
            Dimension parent = getParent().getSize();
            Dimension child = new Dimension();

            int value = (parent.width * 3) - (parent.height * 4);

            if (value > 0)
            {
                child.height = parent.height;
                child.width = (int)(child.height * 4 / 3);
            }
            else
            {
                child.width = parent.width;
                child.height = (int)(child.width * 3 / 4);
            }

            return child;
        }
    };

    inside.setBackground(Color.BLUE);

    frame.setLayout( new GridBagLayout() );
    frame.add(inside, new GridBagConstraints());
    frame.pack();
    frame.setVisible(true);
  }
}
camickr
  • 321,443
  • 19
  • 166
  • 288
0

Regarding why my approach does not work:

This source from this question (thanks to @Andrew Thompson for linking) states:

[setPreferredSize is] used by LayoutManager as hints to balance the actual layout

So my setPreferredSize was probably overridden by the BorderLayout, in order to make sure the LayoutManager does not interfere you have to overwrite the getPreferredSize

MDK
  • 499
  • 3
  • 14