Search code examples
javamultithreadingswinggraphics2d

How to multithread with paintComponent()?


I created an app that contains a square that bounces every time it touches an edge of the frame.I don't have issues lunching the app,the problem is that i don't know how to create various threads in order to have multiples squares inside the frame. I tried multiple things but i can't figure out where i should create the threads. I also noticed that the square is visible only when i add it directly inside the frame and not when i put it inside a JPanel.

Square.java

public class Square extends JComponent implements ActionListener {

  int width = 20;
  int height = 20;
  double y = Math.random() * 360;
  double x = Math.random() * 360;
  boolean xMax = false;
  boolean yMax = false;
  boolean xMin = true;
  boolean yMin = true;
  Rectangle2D.Double square = new Rectangle2D.Double(x, y, width, height);

  public Square() {
    Timer t = new Timer(2, this);
    t.start();
  }

  public void paintComponent(Graphics g) {
    Graphics2D g2 = (Graphics2D) g;
    super.paintComponent(g);
    g2.setColor(Color.BLUE);
    g2.fill(square);

    x_y_rules();


  }
  public void x_y_rules() {
    if (xMax == true) {
        x = x - 0.5;
        if (x <= 0) {
            xMax = false;
        }
    } else {
        x = x + 0.5;
        if (x >= this.getWidth()) {
            xMax = true;
        }
    }
    if (yMax == true) {
        y = y - 0.5;
        if (y <= 0) {
            yMax = false;
        }
    } else {
        y = y + 0.5;
        if (y >= this.getHeight()) {
            yMax = true;
        }
    }
    square.setFrame(x, y, width, height);
  }

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

App.java

public class App extends JFrame {

public static void main(String[] args) {
    JFrame jf = new JFrame();
    Square sqr = new Square();
    jf.setSize(400, 400);
    jf.setVisible(true);
    jf.add(sqr);
    jf.setDefaultCloseOperation(EXIT_ON_CLOSE);
    jf.setLocationRelativeTo(null);
 }
}

Is it normal that despite i put a time of 2 inside the Timer,the square moves very slowly?


Solution

  • Issues:

    1. you've got program logic, the x_y_rules() method call, inside of the paintComponent method. Get it out as it does not belong there, and instead put it into the Timer's ActionListener code where it belongs.
    2. you can give each Square its own Swing Timer if you want. This isn't really a threading issue since each Timer's ActionListener will run on the EDT.
    3. Two milliseconds is an unrealistic time slice to expect to use in a Swing Timer and no timer will run that fast. 11 to 13 is about the fastest to expect or hope for.
    4. if you want your sprite to move faster, give it a greater value for delta-x and delta-y in your movement code.
    5. Your JComponent has no preferred size defined which is likely why it's not showing up in the JPanel, since the default FlowLayout will size it then to [0, 0]. Override its getPreferredSize() and have it return a reasonable Dimension value.
    6. you're calling setVisible(true) on your JFrame before adding all components, a no-no.

    Ok,i put a getPrefferedSize() inside the square class but i've encountered a problem: the squares are not "together",it's like they're bouncing on separate panels

    Then your program structure is broken. You really don't want create separate Swing components, and in fact your Square class shouldn't extend JComponent or JPanel. Rather

    • Square should be a logical class, one that extends from nothing (other than default Object).
    • Give it a drawing method, say public void draw(Graphics g) {....}
    • Create one class that extends JPanel, say called DrawingPanel, and override its paintComponent method.
    • Give the DrawingPanel class an ArrayList<Square> so that it can hold multiple Square objects.
    • Give the DrawingPanel class a Swing Timer
    • In the DrawingPanel class's Timer, have it update the position of all the Squares in the ArrayList, and then call repaint()
    • In the paintComponent method, iterate through all the Squares in the list, using a for loop, and call each one's draw method.

    For example:

    enter image description here

    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.Graphics;
    import java.awt.Image;
    import java.awt.event.ActionEvent;
    import java.awt.event.ActionListener;
    import java.awt.image.BufferedImage;
    import java.util.ArrayList;
    import java.util.List;
    
    import javax.swing.*;
    
    @SuppressWarnings("serial")
    public class DrawingPanel extends JPanel {
        private static final int PREF_W = 600;
        private static final int PREF_H = PREF_W;
        private static final int TIMER_DELAY = 20;
        private static final Color[] SQUARE_COLOR = { Color.BLUE, Color.CYAN, Color.DARK_GRAY,
                Color.BLACK, Color.GRAY, Color.GREEN, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,
                Color.PINK, Color.RED, Color.YELLOW };
        List<Square> squareList = new ArrayList<>();
    
        public DrawingPanel() {
            // create a bunch of squares
            for (int i = 0; i < SQUARE_COLOR.length; i++) {
                squareList.add(new Square(SQUARE_COLOR[i], PREF_W, PREF_H));
            }
    
            setBackground(Color.WHITE);
    
            // create and start the timer
            new Timer(TIMER_DELAY, new TimerListener()).start();
        }
    
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
    
            // simply draw all the squares in the list
            for (Square square : squareList) {
                square.draw(g);
            }
        }
    
        // set size of JPanel
        @Override
        public Dimension getPreferredSize() {
            if (isPreferredSizeSet()) {
                return super.getPreferredSize();
            }
            return new Dimension(PREF_W, PREF_H);
        }
    
        private class TimerListener implements ActionListener {
            @Override
            public void actionPerformed(ActionEvent e) {            
                // simply iterate through list and move all squares
                for (Square square : squareList) {
                    square.move();
                }
                repaint(); // then repaint the GUI
            }
        }
    
        private static void createAndShowGui() {
            DrawingPanel mainPanel = new DrawingPanel();
    
            JFrame frame = new JFrame("Drawing Panel");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.getContentPane().add(mainPanel);
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        }
    
        public static void main(String[] args) {
            SwingUtilities.invokeLater(() -> createAndShowGui());
        }
    }
    
    // this class does *not* extend JPanel or JComponent
    class Square {
        public static final int WIDTH = 20;
    
        // location of Square
        private double sqrX;
        private double sqrY;
    
        // X and Y speed
        private double deltaX;
        private double deltaY;
    
        // width and height of DrawingPanel JPanel
        private int dpWidth;
        private int dpHeight;
    
        // image to draw
        private Image image;
    
        public Square(Color color, int dpWidth, int dpHeight) {
            this.dpWidth = dpWidth;
            this.dpHeight = dpHeight;
    
            // create square at random location with random speed
            sqrX = Math.random() * (dpWidth - WIDTH);
            sqrY = Math.random() * (dpHeight - WIDTH);
            deltaX = Math.random() * 10 - 5;
            deltaY = Math.random() * 10 - 5;
    
            // one way to draw it is to create an image and draw it
            image = new BufferedImage(WIDTH, WIDTH, BufferedImage.TYPE_INT_ARGB);
            Graphics g = image.getGraphics();
            g.setColor(color);
            g.fillRect(0, 0, WIDTH, WIDTH);
            g.dispose();
        }
    
        public void move() {
    
            // check that we're not hitting boundaries
            if (sqrX + deltaX < 0) {
                deltaX = Math.abs(deltaX);
            }
            if (sqrX + deltaX + WIDTH >= dpWidth) {
                deltaX = -Math.abs(deltaX);
            }
            sqrX += deltaX;
    
            // check that we're not hitting boundaries
            if (sqrY + deltaY < 0) {
                deltaY = Math.abs(deltaY);
            }
            if (sqrY + deltaY + WIDTH >= dpHeight) {
                deltaY = -Math.abs(deltaY);
            }
            sqrY += deltaY;
    
        }
    
        public void draw(Graphics g) {
            int x = (int) sqrX;
            int y = (int) sqrY;
            g.drawImage(image, x, y, null);
        }
    }