paintComponent not drawing everything until repaint() is called

I am working on a chess engine, and I have the following problem: my chess pieces are not drawn unless repaint() is called. However, the squares (= the chess field) appear just fine. I read I should avoid using repaint() inside the paintComponent since this will make the function be called continuously. How can I avoid calling repaint(), but still have my image drawn? This is the code:

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Toolkit;
import java.util.ArrayList;

import javax.swing.JFrame;
import javax.swing.JPanel;

@SuppressWarnings("serial")
public class ChessBoard extends JPanel {

    ArrayList<Square> squares = new ArrayList<Square>();
    Piece piece = new Piece("B", 0);
    
    public ChessBoard() {
        for (int i = 0; i < 8; i++) {
            for (int j = 0; j < 8; j++) {
                squares.add(new Square(i, j));
            }
        }
    }
    
    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        for (Square square : squares) {
            square.draw(g);
        }
        piece.draw(g);
        //repaint(); //Image only appears if this is called
    }
    
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        ChessBoard chessboard = new ChessBoard();
        chessboard.createFrame();
    }
    
    public void createFrame() {
        JFrame f = new JFrame("ChessBoard");
        f.getContentPane().setPreferredSize(new Dimension(squareSize()*8, squareSize()*8));
        f.setResizable(false);
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setTitle("Chessboard");
        f.setLocation((int)(screenSize().width-squareSize()*8)/2, (int)(screenSize().height-squareSize()*8)/2);
        f.add(this);
        f.pack();
        Frame.getFrames();
        f.setVisible(true);
    }
    
    public static Dimension screenSize() {
        return Toolkit.getDefaultToolkit().getScreenSize();
    }
    
    public static int squareSize() {
        return (int)screenSize().height/12;
    }

}


class Square {
    int start_x, start_y;
    int square_size;
    Color color;
    
    public Square(int x, int y) {
        // constructor
        start_x = x*ChessBoard.squareSize();
        start_y = y*ChessBoard.squareSize();
        square_size = ChessBoard.squareSize();
        if ((x+y)%2 == 0) {
            // Color of the white squares
            color = new Color(209, 192, 148);
        } else {
            // Color of the black squares
            color = new Color(110, 83, 43);
        }
    }

    public void draw(Graphics g) {
        g.setColor(this.color);
        g.fillRect(this.start_x, this.start_y, square_size, square_size);
    }
}

class Piece {
    String type;
    int coordinate, square_size, piece_size;
    int[] draw_coordinates = {7, 6, 5, 4, 3, 2, 1, 0};
    Image image;
    
    public Piece(String type, int coordinate) {
        this.type = type;
        this.coordinate = coordinate;
        this.square_size = ChessBoard.squareSize();
        this.piece_size = (int)ChessBoard.squareSize()*2/3;
        this.image = Toolkit.getDefaultToolkit().getImage("src/pieces/black_bishop.png");
    }
    
    public int co_x_board() {
        return coordinate - ((int)coordinate/8)*8;
    }
    
    public int co_y_board() {
        return (int)coordinate/8;
    }
    
    public int co_x_draw() {
        return co_x_board()*square_size+((int)square_size/6);
    }
    
    public int co_y_draw() {
        return draw_coordinates[co_y_board()]*square_size+((int)square_size/6);
    }
    
    public void draw(Graphics g) {
        g.drawString(type, co_x_draw(), co_y_draw()); // does work
        g.drawImage(image, co_x_draw(), co_y_draw(), piece_size, piece_size, null); // does not work
    }
}

Thank you in advance!

Answer

The problem lies in the fact that that you didn’t supply the ImageObserver argument of the drawImage method call.

I can reproduce the error you are seeing in your MRE.

I have worked with reading images via Toolkit.getDefaultToolkit().getImage("..."); (or Toolkit.getDefaultToolkit().createImage("...");) and sometimes it does not work if you don’t supply the ImageObserver, or don’t call for example getWidth(null) on the image before painting it, as well as other symptoms, for which I don’t know the cause. But what I do know is that if you supply the ImageObserver argument then it will work.

Remember, Component is an ImageObserver… So you need to actually supply the Component (to which the Graphics object belongs at) to the drawImage last argument.

So, you can change your Piece#draw method to accept the Component on which it is drawn, like so:

public void draw(Graphics g, final Component observer) {
    g.drawString(type, co_x_draw(), co_y_draw());
    g.drawImage(image, co_x_draw(), co_y_draw(), piece_size, piece_size, observer);
}

Then remember to call it properly, by changing the ChessBoard#paintComponent like so:

@Override
public void paintComponent(Graphics g) {
    super.paintComponent(g);
    for (Square square : squares) {
        square.draw(g);
    }
    piece.draw(g, this);
}

Another way that usually corrects the error, even with null as the ImageObserver is to use an ImageIO#read method to read in the image as a BufferedImage. Again, I don’t know why this works, but it does. I also did not test your case with ImageIO#read, but I still tested it with Toolkit.getDefaultTooklit().getImage("..."); and it works (but you need to supply the ImageObserver argument).

Some suggestions:

In my opinion, it will be far less complicated to lay out some JLabels in a JPanel with GridLayout (for example new GridLayout(0, 8);) and then set the images as Icons (via myLabel.setIcon(new ImageIcon(myImage));) to the JLabels…

Leave a Reply

Your email address will not be published. Required fields are marked *