I'm trying to make a tic-tac-toe game with A.I in java, but I'm new to Swing and graphics with Java. First off, I can't change the background color of my JPanel even when I set Opaque true
(see class ttt
below). I was however able to change the background of the JFrame (see class tttFrame
). Secondly, when I try to add JPanels (these panels are where the X and O's will go) to the top of my current JPanel - the one with the lines (or maybe it does, but the background color of the JPanels don't change) they won't show. The testpanel
and the panels in tttGrid
both don't show up. I'm assuming I got the wrong idea of how to implement JFrames and JPanels.
My code also throws a nullpointerexception at this.add(tttGrid[r][c]);
. tttGrid
is basically a 2D array of JPanels that are intended to display X or O when clicked by the user/computer. I'm not sure how my code leads to the nullpointerexception. I did some testing as you'd notice on the lines before this.add(tttGrid[r][c]);
, I get this output on the console window:
actual label, row 0 col 0
actual label, row 0 col 1
actual label, row 0 col 2
null label, row 1 col 0
null label, row 1 col 1
null label, row 1 col 2
null label, row 2 col 0
null label, row 2 col 1
null label, row 2 col 2
import java.awt.*;
import java.util.*;
import javax.swing.*;
public class ttt extends JPanel{
private char playerChoice;
private JPanel[][] tttGrid;
public ttt(){
//this.setOpaque(true); Why isn't setBackground working in JPanel and
//this.setBackground(Color.BLUE); only working in JFrame class?
this.setPreferredSize(new Dimension(500,500));
this.setFocusable(true);
tttGrid = new JPanel[3][3];
for(int y = 50; y <= 290; y+=120){
int r = 0;
int c = 0;
for(int x = 70; x <= 310; x+=120){
tttGrid[r][c] = new JPanel();
tttGrid[r][c].setBounds(x,y,120,120);
tttGrid[r][c].setBackground(Color.GREEN);
tttGrid[r][c].setOpaque(true);
c++;
}
r++;
}
for(int r = 0; r < 3; r++){
for(int c = 0; c < 3; c++){
if(tttGrid[r][c] == null){
System.out.println("null panel, row " + r + " col " + c);
}
else{
System.out.println("real panel, row " + r + " col " + c);
}
this.add(tttGrid[r][c]); //nullpointerexception
}
}
//testing if JPanel is visible
JPanel testpanel = new JPanel();
testpanel.setBounds(70,50,120,120);
testpanel.setBackground(Color.MAGENTA);
testpanel.setOpaque(true);
this.add(testpanel);
}
public void paint(Graphics g){
Graphics2D g2d = (Graphics2D) g;
g2d.setColor(new Color(153, 250, 255));
g2d.setStroke(new BasicStroke(5));
g2d.drawLine(70, 170, 430, 170);
g2d.drawLine(70, 290, 430, 290);
g2d.drawLine(190, 50, 190, 410);
g2d.drawLine(310, 50, 310, 410);
}
public void gameStart(){
}
public void drawXO(){
}
}
import javax.swing.*;
import java.awt.*;
public class tttFrame extends JFrame{
public tttFrame(){
this.getContentPane().add(new ttt());
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setTitle("Tic-Tac-Toe");
ImageIcon icon = new ImageIcon("ttt.png");
this.setIconImage(icon.getImage());
this.setBackground(new Color(0,225,237)); //Background color only works on JFrame
this.setResizable(false);
this.pack();
this.setLocationRelativeTo(null);
this.setVisible(true);
}
public static void main(String[] args) {
new tttFrame();
}
}
The photo below is what I get when I comment out the code this.add(tttGrid[r][c]);
So, several compounding issues.
First, your NPE is been caused by the fact you are resetting the r
value on each iteration of your compounded for-loop
s
for (int y = 50; y <= 290; y += 120) {
int r = 0;
int c = 0;
for (int x = 70; x <= 310; x += 120) {
tttGrid[r][c] = new JPanel();
tttGrid[r][c].setBounds(x, y, 120, 120);
tttGrid[r][c].setBackground(Color.GREEN);
tttGrid[r][c].setOpaque(true);
c++;
}
r++;
}
In this case, r
is ALWAYS 0
Next, you're fighting the layout manager of the panel. A JPanel
by default makes use of a FlowLayout
. Setting the bounds
of a component in this case will have no effect and the FlowLayout
manager will attempt to make use of the components preferredSize
to determine how best it should be laid out.
This would have produced an undesirable result for you, expect ...
Next, you're fighting the paint subsystem.
By overriding paint
you are taking over complete control of how the component, and its children, are painted. If you don't want that much control, you should first call super.paint
to ensure that the paint chain is maintained.
However, as a general rule, you should really consider overriding paintComponent
instead (just don't forget to call super.paintComponent
first)
Take a look at:
for more details.
The following example makes use of GridBagLayout
. This is a some what more complicated layout manager and you could get a similar result using a GridLayout
, but I like the flexibility that GridBagLayout
offers me.
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class Test {
public static void main(String[] args) {
new Test();
}
public Test() {
SwingUtilities.invokeLater(new Runnable() {
@Override
public void run() {
new tttFrame();
}
});
}
public class tttFrame extends JFrame {
public tttFrame() {
this.getContentPane().add(new ttt());
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setTitle("Tic-Tac-Toe");
// ImageIcon icon = new ImageIcon("ttt.png");
// this.setIconImage(icon.getImage());
this.setBackground(new Color(0, 225, 237)); //Background color only works on JFrame
// this.setResizable(false);
this.pack();
this.setLocationRelativeTo(null);
this.setVisible(true);
}
}
public class ttt extends JPanel {
private char playerChoice;
private JPanel[][] tttGrid;
public ttt() {
setLayout(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
tttGrid = new JPanel[3][3];
gbc.gridy = 0;
gbc.fill = gbc.BOTH;
gbc.insets = new Insets(5, 5, 5, 5);
for (int row = 0; row < 3; row++) {
gbc.gridx = 0;
for (int col = 0; col < 3; col++) {
CellPanel panel = new CellPanel();
add(panel, gbc);
tttGrid[row][col] = panel;
gbc.gridx += 1;
}
gbc.gridy += 1;
}
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setColor(new Color(153, 250, 255));
g2d.setStroke(new BasicStroke(5));
int cellSize = 120 + 10;
int boardSize = cellSize * 3;
int x = (getWidth() / 2) - (cellSize / 2);
int y = (getHeight() / 2) - (boardSize / 2);
g2d.drawLine(x, y, x, y + boardSize);
x += cellSize;
g2d.drawLine(x, y, x, y + boardSize);
x = (getWidth() - boardSize) / 2;
y = (getHeight() / 2) - (cellSize / 2);
g2d.drawLine(x, y, x + boardSize, y);
y += cellSize;
g2d.drawLine(x, y, x + boardSize, y);
}
public void gameStart() {
}
public void drawXO() {
}
}
// You don't "need" to do this, but it ensures that each instance
// is always the same
public class CellPanel extends JPanel {
public CellPanel() {
setBackground(Color.GREEN);
}
@Override
public Dimension getPreferredSize() {
return new Dimension(120, 120);
}
}
}
One thing that this example does do, is demonstrates how flexible a layout manager can be, for example, the window is now re-sizable
You could, with minimal effort, have the cells fill ALL of the available space as the window is resized, but, that's up to you