Search code examples
javaswingrepaint

Repainting-Thread doesn't repaint Inner-Class JPanel


I want to make a little rain program in swing, but for some reason I cannot repaint the panel from another class. I tried using an inner class for the panel this time, but it doesn't seem to work with repainting it from another class/thread. Does someone know why?

sscce:

import javax.swing.JPanel;
import javax.swing.Timer;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.JFrame;

public class UI extends JFrame {

    public static void main(String[] args) {
        UI myProgram = new UI();
        myProgram.setVisible(true);
    }
    
    public UI() {
        this.setSize(new Dimension(500,300));
        this.setBackground(Color.WHITE);
        this.setDefaultCloseOperation(EXIT_ON_CLOSE);
        UserPanel p = new UserPanel(this);
    }
    
    public class UserPanel extends JPanel implements ActionListener {
        
        private Timer time = new Timer(1, this);
        private UI myFrame;

        public UserPanel(UI myFrame) {
            this.myFrame = myFrame;
            this.setSize(myFrame.getSize());
            
            time.start();
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            repaint();
        }

        @Override
        public void paintComponent(Graphics g) {
            super.paintComponent(g);
            System.out.println("painting");
            g.setColor(Color.BLACK);
            g.fillRect(this.getWidth()/2, this.getHeight()/2, 50,50);
        }
    }
}

UI Class (with inner class JPanel):

package Rain;

import javax.swing.JPanel;
import javax.swing.Timer;

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.ArrayList;
import java.util.Random;

import javax.swing.JFrame;

public class UI extends JFrame {

    
    public UI() {
        this.setSize(new Dimension(500,300));
        this.setBackground(Color.WHITE);
        this.setDefaultCloseOperation(EXIT_ON_CLOSE);
        UserPanel p = new UserPanel(this);
    }


    private class UserPanel extends JPanel implements ActionListener {
        
        private Timer time = new Timer(1, this);
        private UI myFrame;
        private ArrayList<Raindrop> rain = new ArrayList<Raindrop>();
        private static final int AMOUNT = 50;
        private Random rand = new Random();

        public UserPanel(UI myFrame) {
            this.myFrame = myFrame;
            this.setSize(myFrame.getSize());
            
            for(int i = 0; i < AMOUNT; i++) {
                createRain();
            }
            new Painter(this);
            time.start();
        }

        public void createRain() {
            float distance = rand.nextFloat() * 90 + 10;
            int x = rand.nextInt(this.getWidth());
            int y = 100;
            
            rain.add(new Raindrop(distance,x,y));
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            System.out.println("tick");
            for(Raindrop r : rain) {
                r.fall();
            }
        }

        public void paintComponent(Graphics g) {
            System.out.println("painting");
            g.setColor(this.getBackground());
            g.fillRect(0,0,this.getWidth(),this.getHeight());
            for(Raindrop r : rain) {
                r.draw(g);
            }
        }

    }

}

Painter:

package Rain;

import javax.swing.JPanel;

public class Painter extends Thread {

    private JPanel p;

    public Painter(JPanel p) {
        this.p = p;
        this.start();
    }

    public void run() {
        while(true) {
            System.out.println("trying to paint..");
            p.repaint();
        }
    }

}

Console Output:

trying to paint..

tick

trying to paint..

tick

...

Expected Output:

trying to paint..

painting

tick

trying to paint..

...

The thread does work but it never calls the paintComponent(Graphics g) function in the panel


Solution

  • All Swing applications must run on their own thread, called EDT. (Hopefully, you start your application by calling SwingUtilities#invokelater method). So, repainting a component outside of Event Dispatch Thread is really bad bad (bad) idea. Instead of creating new Thread, repaint the component inside javax.swing.Timer's action listener since it will run in EDT.

    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("tick");
        for(Raindrop r : rain) {
            r.fall();
        }
        repaint(); //repaint in EDT
    }
    

    Also, when you @Override paintComponent method, always start by calling super.paintComponent(g);

    public void paintComponent(Graphics g) {
        super.paintComponent(g);//let component get painted normally
        System.out.println("painting");
        g.setColor(this.getBackground());
        g.fillRect(0,0,this.getWidth(),this.getHeight());
        for(Raindrop r : rain) {
            r.draw(g);
        }
    }
    

    UPDATE after your SSCCE

    In order a component to get painted, it must have a parent. You UserPanel p = new UserPanel(this); but you never add it to the frame:

    UserPanel p = new UserPanel(this);
    getContentPane().setLayout(new BorderLayout());
    getContentPane().add(p);
    

    The complete SSCCE:

    public class UI extends JFrame {
    
        public static void main(String[] args) {
            SwingUtilities.invokeLater(() -> { //Run in EDT
                UI myProgram = new UI();
                myProgram.setVisible(true);
            });
        }
    
        public UI() {
            super("title");//call super for frame
            this.setSize(new Dimension(500, 300));
            this.setBackground(Color.WHITE);
            this.setDefaultCloseOperation(EXIT_ON_CLOSE);
            UserPanel p = new UserPanel(this);
    
            //Use border layout to make p fit the whole frame
            getContentPane().setLayout(new BorderLayout());
            getContentPane().add(p, BorderLayout.CENTER);
        }
    
        public class UserPanel extends JPanel implements ActionListener {
    
            private Timer time = new Timer(1, this);
            private UI myFrame;
    
            public UserPanel(UI myFrame) {
                this.myFrame = myFrame;
                this.setSize(myFrame.getSize());
    
                time.start();
            }
    
            @Override
            public void actionPerformed(ActionEvent e) {
                repaint();
            }
    
            @Override
            public void paintComponent(Graphics g) {
                super.paintComponent(g);
                System.out.println("painting");
                g.setColor(Color.BLACK);
                g.fillRect(this.getWidth() / 2, this.getHeight() / 2, 50, 50);
            }
        }
    }
    

    Don't ignore the SwingUtilities.invokeLater.