Show ScrollBars in FlowLayout only when necessary - java

I will rephrase my question:**
How to prevent Java ScrollBar from being enabled in FlowLayout when there is enough space to show all items by warping them.
**
Here a screenshot of what I am trying to achieve:
Note that scrollbar is disabled when it is not necessary.
And when you resize the window scrollbar should appear if some items are out the viewplane
P.S. I am aware of things called documentation and web.

Your updated screen shots suggest that you may be looking for Wrap Layout, which "extends the FlowLayout" in a way that "will result in synchronizing the preferred size of the container with the layout of the container." See also Creating a Custom Layout Manager.

The default scroll bar policy is as needed, but I remember having to account for the FlowLayout gaps to get an even number initially. If you stretch this example out, you'll see the horizontal scroll bar disappear.
Update: It doesn't really fix your problem, but it shows how I implemented Scrollable. I wanted to get rid of setPreferredSize(), even tho' the image is just a placeholder. The Wrap Layout article talks about why FlowLayout works the way it does.
import javax.swing.*;
import java.awt.*;
import java.util.Random;
public class ImageScrollTest extends JPanel implements Scrollable {
private static final int N = 8;
private static final int W = 120;
private static final int H = 100;
private final FlowLayout layout = new FlowLayout();
private final int hGap = layout.getHgap();
private final int vGap = layout.getVgap();
private final Dimension size;
// Show n of N images in a Scrollable FlowLayout
public ImageScrollTest(int n) {
setLayout(layout);
for (int i = 0; i < N; i++) {
this.add(new ImagePanel());
}
size = new Dimension(n * W + (n + 1) * hGap, H + 2 * vGap);
}
#Override
public Dimension getPreferredScrollableViewportSize() {
return size;
}
#Override
public int getScrollableUnitIncrement(
Rectangle visibleRect, int orientation, int direction) {
return getIncrement(orientation);
}
#Override
public int getScrollableBlockIncrement(
Rectangle visibleRect, int orientation, int direction) {
return getIncrement(orientation);
}
private int getIncrement(int orientation) {
if (orientation == JScrollBar.HORIZONTAL) {
return W + hGap;
} else {
return H + vGap;
}
}
#Override
public boolean getScrollableTracksViewportWidth() {
return false;
}
#Override
public boolean getScrollableTracksViewportHeight() {
return false;
}
private static class ImagePanel extends JPanel {
private static final Random rnd = new Random();
private Color color = new Color(rnd.nextInt());
public ImagePanel() {
this.setBackground(color);
this.setBorder(BorderFactory.createLineBorder(Color.blue));
}
#Override
public Dimension getPreferredSize() {
return new Dimension(W, H);
}
}
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
createAndShowGUI();
}
});
}
private static void createAndShowGUI() {
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
ImageScrollTest ist = new ImageScrollTest(N / 2);
JScrollPane sp = new JScrollPane(ist);
sp.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_NEVER);
f.add(sp);
f.pack();
f.setVisible(true);
}
}

You shoud specify to the scrollPane when you want the scrollBars to apprear (possible in both horizontal and vertical direction):
myScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
There is also: HORIZONTAL_SCROLLBAR_ALWAYS and HORIZONTAL_SCROLLBAR_NEVER if you need that anytime soon.
EDIT: For more information, and exactly my answer: http://docs.oracle.com/javase/tutorial/uiswing/components/scrollpane.html

Related

JPanel not showing in JFrame, but JFrame still changes size

