Search code examples
javamultithreadingswinganimationcustom-painting

Use multiple threads to update GUI


I'm learning to work with java threads, so I decided to make a simple bouncing balls program. However, the program shows multiple threads but only one takes advantage of the window size, other balls are restricted to one area.

I tried setting the size for each balls' JPanel and different layouts which didn't work.

BouncingBall.java

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import javax.swing.*;

public class BouncingBall extends JFrame {

    ArrayList<Ball> balls = new ArrayList<Ball>();

    //GUI Elements
    JLabel lblCount;
    JButton btn= new JButton("Stop");

    BouncingBall() {
//        setDefaultLookAndFeelDecorated(true);
        setTitle("BouncingBall");

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(300, 200);
        for (int i = 0; i < 5; i++) {
            balls.add(new Ball());
        }
        setLayout(new FlowLayout());
        setContentPane(balls.get(0));
        balls.get(0).init();
        for (Ball b : balls
        ) {
            System.out.println(b.getHeight());

            if (b != balls.get(0)) {
                b.init();
                balls.get(0).add(b);
            }
        }

        this.add(btn,BorderLayout.SOUTH);
        btn.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                for (Ball b :balls
                ) {
                    b.stopMoving();
                }
            }
        });
        addMouseListener(new MouseListener() {
            @Override
            public void mouseClicked(MouseEvent e) {
                for (Ball b :balls
                     ) {
                    b.startMoving();
                }
            }

            @Override
            public void mousePressed(MouseEvent e) {
            }

            @Override
            public void mouseReleased(MouseEvent e) {
            }

            @Override
            public void mouseEntered(MouseEvent e) {
            }

            @Override
            public void mouseExited(MouseEvent e) {
            }
        });
        this.setVisible(true);
    }

    public static void main(String[] args) {
        new BouncingBall();
    }
}

Ball.java


import javax.swing.*;
import java.awt.*;

public class Ball extends JPanel implements Runnable {
    // Box height and width
    int width;
    int height;

    // Ball Size
    float radius = 5;
    float diameter = radius * 2;

    // Center of Call
    float X = radius + 50;
    float Y = radius + 20;

    // Direction
    float dx;
    float dy;

    //Vars
    int count = 0;
    float[] colorHSB = new float[3];
    boolean moving = false;

    //Thread
    Thread t;

    Ball() {

        dx = (float) Math.random() * 10;
        dy = (float) Math.random() * 10;
        width = getWidth();
        height = getHeight();

        for (int i = 0; i < 3; i++) {
            colorHSB[i] = (float) Math.random() * 255;
        }
        t = new Thread(this);

    }

    void init() {
        t.start();
    }

    public void run() {
        while (true) {

            width = getWidth();
            height = getHeight();
            if (moving){
                X = X + dx;
                Y = Y + dy;
            }

            if (X - radius < 0) {
                dx = -dx;
                X = radius;
                addCount();
            } else if (X + radius > width) {
                dx = -dx;
                X = width - radius;
                addCount();
            }

            if (Y - radius < 0) {
                dy = -dy;
                Y = radius;
                addCount();
            } else if ((Y + radius) > height) {
                dy = -dy;
                Y = height - radius;
                addCount();
            }
            repaint();

            try {
                Thread.sleep(50);
            } catch (InterruptedException ex) {
            }
        }
    }

    public void startMoving() {
        moving = true;
    }

    public void stopMoving(){
        moving=false;
    }

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        g.setColor(Color.getHSBColor(colorHSB[0], colorHSB[1], colorHSB[2]));
        g.fillOval((int) (X - radius), (int) (Y - radius), (int) diameter, (int) diameter);
    }

    public void addCount() {
        count++;
        System.out.println(count);
    }
}

A photo of the program running

enter image description here

it should show all the balls bouncing around the frame taking advantage of the whole window.


