Benjamin Obeng-Apori
Benjamin Obeng-Apori

Reputation: 13

Moving Graphics in JPanel

I'll like to make an Oval move from one place to the other in a JPanel when a button is clicked. This is the code I came up with. When I click the button however it all happens at once without visible movement the slow from the start to finish seen. The Oval just appears in a new location.

import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.*;
import javax.swing.JPanel;
public class testtest implements ActionListener{
    JButton button;
    MyDrawPanel panel;
    int x = 0;
    int y = 0;
    public static void main(String[]args){
        testtest test = new testtest();
        test.go();
    }
    public void go(){
        JFrame frame = new JFrame("Balloon Balls");
        panel = new MyDrawPanel();
        button = new JButton("Restart");
        button.addActionListener(this);
        panel.add(button);
        frame.setSize(300, 300);
        frame.add(panel);
        frame.setVisible(true);
    }

    @Override
    public void actionPerformed (ActionEvent e){
        for(int i=0;i<130;i++){
            x++;
            y++;
            panel.repaint();
            try {
                Thread.sleep(100);
            } catch(Exception ex) { }
        }
    }
    class MyDrawPanel extends JPanel{
        @Override
        public void paintComponent(Graphics g){
            g.fillOval(x, y, 30, 30);
            g.setColor(Color.BLACK);

        }
    }
}

Upvotes: 1

Views: 596

Answers (3)

MadProgrammer
MadProgrammer

Reputation: 347334

Swing is single thread AND not thread safe.

Using Thread.sleep(100) within the ActionListener is blocking the Event Dispatching Thread, preventing anything from been painted. A new paint pass won't occur until after the actionPerformed method exists.

See Concurrency in Swing for more details.

Swing is also not thread safe, this means you should never make changes to the UI from outside the context of the EDT.

The easiest solution is to make use of a Swing Timer, which will allow to establish regularly timed callbacks, which are executed within the Event Dispatching Thread, but which won't block the EDT.

You're also missing one of the important concepts of OO, encapsulation. The x/y properties should actually be managed by the MyDrawPanel, not testtest

For example...

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.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;

public class testtest implements ActionListener {

    JButton button;
    MyDrawPanel panel;

    public static void main(String[] args) {
        testtest test = new testtest();
        test.go();

    }

    public void go() {
        JFrame frame = new JFrame("Balloon Balls");
        panel = new MyDrawPanel();
        button = new JButton("Restart");
        button.addActionListener(this);
        panel.add(button);
        frame.add(panel);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);

    }

    private Timer timer;

    public void actionPerformed(ActionEvent e) {
        if (timer != null) {
            return;
        }
        timer = new Timer(100, new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent evt) {
                if (panel.update()) {
                    timer.stop();
                    timer = null;
                }
            }
        });
        timer.start();
    }

    class MyDrawPanel extends JPanel {

        private int xPosy = 0;
        private int yPosy = 0;

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(300, 300);
        }

        public boolean update() {
            xPosy++;
            yPosy++;
            repaint();

            return xPosy > getWidth() || yPosy > getHeight();
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);            
            g.fillOval(xPosy, yPosy, 30, 30);
            g.setColor(Color.BLACK);
        }
    }
}

Upvotes: 1

c0der
c0der

Reputation: 18812

In addition to the explanation about Swing thread issues in MadProgrammer's answer I would recommend separating the gui from its control by implementing the MVC Pattern.
This offers better encapsulation, better separation of responsibilities, and makes it easier to use threads for off-edt processing.

Have a model that holds all the information that the view (gui) needs:

/*
 * The model contains the information for the view and information from the view
 * The model is independent of the user interface.
 * It notifies Listener on changes. 
 */
class Model {

    private Listener listener;
    private int x = 0,  y = 0;

    synchronized int getX() {return x;}

    synchronized void setX(int x) { this.x = x; }

    synchronized int getY() {return y;}

    synchronized void setY(int y) { this.y = y; }

    void setListener(Listener listener){
        this.listener = listener;
    }
    //notify listener when changed 
    void notifyListener(){
        if(listener != null) {
            listener.onChange();
        }
    }
}