I don't know what I did, or what went wrong, but a change I made at some point in the last while has made my JPanel completely invisible. The JFrame it's nested in still changes in size to house it, and I can still toggle the content in the combobox.
In my desperation, I tried replacing the content of the SnakeSettingsPanel class with a single button, but the same thing happened - completely invisible, yet I can still interact with it. I figured it might be a computer error, so I tried restarting, but still nothing. When I tried adding a button to the frame outside of the JPanel, it worked just fine. What am I doing wrong?
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.*;
public class SnakeSettingsPanel extends JPanel {
public boolean quit = false;
public boolean play = false;
public int width = 20;
public int height = 15;
public Speed speed = Speed.SLOW;
public JTextField wField;
public JTextField hField;
public JComboBox<Speed> sField;
public static void main(String[] args) {
JFrame jf = new JFrame();
jf.setTitle("Snake");
SnakeSettingsPanel settings = new SnakeSettingsPanel();
jf.add(settings);
jf.pack();
jf.setVisible(true);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
public SnakeSettingsPanel() {
setLayout(new GridBagLayout());
// #author Create our labels.
JLabel wLabel = new JLabel("Width:");
JLabel hLabel = new JLabel("Height:");
JLabel sLabel = new JLabel("Speed:");
GridBagConstraints p = new GridBagConstraints();
p.gridx = 0;
p.gridy = 0;
p.insets = new Insets(10, 10, 10, 10);
// #author Create the buttons, and add listeners
JButton y = new JButton("Play");
JButton n = new JButton("Quit");
y.addActionListener(new PlayListener());
n.addActionListener(new QuitListener());
// #author Create text fields for height/width
wField = new JTextField(15);
wField.setText("20");
hField = new JTextField(15);
hField.setText("15");
// #author Creates a combobox for selecting speed.
Speed[] speeds = {Speed.SLOW, Speed.MEDIUM, Speed.FAST};
sField = new JComboBox<Speed>(speeds);
// #author Stitch everything into the panel.
add(wLabel, p);
p.gridx = 1;
add(wField, p);
p.gridx = 0;
p.gridy = 1;
add(hLabel, p);
p.gridx = 1;
add(hField, p);
p.gridx = 0;
p.gridy = 2;
add(sLabel, p);
p.gridx = 1;
add(sField, p);
p.gridx = 0;
p.gridy = 3;
add(y, p);
p.gridx = 1;
add(n, p);
setVisible(true);
}
public boolean getPlay() {
return play;
}
public boolean getQuit() {
return quit;
}
// #author Returns all settings as a SnakeSettings object
public SnakeSettings getSettings() {
return new SnakeSettings(width, height, speed);
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public Speed getSpeed() {
return speed;
}
// #author Sends out the word to start a new game.
public class PlayListener implements ActionListener {
public void actionPerformed(ActionEvent e) {
quit = false;
play = true;
width = Integer.parseInt(wField.getText());
height = Integer.parseInt(hField.getText());
speed = (Speed) sField.getSelectedItem();
}
}
// #author Sends out the word to shut down the program.
public class QuitListener implements ActionListener {
public void actionPerformed(ActionEvent e) {
quit = true;
play = false;
}
}
}
Let this be a lesson on why you should avoid mixing Model (your application's data) with View (how it is displayed). Your SnakeSettingsPanel is currently both.
As Model, it contains 3 important fields: width, height, and speed.
As View, it is a full JPanel. JPanels have a lot of fields which you should avoid touching directly. Including width and height, usually accessed via getHeight and getWidth -- which you are overwriting with a version that always returns the same built-in values of 20 and 15 (until the user changes those values through a UI that they cannot see).
The fast fix is to rename your current getWidth() and getHeight() to avoid clashing with the built-in getWidth() and getHeight() methods of the parent JPanel class. Call them getMyWidth(), getMyHeight(), and suddenly everything works.
The better fix is to remove those fields and methods entirely, and store your own model attributes in a SnakeSettings attribute. Update it when the user clicks on play, and return it when it is requested via getSettings(). Less code for you, less chance of accidental name clashes with your parent JPanel class. This would look like:
// SnakeSettingsPanel, before
public int width = 20;
public int height = 15;
public Speed speed = Speed.SLOW;
public int getWidth() { // <-- clashes with superclass
return width;
}
public int getHeight() { // <-- clashes with superclass
return height;
}
public Speed getSpeed() {
return speed;
}
public SnakeSettings getSettings() {
return new SnakeSettings(width, height, speed);
}
// SnakeSettingsPanel, after
SnakeSettings settings = new SnakeSettings();
public SnakeSettings getSettings() {
return settings;
}

How can I add a rectangle in BorderLayout.SOUTH?

I am trying to add a thing like this in my music player application in swing.
I tried to add a rectangle to BorderLayout.SOUTH, but it never appeared on screen. Here is what I did:
public class MyDrawPanel extends JPanel {
#Override
public void paintComponent(Graphics g) {
g.setColor(Color.GREEN);
g.fillRect(200,200,200,200);
}
public static void main(String[] args) {
JFrame frame = new JFrame();
MyDrawPanel a = new MyDrawPanel();
frame.getContentPane().add(BorderLayout.SOUTH,a);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(1000,1000);
frame.setVisible(true);
}
}
I just did not try 200,200,200,200, but I tried a lot of values, even with the help of a for loop, but it never appeared on screen. If I used CENTER instead of SOUTH it appeared. I read the documentation to check how fillRect works, but it simply said it added x+width and y+height. The point (0,0) is the top left corner. I checked that by adding a rectangle to CENTER layout. How cam I do it?
I did not share the output, because it was just a blank screen.
The values you give to fillRect are wrong. The first two are the top left corner's coordinates, relative to the component you're painting in; in your case the MyDrawPanel. With the code you posted, this drawing area is outside of the container the panel is placed in. You want to do
g.fillRect(0,0,200,200);
A note: You usually want to call frame.pack() after you've finished adding all components, so it can layout itself. In your case, this results in a tiny window because the system doesn't know how large it should be. You probably want to add a method
public Dimension getPreferredSize() {
System.out.println("getting pref size");
return new Dimension(200, 200);
}
to ensure it's always large enough to draw the full rectangle.
Also, you should call frame.getContentPane().setLayout(new BorderLayout()) before. You can print it out without setting it to see it is not the default. EDIT: As VGR points out, the documentation says that it is in fact a BorderLayout. I cannot confirm that is the case - it is in fact a RootLayout. That seems to behave like a BorderLayout though.
I thought this might make a quick little project. Here's the level meter I came up with.
The important parts are the DrawingPanel and the LevelMeterModel. The DrawingPanel takes the information from the LevelMeterModel and paints the bars on a JPanel.
The LevelMeterModel is an int array of levels, a minimum level, and a maximum level. The maximum level could be calculated, but I assumed music has a certain volume and frequency range.
The JFrame holds the DrawingPanel. A Swing Timer varies the levels somewhat randomly. The random numbers are in a small range so the bar heights don't change abruptly.
Here's the complete runnable code.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class LevelMeterGUI implements Runnable {
public static void main(String[] args) {
SwingUtilities.invokeLater(new LevelMeterGUI());
}
private final DrawingPanel drawingPanel;
private final LevelMeterModel model;
public LevelMeterGUI() {
this.model = new LevelMeterModel();
this.drawingPanel = new DrawingPanel(model);
}
#Override
public void run() {
JFrame frame = new JFrame("Level Meter GUI");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(drawingPanel, BorderLayout.CENTER);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
System.out.println("Frame size: " + frame.getSize());
Timer timer = new Timer(250, new ActionListener() {
#Override
public void actionPerformed(ActionEvent event) {
model.setRandomLevels();
drawingPanel.repaint();
}
});
timer.start();
}
public class DrawingPanel extends JPanel {
private static final long serialVersionUID = 1L;
private final int drawingWidth, drawingHeight, margin, rows;
private final Dimension barDimension;
private final LevelMeterModel model;
public DrawingPanel(LevelMeterModel model) {
this.model = model;
this.margin = 10;
this.rows = 20;
this.barDimension = new Dimension(50, 10);
int columns = model.getLevels().length;
drawingWidth = columns * barDimension.width + (columns + 1) * margin;
drawingHeight = rows * barDimension.height + (rows + 1) * margin;
this.setBackground(Color.WHITE);
this.setPreferredSize(new Dimension(drawingWidth, drawingHeight));
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
int maximum = model.getMaximumLevel();
double increment = (double) maximum / rows;
int peak = rows * 75 / 100;
int x = margin;
for (int level : model.getLevels()) {
int steps = (int) Math.round((double) level / increment);
int y = drawingHeight - margin - barDimension.height;
for (int index = 0; index < steps; index++) {
if (index < peak) {
g.setColor(Color.GREEN);
} else {
g.setColor(Color.RED);
}
g.fillRect(x, y, barDimension.width, barDimension.height);
y = y - margin - barDimension.height;
}
x += margin + barDimension.width;
}
}
}
public class LevelMeterModel {
private final int minimumLevel, maximumLevel;
private int[] levels;
private final Random random;
public LevelMeterModel() {
this.minimumLevel = 100;
this.maximumLevel = 999;
this.levels = new int[8];
this.random = new Random();
setRandomLevels();
}
public void setRandomLevels() {
for (int index = 0; index < levels.length; index++) {
levels[index] = getRandomLevel(levels[index]);
}
}
private int getRandomLevel(int level) {
if (level == 0) {
return random.nextInt(maximumLevel - minimumLevel) + minimumLevel;
} else {
int minimum = Math.max(level * 90 / 100, minimumLevel);
int maximum = Math.min(level * 110 / 100, maximumLevel);
return random.nextInt(maximum - minimum) + minimum;
}
}
public int[] getLevels() {
return levels;
}
public int getMinimumLevel() {
return minimumLevel;
}
public int getMaximumLevel() {
return maximumLevel;
}
}
}

GUI (view) start up is messed up

I previously had help with creating the GUI (Thank you #Hovercraft Full Of Eels).
Initially my GUI looked like this when start up.
and when you run the program, it looks like this.
Now after I edited it, it's start up looks like this.(Messed up)
and then when you run the program it looks like this.
Basically the startup screen looks messed up but when you press the button, it reverts back to normal but the background looks messed up.
Here is how the code looks like
The Main class:
public static void main(String[] args) {
JFrame f = new JFrame("Envrionment Stimulation");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.getContentPane().add(input.getGui()); //input is a class that takes the size of the grid and uses it to call the Layout class
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
}
This is the Layout class that input calls after getting the inputs for it
public class Layout extends JPanel {
private static final int WIDTH = 75;
private static final int SPACING = 15;
private final GridControls btnAndTxt;
private final Grid map;
public Layout(Earth earth, int xCount, int yCount) {
//Making our borders
setBorder(BorderFactory.createEmptyBorder(SPACING, SPACING, SPACING,SPACING));
setLayout(new BorderLayout(SPACING, SPACING));
//The Map
map = new Grid(WIDTH, xCount, yCount);
//Buttons and Text
btnAndTxt = new GridControls(map, earth);
//Adding the JPanels.
add(btnAndTxt, BorderLayout.PAGE_START);
add(map, BorderLayout.CENTER);
}
}
Next will be the Grid which is how our GUI will be initialized
public class Grid extends JPanel {
private final JLabel[][] label;
private final int xCount, yCount;
public Grid(int width, int xCount, int yCount) {
this.xCount = xCount;
this.yCount = yCount;
setBackground(Color.BLACK);
setBorder(BorderFactory.createLineBorder(Color.BLACK));
setLayout(new GridLayout(yCount, xCount, 1, 1));
label = new JLabel[yCount][xCount];
for (int y = 0; y < yCount; y++)
for (int x = 0; x < xCount; x++) {
label[y][x] = new JLabel(".", SwingConstants.CENTER);
label[y][x].setPreferredSize(new Dimension(width, width));
label[y][x].setBackground(Color.GREEN);
label[y][x].setOpaque(true);
label[y][x].setFont(new Font("Ariel", Font.PLAIN, 25));
add(label[y][x]);
}
}
public void updateGrid(Mappable[][] map) {
for (int y = 0; y < yCount; y++)
for (int x = 0; x < xCount; x++) {
label[y][x].setText(map[y][x] == null ? "." : map[y][x].toString());
}
}
public int getY() {
return yCount;
}
public int getX() {
return xCount;
}
}
For the GridControl class, it extends Jpanel. I used add() method for Button and Text (Button, Text, Button) order but I did not use any layout managers. Could this be the problem? The Initial GUI did not utilize layout manager for the Buttons and Text panel as well. (Layout class stayed the same)
*Edit1: Updated how grid looks like
I had getX() and getY() method while the class was extending JPanel. It override the methods that's why it was causing errors on the GUI.

Force space between Panels to be 0 using BoxLayout?

Currently as part of a project I'm working on, I am implementing a component which can be used to visualize a permutation of bits (as part of a cryptographic algorithm). I am doing so by creating two rows of "pins" and connecting them by drawing lines between the tips, creating a sort of web between them.
An important part of this is that I am using this visualization both on its own as well as a part of other visualizations (for example, I may want to include S-Boxes) and therefore I need to be able to turn the pins on and off. My solution to this was using JPanels to put the rows of pins into a header and footer panel which can be made invisible.
I am laying them out vertically in a BoxLayout, but I end up with space between them, even if I add glue above the header and below the footer.
My example looks like this when initialized:
And when I resize it, they come together a bit, but still only touch on one side:
I'm guessing this is some sort of silly mistake converting my user space into device space in terms of component size and layout, but for the life of me I cannot find it. This is my code, although I apologize for the mess:
import java.awt.BasicStroke;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Ellipse2D;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class PermutationWeb extends JPanel
{
private static enum EndPanelType
{
HEADER, FOOTER
}
private final JPanel header;
private final JPanel mainPanel;
private final JPanel footer;
private double widthFactor;
private double heightFactor;
private int widthMax;
private int heightMax;
private int[] indexMappings;
private Point2D.Double[] endpoints;
private Line2D.Double[] drawingLines;
public PermutationWeb(int indices, boolean endPanelsOn)
{
super();
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
widthMax = (indices + 1)*2;
heightMax = (int)Math.round(widthMax*(3.0/17.0));
widthFactor = 1;
heightFactor = 1;
endpoints = new Point2D.Double[indices * 2];
drawingLines = new Line2D.Double[indices];
for(int i=0; i<indices; i++)
{
endpoints[i] = new Point2D.Double(i*2+2, 0);
endpoints[i+indices] = new Point2D.Double(i*2+2, heightMax);
drawingLines[i] = new Line2D.Double();
}
header = new WebEndPanel(EndPanelType.HEADER);
mainPanel = new WebMainPanel();
footer = new WebEndPanel(EndPanelType.FOOTER);
add(Box.createVerticalGlue());
add(header);
add(mainPanel);
add(footer);
add(Box.createVerticalGlue());
setEndPanelsOn(endPanelsOn);
}
public Point2D getEndpoint(int index)
{
return endpoints[index];
}
public void updateMappings(int[] mappings)
{
this.indexMappings = mappings;
for(int i=0; i<indexMappings.length; i++)
{
drawingLines[i].setLine(endpoints[i], endpoints[indexMappings.length + indexMappings[i]]);
}
//paint();
}
public void setEndPanelsOn(boolean endPanelsOn)
{
header.setVisible(endPanelsOn);
footer.setVisible(endPanelsOn);
}
#Override
public Dimension getMaximumSize()
{
int height = mainPanel.getHeight();
if(header.isVisible())
{
height += (header.getHeight() * 2);
}
int width = mainPanel.getWidth();
return new Dimension(width, height);
}
#Override
public Dimension getPreferredSize()
{
return getMaximumSize();
}
public static void main(String[] args)
{
JFrame jf = new JFrame();
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setSize(800, 600);
int[] mappings = {0,4,8,12,1,5,9,13,2,6,10,14,3,7,11,15};
PermutationWeb webTest = new PermutationWeb(16, true);
jf.add(webTest);
jf.setVisible(true);
webTest.setVisible(true);
webTest.updateMappings(mappings);
System.out.printf("Header: [%s]\nMainPanel: [%s]\nFooter: [%s]\n",
webTest.header.getSize().toString(),
webTest.mainPanel.getSize().toString(),
webTest.footer.getSize().toString());
}
private class WebMainPanel extends WebSubPanel
{
private static final double HEIGHT_RATIO = 0.25;
#Override
public void paintComponent(Graphics g)
{
Graphics2D g2 = (Graphics2D)g;
super.paintComponent(g2);
scaleTo(getSize());
g2.scale(widthFactor, widthFactor);
g2.setStroke(new BasicStroke((float)(2.0/widthFactor)));
for(Line2D line: drawingLines)
{
g2.draw(line);
}
}
#Override
public Dimension getMaximumSize()
{
return new Dimension(MAX_WIDTH_PX, (int)(MAX_WIDTH_PX*HEIGHT_RATIO));
}
}
private class WebEndPanel extends WebSubPanel
{
private static final double HEIGHT_RATIO = 0.125;
private static final double PIN_RADIUS = 0.5;
private final EndPanelType endType;
private Line2D.Double[] edgeLines;
private Ellipse2D.Double[] pinHeads;
public WebEndPanel(EndPanelType endType)
{
super();
this.endType = endType;
this.edgeLines = new Line2D.Double[endpoints.length/2];
this.pinHeads = new Ellipse2D.Double[endpoints.length/2];
for(int i=0; i<edgeLines.length; i++)
{
Point2D pointA;
Point2D pointB;
if(EndPanelType.HEADER.equals(this.endType))
{
pointA = new Point2D.Double(i*2+2, 4);
pointB = new Point2D.Double(i*2+2, 2);
pinHeads[i] = new Ellipse2D.Double(
pointB.getX()-PIN_RADIUS,
pointB.getY()-PIN_RADIUS*2,
PIN_RADIUS*2,
PIN_RADIUS*2);
}
else // FOOTER
{
pointA = new Point2D.Double(i*2+2, 0);
pointB = new Point2D.Double(i*2+2, 2);
pinHeads[i] = new Ellipse2D.Double(
pointB.getX()-PIN_RADIUS,
3-PIN_RADIUS*2,
PIN_RADIUS*2,
PIN_RADIUS*2);
}
edgeLines[i] = new Line2D.Double(pointA, pointB);
}
}
#Override
public Dimension getMaximumSize()
{
return new Dimension(MAX_WIDTH_PX, (int)(MAX_WIDTH_PX*HEIGHT_RATIO));
}
#Override
public void paintComponent(Graphics g)
{
Graphics2D g2 = (Graphics2D)g;
super.paintComponent(g2);
scaleTo(getSize());
g2.scale(widthFactor, widthFactor);
g2.setStroke(new BasicStroke((float)(2.0/widthFactor)));
for(Line2D line: edgeLines)
{
g2.draw(line);
}
for(Ellipse2D pin: pinHeads)
{
g2.draw(pin);
}
}
}
private abstract class WebSubPanel extends JPanel
{
protected static final int MAX_WIDTH_PX = 800;
public WebSubPanel()
{
super();
setBorder(null);
setLayout(new FlowLayout(FlowLayout.CENTER, 0, 0));
}
public void scaleTo(Dimension d)
{
widthFactor = d.getWidth() / (double)widthMax;
heightFactor = d.getHeight() / (double)heightMax;
}
#Override
public Dimension getPreferredSize()
{
return getMaximumSize();
}
}
}
The ultimate goal here is a resizable web where the header and footer WebEndPanels can be invisible, but have 0 space between them and the WebMainPanel when shown (as though they were a single entity).
A BoxLayout will resize a component up to its maximum size if space is available.
So you first need to implement the getPreferredSize() method of the component so it can be packed as displayed at its normal size.
Then if it has the ability to grow (and your custom painting supports this) you override the getMaximumSize() method to return the size.
So the painting needs to be based on the actual size of the panel if you want it to be contiguous with other panels.

Java Layout Proportions: Creating a scalable square Panel

I am making a GUI component to represent something like a Chess board in a window. Normally it will be a grid of 8x8 squares, although some variants require a 10x8 board etc. The first step is to make a panel that contains a grid of 8x8 components.
The class Board extends JPanel and uses a GridLayout to model a grid of 8x8 components. In an effort to get something done these are simply of class Square which extends JButton. The trouble is that they're not squares!
The Board has been added to a freshly instantiated JFrame, packed and rendered on the screen. Of course, right now the board takes up the entire frame as it is resized by the user. The grid scales with the board and this distorts the squares into rectangles.
This is not entirely undesired behaviour. I would like the board to scale with the frame. However, I would like to ensure that the squares remain square at all times. The board could be rectangular (10x8) but should maintain a fixed proportion.
How do I get square squares?
You can choose to use a LayoutManager that honors the preferred size of the cells instead. GridLayout will provide a equal amount of the available space to each cell, which doesn't appear to be quite what you want.
For example, something like GridBagLayout
public class TestChessBoard {
public static void main(String[] args) {
new TestChessBoard();
}
public TestChessBoard() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception ex) {
}
JFrame frame = new JFrame("Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new ChessBoardPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class ChessBoardPane extends JPanel {
public ChessBoardPane() {
int index = 0;
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
for (int row = 0; row < 8; row++) {
for (int col = 0; col < 8; col++) {
Color color = index % 2 == 0 ? Color.BLACK : Color.WHITE;
gbc.gridx = col;
gbc.gridy = row;
add(new Cell(color), gbc);
index++;
}
index++;
}
}
}
public class Cell extends JButton {
public Cell(Color background) {
setContentAreaFilled(false);
setBorderPainted(false);
setBackground(background);
setOpaque(true);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(25, 25);
}
}
}
Updated with proportional example
Now, if you want to do a proportional layout (so that each cell of the grid remains proportional to the other regardless of the available space), things begin to get ... fun ...
public class TestChessBoard {
public static void main(String[] args) {
new TestChessBoard();
}
public TestChessBoard() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception ex) {
}
JFrame frame = new JFrame("Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestChessBoard.ChessBoardPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class ChessBoardPane extends JPanel {
public ChessBoardPane() {
int index = 0;
setLayout(new ChessBoardLayoutManager());
for (int row = 0; row < 8; row++) {
for (int col = 0; col < 8; col++) {
Color color = index % 2 == 0 ? Color.BLACK : Color.WHITE;
add(new TestChessBoard.Cell(color), new Point(col, row));
index++;
}
index++;
}
}
}
public class Cell extends JButton {
public Cell(Color background) {
setContentAreaFilled(false);
setBorderPainted(false);
setBackground(background);
setOpaque(true);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(25, 25);
}
}
public class ChessBoardLayoutManager implements LayoutManager2 {
private Map<Point, Component> mapComps;
public ChessBoardLayoutManager() {
mapComps = new HashMap<>(25);
}
#Override
public void addLayoutComponent(Component comp, Object constraints) {
if (constraints instanceof Point) {
mapComps.put((Point) constraints, comp);
} else {
throw new IllegalArgumentException("ChessBoard constraints must be a Point");
}
}
#Override
public Dimension maximumLayoutSize(Container target) {
return preferredLayoutSize(target);
}
#Override
public float getLayoutAlignmentX(Container target) {
return 0.5f;
}
#Override
public float getLayoutAlignmentY(Container target) {
return 0.5f;
}
#Override
public void invalidateLayout(Container target) {
}
#Override
public void addLayoutComponent(String name, Component comp) {
}
#Override
public void removeLayoutComponent(Component comp) {
Point[] keys = mapComps.keySet().toArray(new Point[mapComps.size()]);
for (Point p : keys) {
if (mapComps.get(p).equals(comp)) {
mapComps.remove(p);
break;
}
}
}
#Override
public Dimension preferredLayoutSize(Container parent) {
return new CellGrid(mapComps).getPreferredSize();
}
#Override
public Dimension minimumLayoutSize(Container parent) {
return preferredLayoutSize(parent);
}
#Override
public void layoutContainer(Container parent) {
int width = parent.getWidth();
int height = parent.getHeight();
int gridSize = Math.min(width, height);
CellGrid grid = new CellGrid(mapComps);
int rowCount = grid.getRowCount();
int columnCount = grid.getColumnCount();
int cellSize = gridSize / Math.max(rowCount, columnCount);
int xOffset = (width - (cellSize * columnCount)) / 2;
int yOffset = (height - (cellSize * rowCount)) / 2;
Map<Integer, List<CellGrid.Cell>> cellRows = grid.getCellRows();
for (Integer row : cellRows.keySet()) {
List<CellGrid.Cell> rows = cellRows.get(row);
for (CellGrid.Cell cell : rows) {
Point p = cell.getPoint();
Component comp = cell.getComponent();
int x = xOffset + (p.x * cellSize);
int y = yOffset + (p.y * cellSize);
comp.setLocation(x, y);
comp.setSize(cellSize, cellSize);
}
}
}
public class CellGrid {
private Dimension prefSize;
private int cellWidth;
private int cellHeight;
private Map<Integer, List<Cell>> mapRows;
private Map<Integer, List<Cell>> mapCols;
public CellGrid(Map<Point, Component> mapComps) {
mapRows = new HashMap<>(25);
mapCols = new HashMap<>(25);
for (Point p : mapComps.keySet()) {
int row = p.y;
int col = p.x;
List<Cell> rows = mapRows.get(row);
List<Cell> cols = mapCols.get(col);
if (rows == null) {
rows = new ArrayList<>(25);
mapRows.put(row, rows);
}
if (cols == null) {
cols = new ArrayList<>(25);
mapCols.put(col, cols);
}
Cell cell = new Cell(p, mapComps.get(p));
rows.add(cell);
cols.add(cell);
}
int rowCount = mapRows.size();
int colCount = mapCols.size();
cellWidth = 0;
cellHeight = 0;
for (List<Cell> comps : mapRows.values()) {
for (Cell cell : comps) {
Component comp = cell.getComponent();
cellWidth = Math.max(cellWidth, comp.getPreferredSize().width);
cellHeight = Math.max(cellHeight, comp.getPreferredSize().height);
}
}
int cellSize = Math.max(cellHeight, cellWidth);
prefSize = new Dimension(cellSize * colCount, cellSize * rowCount);
System.out.println(prefSize);
}
public int getRowCount() {
return getCellRows().size();
}
public int getColumnCount() {
return getCellColumns().size();
}
public Map<Integer, List<Cell>> getCellColumns() {
return mapCols;
}
public Map<Integer, List<Cell>> getCellRows() {
return mapRows;
}
public Dimension getPreferredSize() {
return prefSize;
}
public int getCellHeight() {
return cellHeight;
}
public int getCellWidth() {
return cellWidth;
}
public class Cell {
private Point point;
private Component component;
public Cell(Point p, Component comp) {
this.point = p;
this.component = comp;
}
public Point getPoint() {
return point;
}
public Component getComponent() {
return component;
}
}
}
}
}
This got a bit long, so here is the quick answer: You can't maintain a square board with square squares given your board dimensions (8x8, 10x8) and fully fill the screen if the user can resize it. You should limit the size of the board so that it maintains the aspect ratio even if that means you have some blank space in your frame. OK, read on for the long-winded explanation...
There are two ways you can make this work. Either you can limit the possible sizes of the JFrame, or you can limit the size of your Board so it doesn't always fill the frame. Limiting the size of the board is the more common method, so let's start with that.
Option 1: Limiting the Board
If you are working with a fixed set of board dimensions (8x8, 10x8, and a couple others maybe), and assuming each square has some minimum size (1 pixel squares on a chess board don't sound too practical), there are only so many frame dimensions that the board can fully fill. If your frame is 80pixels by 80pixels, your 8x8 board fits perfectly. But as soon as the user resizes to something like 85x80 you're stuck because you can't fully fill that while maintaining squares with the board dimensions you gave.
In this case you want to leave 5 pixels empty, whether it's 5 above or below, or 2.5 above and below, or whatever, doesn't matter. This should sound familiar - it's an aspect ratio problem and basically why you can get black bars on the edges of your TV depending on TV vs. movie dimensions.
Option 2: Limiting the Frame
If you want the board to always fully fill the frame, probably not what you want, then you have to adjust the size of the frame after a user resizes it. Say you are using a 10x8 board, and the user sets the frame to 107x75. That's not too bad, and with a little math you can figure out 100x80 is your closest aspect ratio that works, and fix the window. It will probably a bit frustrating for the user if the window keeps jumping around on them though, especially if they tried to make it something way off like 50x200.
Last thoughts / Example
Limiting the board is most likely the correct solution. Everything from games to desktop apps follows that principle. Take the ribbon in MS Office products for example. As you make the window larger, the ribbon will expand (maintaining its proportions) until it hits it max size, and then you just get more space for your document. When you make the window smaller the ribbon gets smaller (again maintaining its proportions) until it hits a minimum size and then you start losing parts of it (remember, don't want 1x1 squares on your board).
On the other hand you can prevent the user from resizing the window at all. I'm pretty sure this is how MineSweeper works (don't have it on this computer to double check), and may be a better/easier solution for what you need.

Categories