So here is my basic template for Tic Tac Toe:
package myProjects;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JLabel;
import javax.swing.JButton;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.event.*;
public class SecondTickTacToe extends JFrame{
public JPanel mainPanel;
public static JPanel[][] panel = new JPanel[3][3];
public static void main(String[] args) {
new SecondTickTacToe();
}
public SecondTickTacToe(){
this.setSize(310, 400);
this.setTitle("Tic Tac Toe");
this.setLocationRelativeTo(null);
this.setDefaultCloseOperation(EXIT_ON_CLOSE);
mainPanel = new JPanel();
for(int row=0; row<3; row++){
for(int column=0; column<3; column++){
int top=0;
int bottom=0;
int left=0;
int right=0;
panel[row][column] = new JPanel();
panel[row][column].addMouseListener(new Mouse());
panel[row][column].setPreferredSize(new Dimension(90, 90));
panel[row][column].setBackground(Color.GREEN);
if(column==0||column==1)
right = 5;
if(column==1||column==2)
left = 5;
if(row==0||row==1)
bottom = 5;
if(row==1||row==2)
top = 5;
panel[row][column].setBorder(BorderFactory.createMatteBorder(top, left, bottom, right, Color.BLACK));
addItem(panel[row][column], row, column);
}
}
this.add(mainPanel);
this.setVisible(true);
}
private void addItem(JComponent c, int x, int y){
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = x;
gbc.gridy = y;
gbc.weightx = 100.0;
gbc.weighty = 100.0;
gbc.fill = GridBagConstraints.NONE;
mainPanel.add(c, gbc);
}
}
class Mouse extends MouseAdapter{
public void mousePressed(MouseEvent e){
if(e.getSource() instanceof JPanel)
((JPanel)e.getSource()).setBackground(Color.BLUE);
}
}
(You'll have to run the program to see what I'm talking about)
So here is my question:
Is there a way that I can fill in the space between the borders of the JPanels? I want it to be solid. Making the panels bigger doesn't seem to work, and making the border size bigger doesn't seem to do anything either. Does anyone know how to make this work? (My end goal is to make the borders look like a solid tic tac toe template)
mainPanel = new JPanel();
The default layout for a JPanel is a FlowLayout. By default a FlowLayout leaves a 5 pixel gap between all components. If you don't want this gap then you need to change the layout. Read the FlowLayout API and you will find the constructor that allows you to specify a 0 gap.
However, using a FlowLayout may not be the best layout. A GridLayout is the easier layout to use for row and columns. I suggest you read the section from the Swing tutorial on How to Use Grid Layout for more information and examples. There will be no need for your addItem(...) method. When you create the GridLayout with the proper row/columns the components will automatically wrap to a new row so you just add the component to the panel.
mainPanel.add(c, gbc);
By the way, specifying a GridBagConstraint does nothing unless the panel is actually using a GridBagLayout, which is not.
Related
I'm having issues with my code regarding the fact that when I instantiate my City class as an object and add it to the right side of my JSplitPane (or even the left), the circle that is supposed to be drawn is not showing up. My cities class uses paintComponent and should draw a circle just by calling the constructor. I have also tried putting the repaint in its own drawIt() method but the result is still the same. The buttons and spinner show up on the left side of the divider, but the circle I am trying to draw does not show up at all.
Here is my City class.
import javax.swing.*;
import java.awt.*;
public class City extends JPanel{
int xPos, yPos;
City(int x, int y){
xPos = x;
yPos = y;
repaint();
}
public void paintComponent(Graphics g){
super.paintComponent(g);
g.setColor(Color.BLACK);
g.fillOval(xPos, yPos, 10, 10);
}
}
And here is my main.
Here I try to instantiate my city and add it to the right side of the JSplitPane (under Add Components) and that is where I am having issues with, as the black circle will not be drawn on the JSplitPane.
import java.awt.*;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.JSplitPane;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
public class TSP{
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
new TSP();
}
});
}
TSP(){
JLabel instructions = new JLabel("Enter the number of cities: ");
instructions.setBounds(30, 150, 300, 40);
SpinnerNumberModel numMod = new SpinnerNumberModel(2, 2, 10, 1);
JSpinner numOfCities = new JSpinner(numMod);
numOfCities.setBounds(185, 150, 80, 40);
JButton start = new JButton("Start Simulation");
start.setBounds(50, 400, 200, 40);
JFrame frame = new JFrame("Travelling Salesperson");
JSplitPane sp = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT);
JPanel lp = new JPanel(null);
JPanel rp = new JPanel(null);
sp.setDividerLocation(300);
sp.setLeftComponent(lp);
sp.setRightComponent(rp);
sp.setEnabled(false);
frame.setDefaultCloseOperation(3);
frame.setSize(1100,600);
frame.setResizable(false);
////////////////Add Components//////////////////////////
lp.add(instructions);
lp.add(numOfCities);
lp.add(start);
City test = new City(301, 301);
rp.add(test);
frame.add(sp);
////////////////////////////////////////////////////////
frame.setVisible(true);
}
}
I feel like the circle is being drawn under the JSplitPane as if I add my cities object (test) to my frame instead of the JSplitPane(frame.add(test) instead of rp.add(test) under the Add Components section) the black circle will appear in the desired spot but the JSplitPane along with the buttons and spinners will disappear so I feel as if they are conflicting. Is there any fix to this or is there another way altogether to make the circle appear on the right side while the other components are on the left side.
I do not know why it is not drawing the circle on the JSplitPane, but any sort of help would be appreciated. Thanks!
Sorry if anything is unclear or there is any ambiguity in my code, or if I need to post more information as I am quite new to posting here. Let me know if there is anything else I need to add or if there are any questions regarding what I am asking!
EDIT:
It seems there is something blocking where I draw the circle, like another JPanel. Here is an image below. As you can see part of the circle looks as if it is being covered. The small box I drew is the only area that the dot is visible from (everywhere else the circle is covered up by white). Also, the coordinates for the circle in the image below is at (3, 0), i.e City test = new City(3, 0);
I am not quite sure why this is happening though.
the invisible JPanel?
Now that I've seen what you're trying to do, I can provide a more proper answer.
You have a control panel on the left and a drawing panel on the right. Usually, you don't use a JSplitPane to separate the panels. To create your layout, you would add the control panel to the LINE_START of the JFrame BorderLayout and the drawing panel to the CENTER.
The reason for this is that you don't want to constantly recalculate the size of the drawing panel.
So let me show you one way to get a solid start. Here's the GUI I created.
Here are the things I did.
All Swing GUI applications must start with a call to the SwingUtilities invokeLater method. This method ensures that Swing components are created and executed on the Event Dispatch Thread.
I separated the creation of the JFrame, the control panel, and the drawing panel. That way, I could focus on one part of the GUI at a time.
The JFrame methods must be called in a certain order. This is the order that I use for most of my Swing applications.
The JFrame is not sized. It is packed. The Swing layout managers will calculate the size of the components and the JPanels.
I used a FlowLayout and a GridBagLayout to create the control panel. Yes, this looks more complicated than absolute positioning, but in the long run, layout managers allow the GUI to be more flexible.
I used the setPreferredSize method in the drawing panel to set the preferred size of the drawing panel. Because I know the drawing panel size, I can put the first city in the center of the drawing panel.
And here's the code. You don't have to code exactly like this, but this code should give you a good basis to start your project. Take a look at the model / view / controller pattern and see how to further separate your code into smaller pieces that allow you to focus on one part of your application at a time.
I put all the classes in one file to make it easier to paste. You should separate these classes into separate files.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
public class CitySimulation implements Runnable {
public static void main(String[] args) {
SwingUtilities.invokeLater(new CitySimulation());
}
private ControlPanel controlPanel;
private DrawingPanel drawingPanel;
private JFrame frame;
#Override
public void run() {
frame = new JFrame("Traveling Salesperson");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
controlPanel = new ControlPanel();
frame.add(controlPanel.getPanel(), BorderLayout.LINE_START);
drawingPanel = new DrawingPanel();
frame.add(drawingPanel, BorderLayout.CENTER);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public class ControlPanel {
private JPanel panel;
public ControlPanel() {
panel = new JPanel(new FlowLayout());
JPanel mainPanel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.anchor = GridBagConstraints.LINE_START;
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.gridx = 0;
gbc.gridy = 0;
gbc.insets = new Insets(10, 10, 10, 10);
JLabel instructions = new JLabel("Enter the number " +
"of cities:");
mainPanel.add(instructions, gbc);
gbc.gridx++;
gbc.insets = new Insets(10, 0, 10, 10);
SpinnerNumberModel numMod =
new SpinnerNumberModel(2, 2, 10, 1);
JSpinner numOfCities = new JSpinner(numMod);
mainPanel.add(numOfCities, gbc);
gbc.anchor = GridBagConstraints.CENTER;
gbc.gridx = 0;
gbc.gridy++;
gbc.gridwidth = 2;
gbc.insets = new Insets(10, 10, 10, 10);
JButton start = new JButton("Start Simulation");
mainPanel.add(start, gbc);
panel.add(mainPanel);
}
public JPanel getPanel() {
return panel;
}
}
public class DrawingPanel extends JPanel {
private static final long serialVersionUID = 1L;
public DrawingPanel() {
this.setBackground(Color.WHITE);
this.setPreferredSize(new Dimension(400, 400));
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.setColor(Color.BLACK);
g.fillOval(195, 195, 10, 10);
}
}
}
I plan to design a gui program which contains the one outer JPanel which contains a lot of recurring JPanel which has text and button.Yet , I couldn't decide which layouts are suitable for this task.I want it to be like this :
I just copied and pasted first JPanel which will be recurred programmatically like you see in the image.
Which layouts should I use in order to get a result like this ?
In my head it looks like this:
JScrollPane > JPanel (outerPane) > JPanel (innerPane [many])
Based on this, we need to think which layout manager is the outerPane going to use and the innerPanes...
In order to provide spacing between innerPanes I would go for GridLayout (rows, columns, hgap, vgap) like:
GridLayout(0, 1, 5, 5);
Now for each innerPane we could go for GridLayout, GridBagLayout or FlowLayout, let's see what would happen with each:
If we used FlowLayout the components wouldn't be in a "grid" or "table" like layout, so, it's a no no... this is how it would look like:
Altough they seem like what we need, I'm not sure if each label is going to change over time or not, but you could try...
Using GridLayout would make our JButtons to take the whole space of the cell, and it wouldn't look good (at least when resizing it), here's an image with the before and after resizing the GUI to show what I mean (cropped to not use a lot of space in the post):
If your GUI won't resize you can go with this path, if it will, then you should another layout.
GridBagLayout is my favorite in this case because each label will stay in their own column and the buttons won't fill all the available space and our GUI will look more like the image you posted:
In the example above, I used a CustomBorder class to provide spacing between the innerPanes and the outerPane while also providing a a colored border as well as showing the vertical JScrollPane always.
The code that produces those outputs is:
package sof;
import java.awt.Color;
import java.awt.Component;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.geom.Rectangle2D;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.SwingUtilities;
import javax.swing.border.AbstractBorder;
public class NestedPanels {
private JFrame frame;
private JPanel outerPane;
private JPanel innerPane;
private GridBagConstraints gbc;
private JScrollPane scrollPane;
public static void main(String[] args) {
SwingUtilities.invokeLater(new NestedPanels()::createAndShowGui);
}
private void createAndShowGui() {
frame = new JFrame(getClass().getSimpleName());
outerPane = new JPanel();
outerPane.setLayout(new GridLayout(0, 1, 5, 5));
for (int i = 0; i < 5; i++) {
innerPane = new JPanel();
innerPane.setLayout(new GridBagLayout());
gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 0;
gbc.fill = GridBagConstraints.BOTH;
gbc.insets = new Insets(10, 10, 10, 10);
innerPane.add(new JLabel("Recurring JLabel1"), gbc);
gbc.gridx++;
innerPane.add(new JLabel("Recurring JLabel2"), gbc);
gbc.gridx++;
innerPane.add(new JLabel("Recurring JLabel3"), gbc);
gbc.gridx++;
innerPane.add(new JButton("Recurring JButton1"), gbc);
innerPane.setBorder(BorderFactory.createLineBorder(Color.BLACK));
outerPane.add(innerPane);
}
outerPane.setBorder(new CustomBorder(5, Color.BLACK));
scrollPane = new JScrollPane(outerPane, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
frame.add(scrollPane);
frame.pack();
frame.setVisible(true);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
}
#SuppressWarnings("serial")
class CustomBorder extends AbstractBorder {
private int gap;
private Color color;
public CustomBorder(int gap, Color color) {
this.gap = gap;
this.color = color;
}
#Override
public Insets getBorderInsets(Component c) {
return new Insets(gap, gap, gap, gap);
}
#Override
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
super.paintBorder(c, g, x, y, width, height);
Graphics2D g2d = (Graphics2D) g;
g2d.setColor(color);
g2d.draw(new Rectangle2D.Double(x, y, width - 1, height - 1));
}
}
}
Play with the border styles to get the desired one, I painted the border with a -1 pixel on the GUI, if I didn't it would only show left and top borders...
Another option would be using a JTable but I leave that to you
I'm trying to implement a feature that (in my test project) once a button is pressed, it adds a random number to my JPanel. (I use the layouts I have because in my real program, I have more items inside and it displays correctly). But I need my program to recognize when the scrollbar is visible (which I implemented that, but it's a little delay. What I mean by delay is I push the button to add a number, if the scrollbar becomes visible nothing happens. But then the next time I press the button it shifts over like I want). The other problem I have (the one I'm focused on now) is that when I dynamically change the size of the JPanel, if the scrollbar is visible, I have it set to change the width to my width - the width of the scrollbar. But It seems like when the scrollbar is visible, the newly inputted number moves over twice the scrollbar width instead of just once. I've been at this part of my program for over a day and can't figure it out. I'll add my full code and some screenshots.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Random;
import javax.swing.BorderFactory;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.UIManager;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
public class Main {
JFrame frame;
JPanel topPanel;
JPanel memoryPanel;
JScrollPane sPane;
JButton button;
ArrayList<Integer> list = new ArrayList<>();
boolean isVScrollVisible = false;
int scrollBarSize = 0;
public class MyChangeListener implements ChangeListener {
#Override
public void stateChanged(ChangeEvent e) {
isVScrollVisible = (sPane.getVerticalScrollBar().isVisible());
}
}
public class ButtonListener implements ActionListener {
#Override
public void actionPerformed(ActionEvent e) {
Random random = new Random();
int r = random.nextInt(10);
list.add(r);
int n;
if (isVScrollVisible) {
n = scrollBarSize;
} else {
n = 0;
}
JPanel nextPanel = new JPanel();
nextPanel.setName("" + r);
nextPanel.setForeground(Color.BLACK);
nextPanel.setPreferredSize(new Dimension(200 - n, 55));
nextPanel.setMinimumSize(new Dimension(200 - n, 55));
nextPanel.setMaximumSize(new Dimension(200 - n, 55));
JPanel labelPanel = new JPanel();
labelPanel.setLayout(new BorderLayout());
JLabel label = new JLabel();
label.setText("" + r);
label.setPreferredSize(new Dimension(200 - n, 55));
label.setMinimumSize(new Dimension(200 - n, 55));
label.setMaximumSize(new Dimension(200 - n, 55));
label.setHorizontalAlignment(JLabel.RIGHT);
label.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 17));
label.setFont(new Font("Sans-Serif", Font.BOLD, 20));
labelPanel.add(label);
nextPanel.add(labelPanel, BorderLayout.LINE_START);
for (int i = 0; i < memoryPanel.getComponents().length; i++) {
memoryPanel.getComponent(i).setPreferredSize(new Dimension(200 - n, 55));
memoryPanel.getComponent(i).revalidate();
memoryPanel.getComponent(i).repaint();
}
memoryPanel.add(nextPanel, 0);
memoryPanel.revalidate();
memoryPanel.repaint();
sPane.revalidate();
sPane.repaint();
}
}
public Main() {
frame = new JFrame();
topPanel = new JPanel();
memoryPanel = new JPanel();
memoryPanel.setLayout(new BoxLayout(memoryPanel, BoxLayout.Y_AXIS));
sPane = new JScrollPane(memoryPanel, JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
sPane.setPreferredSize(new Dimension(200, 300));
sPane.getViewport().addChangeListener(new MyChangeListener());
scrollBarSize = ((Integer)UIManager.get("ScrollBar.width")) + 1;
button = new JButton("Add Random Number");
button.addActionListener(new ButtonListener());
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
topPanel.add(button);
frame.add(topPanel, BorderLayout.PAGE_START);
frame.add(sPane, BorderLayout.CENTER);
frame.setResizable(false);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(String[] args) {
new Main();
}
}
I need them to look exactly the same. Before I had the code I have now, the scrollbar would appear over the numbers which looked ugly. And the reason I have the frame resizable false is because In my real program I hard coded all the sizes, which in the future I will calculate the correct sizes based on the size of the frame, so right now setting resizable to true is out of the question. Any suggestions on what to do?
This is what I'm trying to accompolish.
Get rid of all the logic that sets the preferred/minimum/maximum sizes. Each component knows what its size should be. Each layout manager will in turn know what the preferred size of the panel should be. Let the layout manager use the information to do its job.
The basic logic for dynamically adding components is:
panel.add(...);
panel.revalidate();
panel.repaint();
Then the scrollbars will appear automatically when required. There is no need for listeners or anything.
Edit:
The reason I set all the sizes is because If I take them out then everything appears centered
Learn how to use layout managers properly and effectively.
For example when using a BoxLayout you can control the alignment of components by using:
component.setAlignmentX(JLabel.RIGHT_ALIGNMENT);
and the component will be aligned to the right edge of the space available to the component.
When using a JLabel you may also need to set a property on the JLabel to align the text to the right edge of the label. Read the JLabel API for the appropriate method.
Trying to learn how to do some elegant and more advanced JFrame layouts. I'm trying to make something I thought would be rather simple, but I'm having some difficulty with it. As opposed to messing around for hours (even though I already have) trying to get the layout to work, I thought I would ask what the best convention for a the layout I'm about to describe would be.
The Layout
Basically, I want 2 columns, but the first column to be wider than the second column. Within the first column there will be just one cell, and that cell will have a JLabel with an icon attached to it, centered in the cell. The second column will have 4 rows, each with a JComponent in it (doesn't matter what). Another key is that every component in the JFrame retains its preferred size, and doesn't stretch to fit its cell or what have you.
Here's a picture of the desired layout:
So far I've thought of doing this a couple different ways:
A BorderLayout, with the JLabel-icon in the center, and a GridLayout/GridBagLayout controlling the rest.
A GridBagLayout 4x4 where the JLabel-icon takes up a 3x4 area, in theory giving it 75% of the space.
Neither give me the results I'm looking for. Thoughts/Suggestions? All help and suggestions are greatly appreciated.
For my own sanity, I would normally separate the individual elements of the layout. This can, sometimes, simplify the process as you only need to focus on the areas of the layout that are important (to each section).
The following example uses a single layout just to demonstrate the power of GridBagLayout
import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class AdvancedLayout {
public static void main(String[] args) {
new AdvancedLayout();
}
public AdvancedLayout() {
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new BorderLayout());
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
private JLabel image;
private JButton button;
private JLabel label;
private JComboBox comboBox;
private JButton otherButton;
public TestPane() {
setLayout(new GridBagLayout());
image = new JLabel();
button = new JButton("A button");
label = new JLabel("A label");
comboBox = new JComboBox();
otherButton = new JButton("Other");
try {
image.setIcon(new ImageIcon(ImageIO.read(new File("/path/to/a/image"))));
} catch (IOException ex) {
ex.printStackTrace();
}
GridBagConstraints gbc = new GridBagConstraints();
gbc.gridx = 0;
gbc.gridy = 0;
gbc.weightx = 0.6666666666666667;
gbc.weighty = 1f;
gbc.gridheight = GridBagConstraints.REMAINDER;
add(image, gbc);
gbc.gridheight = 1;
gbc.gridx++;
gbc.weightx = 0.3333333333333333f;
gbc.weighty = 0.25f;
add(button, gbc);
gbc.gridy++;
add(label, gbc);
gbc.gridy++;
add(comboBox, gbc);
gbc.gridy++;
add(otherButton, gbc);
}
}
}
This could, just as easily, be used with two JPanels, one been for the image and one for the options. This would remove the need to use gridheight...
Your first suggestion is reasonable.
The label can be centered both vertically and horizontally.
For the GridLayout the trick would be to add each component to a JPanel that uses a GridBagLayout with the default gridbag constraints. Then add the panel to the GridLayout. Now, if the frame does resize, the panel will grow, but not the component on the panel and the component will be centered in the panel.
Instead of using the GridLayout with other sub panels, you can also use a vertical BoxLayout and add "glue" before/after every component. Not that with this approach you will need to make sure every component uses an alignment that is centered. Also, you may need to set the maximum size of some components equal to the preferred size of the component so it doesn't grow when space is available.
I'm having what seems to me should be a really simple issue. In my opinion it's just a massive oversight of the people who made GridBagLayout...
Anyway. I'm using GridBagLayout to display a 27x13 grid of tiles for the game I'm making. I used this layout because of its ability to resize the components and because of how easy it is to configure, but there's just one small problem. When it's resizing the tiles, if the width is not a multiple of 27 or if the height is not a multiple of 13, there will be white space put around the borders.
To show what I mean:
Here is what it looks like when I resize the frame so that the JPanel within is sized 864x416, perfect multiples of 27 and 13.
Here is what it looks like when I resize the frame so that the JPanel within is sized 863x415, just barely not multiples of 27 or 13.
It simply is not distributing the extra pixels among the tiles. I don't know why. When I fiddle with the minimum/maximum/preferred size using their respective methods or overriding them, or even use GridBagLayout's ipadx and ipady constraints, I can remove the white space - but all it does is just squish the outermost tiles to fit the rest. You can see this in the example code below.
Here's an SSCCE:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.util.Random;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class Game extends JPanel {
public static void main(String[] args) {
javax.swing.SwingUtilities.invokeLater(new Runnable() {
public void run() {
start();
}
});
}
static void start() {
JFrame frame = new JFrame("Game");
JPanel newFrame = new MainGameScreen();
frame.getContentPane().add(newFrame);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
class MainGameScreen extends JPanel {
public MainGameScreen() {
setPreferredSize(new Dimension(864, 551));
setLayout(new GridBagLayout());
setBackground(Color.green);
GridBagConstraints gbc = new GridBagConstraints();
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1;
gbc.weighty = 1;
gbc.ipadx = 0; //Change to 64 to see undesired effects
gbc.ipady = 0; //^
for (int i=0;i<13;i++) {
for (int j=0;j<27;j++) {
gbc.gridx = j;
gbc.gridy = i;
add(new ImagePanel(), gbc);
}
}
}
}
class ImagePanel extends JComponent {
private int r,g,b;
public ImagePanel() {
Random generator = new Random();
r = generator.nextInt(100)+1;
g = generator.nextInt(100)+1;
b = generator.nextInt(100)+1;
}
#Override
public void paintComponent(Graphics gr) {
super.paintComponent(gr);
gr.setColor(new Color(r,g,b));
gr.fillRect(0, 0, getWidth(), getHeight());
}
}
My question is, how can I make the layout constantly look like the first image? Do I need a different layout? I'm very lost here.
I would use a GridLayout for the panel with square tiles, and nest that inside another layout (either GridBagLayout, BorderLayout, BoxLayout, or some combination of layouts) for the rest of the window.
That said, both GridLayout and GridBagLayout will distribute pixels evenly in this case. Since you can only use whole pixels, you'll be left with a remainder after dividing up the the space, and that will be used as padding. This is largely unavoidable, although there are a few potential workarounds:
enforce certain window sizes
draw a larger panel than you need, but use a viewport so the extra tiles are truncated in the view
add other elements bordering the tiled area to effectively hide the fact that there's extra padding
If you're okay with the first option, you can make a slight modification to snap the window to a compatible size, such that there is no remainder (note: I also changed a couple of your hard-coded ints to constants):
package com.example.game;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.util.Random;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class Game extends JPanel {
public static void main(String[] args) {
javax.swing.SwingUtilities.invokeLater(new Runnable() {
public void run() {
start();
}
});
}
static void start() {
final JFrame frame = new JFrame("Game");
JPanel newFrame = new MainGameScreen();
frame.addComponentListener(new ComponentAdapter() {
#Override
public void componentResized(ComponentEvent e) {
int h = frame.getContentPane().getHeight() % MainGameScreen.ROWS;
int w = frame.getContentPane().getWidth() % MainGameScreen.COLS;
// Subtract the remainder pixels from the size.
frame.setSize(frame.getWidth() - w, frame.getHeight() - h);
}
});
frame.getContentPane().add(newFrame);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
class MainGameScreen extends JPanel {
static final int ROWS = 13;
static final int COLS = 27;
public MainGameScreen() {
setPreferredSize(new Dimension(864, 551));
setLayout(new GridBagLayout());
setBackground(Color.green);
GridBagConstraints gbc = new GridBagConstraints();
gbc.fill = GridBagConstraints.BOTH;
gbc.weightx = 1;
gbc.weighty = 1;
gbc.ipadx = 0; //Change to 64 to see undesired effects
gbc.ipady = 0; //^
for (int i=0;i<ROWS;i++) {
for (int j=0;j<COLS;j++) {
gbc.gridx = j;
gbc.gridy = i;
add(new ImagePanel(), gbc);
}
}
}
}
class ImagePanel extends JComponent {
private int r,g,b;
public ImagePanel() {
Random generator = new Random();
r = generator.nextInt(100)+1;
g = generator.nextInt(100)+1;
b = generator.nextInt(100)+1;
}
#Override
public void paintComponent(Graphics gr) {
super.paintComponent(gr);
gr.setColor(new Color(r,g,b));
gr.fillRect(0, 0, getWidth(), getHeight());
}
}