Solution

  • My answer is based on the MCV model. This splits responsibilities between the Model, the View, and the Controller.
    Each one (M,V and C) becomes a well defined single-responsibility class. At first the number of classes, and the relations between them may look puzzling. After studying and understanding the structure you realize that it actually divides the "problem" you are trying to solve into smaller and easier to handle parts.

    The ball can be a simple example of a Model. It is actually a pojo that holds all the information the view needs to draw a ball:

    //a model representing ball
    class Ball  {
    
        //Ball attributes
        private static final int SIZE = 10;  //diameter
        private int x, y;  // Position
        private final Color color;
        private Observer observer;  //to be notified on changes
    
        Ball() {
    
            Random rnd = new Random();
            color = new Color(rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256));
        }
    
        Color getColor() {
            return color;
        }
    
        int getSize(){
            return SIZE;
        }
    
        synchronized int getX() {
            return x;
        }
    
        synchronized void setX(int x) {
            this.x = x;
            notifyObserver();
        }
    
        synchronized int getY() {
            return y;
        }
    
        synchronized void setY(int y) {
            this.y = y;
            notifyObserver();
        }
    
        void registerObserver(Observer observer){
            this.observer = observer;
        }
    
        void notifyObserver(){
            if(observer == null) return;
            observer.onObservableChanged();
        }
    }
    

    Note that you can register an Observer to a Ball. An Observer is defined by:

    //listening interface. Implemented by View and used by Ball to notify changes
    interface Observer {
        void onObservableChanged();
    }
    

    It is used by a Ball to notify the observer that a change has occurred. A Ball has also some synchronized getters and setters so it attributes can be accesses by more than one thread.

    We should also define a Model, another pojo which is the class that encapsulates all the information the view needs:

    //view model: hold info that view needs
    class Model {
    
        private final ArrayList<Ball> balls;
        private final int width, height;
    
        Model(){
            balls = new ArrayList<>();
            width = 300; height = 200;
        }
    
        boolean addBall(Ball ball){
            return balls.add(ball);
        }
    
        List<Ball> getBalls() {
            return new ArrayList<>(balls); //return a copy of balls
        }
    
        int getWidth() {
            return width;
        }
    
        int getHeight() {
            return height;
        }
    }
    

    A View, as its name suggests is just that:

    class View {
    
        private final BallsPane ballsPane;
    
        View(Model model){
            ballsPane = new BallsPane(model);
        }
    
        void createAndShowGui(){
            JFrame frame = new JFrame();
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setLocationRelativeTo(null);
            frame.add(ballsPane);
            frame.pack();
            frame.setResizable(false);
            frame.setVisible(true);
        }
    
        Observer getObserver(){
            return ballsPane;
        }
    }
    
    class BallsPane extends JPanel implements Observer {
    
        private final Model model;
    
        BallsPane(Model model){
            this.model = model;
            setPreferredSize(new Dimension(model.getWidth(), model.getHeight()));
        }
    
        @Override
        public void paintComponent(Graphics g) {
            super.paintComponent(g);
            for(Ball b : model.getBalls()){
                g.setColor(b.getColor());
                g.fillOval(b.getX(), b.getY(), b.getSize(), b.getSize());
            }
        }
    
        @Override
        public void onObservableChanged() {
            repaint(); //when a change was notified
        }
    }
    

    Note that the View (actually BallsPane) implements Observer. It will observe (or listen to) changes in Ball, and respond to every change by invoking repaint().

    Since each Ball has synchronized position (x,y) getters and setters, you can alter those attributes:

    class BallAnimator implements Runnable{
    
        private final Ball ball;
        private final int maxX, maxY;
        private final Random rnd;
        private boolean moveRight = true,  moveDown = true;
        private static final int STEP =1, WAIT = 40;
    
        BallAnimator(Ball ball, int maxX, int maxY) {
            this.ball = ball;
            this.maxX = maxX;
            this.maxY = maxY;
            rnd = new Random();
            ball.setX(rnd.nextInt(maxX - ball.getSize()));
            ball.setY(rnd.nextInt(maxY - ball.getSize()));
            new Thread(this).start();
        }
    
        @Override
        public void run() {
    
            while(true){
    
                int dx = moveRight ? STEP : -STEP ;
                int dy = moveDown  ? STEP : -STEP ;
    
                int newX = ball.getX() + dx;
                int newY = ball.getY() + dy;
    
                if(newX + ball.getSize()>= maxX || newX <= 0){
                    newX = ball.getX() - dx;
                    moveRight = ! moveRight;
                }
    
                if(newY +ball.getSize()>= maxY || newY <= 0){
                    newY = ball.getY() - dy;
                    moveDown = ! moveDown;
                }
    
                ball.setX(newX);
                ball.setY(newY);
    
                try {
                    Thread.sleep(WAIT);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
    


    Adding a Controller and putting it all together: BouncingBalls acts as the controller. It "wires" the different parts of the solution .
    For convenience and simplicity, the entire following code can be copy-pasted into one file called BouncingBalls.java, and run.

    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.Graphics;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Random;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    
    public class BouncingBalls{
    
        BouncingBalls(int numOfBalls) {
    
            Model model = new Model();
            View view = new View(model);;
    
            for (int i = 0; i < numOfBalls; i++) {
                Ball b = new Ball(); //construct  a ball
                model.addBall(b);    //add it to the model 
                b.registerObserver(view.getObserver());  //register view as an observer to it 
                new BallAnimator(b, model.getWidth(), model.getHeight()); //start a thread to update it 
            }
    
            view.createAndShowGui();
        }
    
        public static void main(String[] args) {
            new BouncingBalls(5);
        }
    }
    
    //listening interface. Implemented by View and used by Ball to notify changes
    interface Observer {
        void onObservableChanged();
    }
    
    class View {
    
        private final BallsPane ballsPane;
    
        View(Model model){
            ballsPane = new BallsPane(model);
        }
    
        void createAndShowGui(){
            JFrame frame = new JFrame();
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setLocationRelativeTo(null);
            frame.add(ballsPane);
            frame.pack();
            frame.setResizable(false);
            frame.setVisible(true);
        }
    
        Observer getObserver(){
            return ballsPane;
        }
    }
    
    class BallsPane extends JPanel implements Observer {
    
        private final Model model;
    
        BallsPane(Model model){
            this.model = model;
            setPreferredSize(new Dimension(model.getWidth(), model.getHeight()));
        }
    
        @Override
        public void paintComponent(Graphics g) {
            super.paintComponent(g);
            for(Ball b : model.getBalls()){
                g.setColor(b.getColor());
                g.fillOval(b.getX(), b.getY(), b.getSize(), b.getSize());
            }
        }
    
        @Override
        public void onObservableChanged() {
            repaint(); //when a change was notified
        }
    }
    
    //view model: hold info that view needs
    class Model {
    
        private final ArrayList<Ball> balls;
        private final int width, height;
    
        Model(){
            balls = new ArrayList<>();
            width = 300; height = 200;
        }
    
        boolean addBall(Ball ball){
            return balls.add(ball);
        }
    
        List<Ball> getBalls() {
            return new ArrayList<>(balls); //return a copy of balls
        }
    
        int getWidth() {
            return width;
        }
    
        int getHeight() {
            return height;
        }
    }
    
    //a model representing ball
    class Ball  {
    
        //Ball attributes
        private static final int SIZE = 10;  //diameter
        private int x, y;  // Position
        private final Color color;
        private Observer observer;  //to be notified on changes
    
        Ball() {
    
            Random rnd = new Random();
            color = new Color(rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256));
        }
    
        Color getColor() {
            return color;
        }
    
        int getSize(){
            return SIZE;
        }
    
        synchronized int getX() {
            return x;
        }
    
        synchronized void setX(int x) {
            this.x = x;
            notifyObserver();
        }
    
        synchronized int getY() {
            return y;
        }
    
        synchronized void setY(int y) {
            this.y = y;
            notifyObserver();
        }
    
        void registerObserver(Observer observer){
            this.observer = observer;
        }
    
        void notifyObserver(){
            if(observer == null) return;
            observer.onObservableChanged();
        }
    }
    
    class BallAnimator implements Runnable{
    
        private final Ball ball;
        private final int maxX, maxY;
        private final Random rnd;
        private boolean moveRight = true,  moveDown = true;
        private static final int STEP =1, WAIT = 40;
    
        BallAnimator(Ball ball, int maxX, int maxY) {
            this.ball = ball;
            this.maxX = maxX;
            this.maxY = maxY;
            rnd = new Random();
            ball.setX(rnd.nextInt(maxX - ball.getSize()));
            ball.setY(rnd.nextInt(maxY - ball.getSize()));
            new Thread(this).start();
        }
    
        @Override
        public void run() {
    
            while(true){
    
                int dx = moveRight ? STEP : -STEP ;
                int dy = moveDown  ? STEP : -STEP ;
    
                int newX = ball.getX() + dx;
                int newY = ball.getY() + dy;
    
                if(newX + ball.getSize()>= maxX || newX <= 0){
                    newX = ball.getX() - dx;
                    moveRight = ! moveRight;
                }
    
                if(newY +ball.getSize()>= maxY || newY <= 0){
                    newY = ball.getY() - dy;
                    moveDown = ! moveDown;
                }
    
                ball.setX(newX);
                ball.setY(newY);
    
                try {
                    Thread.sleep(WAIT);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
            }
        }
    }
    


    enter image description here