In this case synchronization was added to allow the model to be used by threads.
Listener is defined by :

/*
* A simple interface used to link View and Model 
*/
interface Listener {
    void onChange();
}

View is just that. It implements Listener so it can listen to Model changes:

/*
 * View is just that: a dumb as possible display 
 */
public class View implements Listener{

    private final JButton button;
    private final MyDrawPanel panel;
    private final Model model;

    public View(Model model) {
        this.model = model;
        panel = new MyDrawPanel();
        button = new JButton("Restart");
        panel.add(button);
    }

    public void go(){
        JFrame frame = new JFrame("Balloon Balls");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(300, 300);
        frame.add(panel);
        frame.setVisible(true);
    }

    class MyDrawPanel extends JPanel{
        @Override
        public void paintComponent(Graphics g){
            super.paintComponent(g); //always call super
            g.fillOval(model.getX(), model.getY(), 30, 30);
            g.setColor(Color.BLACK);
        }
    }

    @Override
    public void onChange() {
        panel.repaint();
    }

    void addActionListener(ActionListener listener){
        button.addActionListener(listener);
    }
}

Putting it all together: see the following mvce : it adds a controller that controls the model and view.
For convenience and simplicity, the following code can be copy-pasted into one file called View.java, and run.

import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;

/*
 * View is just that: a dumb as possible display 
 */
public class View implements Listener{

    private final JButton button;
    private final MyDrawPanel panel;
    private final Model model;

    public View(Model model) {
        this.model = model;
        panel = new MyDrawPanel();
        button = new JButton("Restart");
        panel.add(button);
    }

    public void go(){
        JFrame frame = new JFrame("Balloon Balls");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(300, 300);
        frame.add(panel);
        frame.setVisible(true);
    }

    class MyDrawPanel extends JPanel{
        @Override
        public void paintComponent(Graphics g){
            super.paintComponent(g); //always call super
            g.fillOval(model.getX(), model.getY(), 30, 30);
            g.setColor(Color.BLACK);
        }
    }

    @Override
    public void onChange() {
        panel.repaint();
    }

    void addActionListener(ActionListener listener){
        button.addActionListener(listener);
    }

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

/*
 * A simple interface used to link View and Model 
 */
interface Listener {
    void onChange();
}

/*
 * The model contains the information for the view and information from the view
 * The model is independent of the user interface.
 * It notifies Listener on changes. 
 */
class Model {

    private Listener listener;
    private int x = 0,  y = 0;

    synchronized int getX() {return x;}

    synchronized void setX(int x) { this.x = x; }

    synchronized int getY() {return y;}

    synchronized void setY(int y) { this.y = y; }

    void setListener(Listener listener){
        this.listener = listener;
    }
    //notify listener when changed 
    void notifyListener(){
        if(listener != null) {
            listener.onChange();
        }
    }
}

/*
 * The controller "wires" the view and model, and does the processing.
 */
class Controller implements ActionListener{

    private final Model model;
    private final View view;

    public Controller() {
        model = new Model();
        view = new View(model);
        model.setListener(view);
        view.addActionListener(this);
        view.go();
    }

    @Override
    public void actionPerformed (ActionEvent e){

        new Thread(()->{
            for(int i=0;i<130;i++){
                model.setX(model.getX()+1);
                model.setY(model.getY()+1);
                model.notifyListener();
                System.out.println(model.getX()+" - "+ model.getY());
                try {
                    Thread.sleep(100);
                } catch(Exception ex) { }
            }
        }).start();
    }
}

Upvotes: 0

SirMonkey
SirMonkey

Reputation: 36

paintComponent does just that, it paints the panel. Initially the panel paints the oval at the start x y. You push the button and the window is erased, and repainted at the new XY.

Movement is a concept you'll need to teach the computer. If we update the panel multiple times a second and slowly move the x y, we would make an illusion of movement.

Make a timer that refreshes every 10ms. Each time it refreshes, slightly increment the x and y values and repaint the panel.

Upvotes: 0

Related Questions