I realize this is an old question, but it's been viewed over 1,000 times.
When creating a Swing GUI, it's a good idea to use the model / view / controller (MVC) pattern. This pattern allows us to separate our concerns and focus on one part of the GUI at a time. If there's a problem, then we have a good idea where in the code the problem lies.
Here's the GUI I created.
So, let's start with the model. Here's the Box
class.
public class Box {
private final Color color;
private final Point boxMotion;
private final Rectangle rectangle;
private final String name;
public Box(String name, Color color, Rectangle rectangle,
Point boxMotion) {
this.name = name;
this.color = color;
this.rectangle = rectangle;
this.boxMotion = boxMotion;
}
public void update(int width, int height) {
this.rectangle.x += boxMotion.x;
this.rectangle.y += boxMotion.y;
int boxWidth = rectangle.x + rectangle.width;
int boxHeight = rectangle.y + rectangle.height;
if (rectangle.x < 0) {
rectangle.x = -rectangle.x;
boxMotion.x = -boxMotion.x;
}
if (boxWidth > width) {
rectangle.x = width - rectangle.width - boxMotion.x;
boxMotion.x = -boxMotion.x;
}
if (rectangle.y < 0) {
rectangle.y = -rectangle.y;
boxMotion.y = -boxMotion.y;
}
if (boxHeight > height) {
rectangle.y = height - rectangle.height - boxMotion.y;
boxMotion.y = -boxMotion.y;
}
}
public Color getColor() {
return color;
}
public Rectangle getRectangle() {
return rectangle;
}
public String getName() {
return name;
}
}
The Box
class is a plain Java class. It has the typical getter methods. There are no setter classes, as the constructor sets all of the internal fields.
We make use of a java.awt.Rectangle
and a java.awt.Point
to hold the size of the box and the direction of box motion, respectively. This keeps us from having to define six int
fields.
By saving the Color
and direction of motion with each box, we can have many boxes with different colors and different directions. The direction of motion is technically a delta X and delta Y instance, but I wasn't going to create a separate class just to make that clear. The Point
class is good enough.
The update
method was the hardest to get correct. The update
method controls the motion of the box, and checks for the box hitting the edges of the drawing JPanel
. This method is included in the Box
class because it acts on each box. This method will be executed by the controller.
The MVC pattern does not imply that the model, view, and controller code reside in separate classes, although that's the ideal. The code can go where it makes the most sense, as long as the execution of the code is separated by model, view, and controller.
Next, we'll look at the Boxes
class.
public class Boxes {
private final int drawingPanelWidth;
private final int drawingPanelHeight;
private final List<Box> boxes;
public Boxes() {
this.drawingPanelWidth = 600;
this.drawingPanelHeight = 400;
this.boxes = new ArrayList<>();
addBoxesFactory();
}
private void addBoxesFactory() {
Rectangle rectangle = new Rectangle(10, 10, 50, 50);
Point point = new Point(3, 3);
this.boxes.add(new Box("box1", Color.BLACK, rectangle, point));
rectangle = new Rectangle(100, 100, 50, 50);
point = new Point(-3, -3);
this.boxes.add(new Box("box2", Color.GREEN, rectangle, point));
}
public int getDrawingPanelWidth() {
return drawingPanelWidth;
}
public int getDrawingPanelHeight() {
return drawingPanelHeight;
}
public List<Box> getBoxes() {
return boxes;
}
}
The Boxes
class is a plain Java class. It has the typical getter methods. There are no setter classes, as the constructor sets all of the internal fields.
We use a List
to hold as many Box
instances as we want to display. In this class, we define two Box
instances. We can define as many Box
instances as we want. The rest of the code will process as many boxes as we define.
We define the width and height of the drawing JPanel
in this class. We do this because the drawing JPanel
will need those dimensions to size itself and because the controller will need those dimensions to determine when a Box
instance hits the width or height edge.
Next, we'll look at the main view class, the BoxMotionGUI
class.
public class BoxMotionGUI implements Runnable {
public static void main(String[] args) {
SwingUtilities.invokeLater(new BoxMotionGUI());
}
private Boxes boxes;
private DrawingPanel drawingPanel;
public BoxMotionGUI() {
this.boxes = new Boxes();
}
@Override
public void run() {
JFrame frame = new JFrame("Multiple Components GUI");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
drawingPanel = new DrawingPanel(boxes);
frame.add(drawingPanel, BorderLayout.CENTER);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
Timer timer = new Timer(20, new AbstractAction() {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
for (Box box : boxes.getBoxes()) {
box.update(boxes.getDrawingPanelWidth(),
boxes.getDrawingPanelHeight());
drawingPanel.repaint();
}
}
});
timer.setInitialDelay(0);
timer.start();
}
}
The BoxMotionGUI
class starts with a call to the SwingUtilities
invokeLater
method. This method ensures that the Swing application will create the Swing components on the Event Dispatch Thread. A well-written Swing program uses concurrency to create a user interface that never "freezes".
The constructor creates one instance of the Boxes
class. Generally, you create the model and pass the model to the view. The view reads from the model but does not update the model in any way. The controller classes will update the model and update / repaint the view.
The run method holds all of the code to create the JFrame
. These methods must be called in this order. This order is the order I use for most of my Swing applications.
The java.swing.Timer
code makes up the controller. I put this code in the view method because it's short. If the actionPerformed
code was more involved, a separate controller class would be warranted.
The actionPerformed
method updates the model and repaints the view.
Next, we'll look at the DrawingPanel
class.
public class DrawingPanel extends JPanel {
private static final long serialVersionUID = 1L;
private final Boxes boxes;
public DrawingPanel(Boxes boxes) {
this.boxes = boxes;
this.setPreferredSize(new Dimension(
boxes.getDrawingPanelWidth(),
boxes.getDrawingPanelHeight()));
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
for (Box box : boxes.getBoxes()) {
g2d.setColor(box.getColor());
Rectangle rectangle = box.getRectangle();
g2d.fillRect(rectangle.x, rectangle.y,
rectangle.width, rectangle.height);
}
}
}
The DrawingPanel
class draws the Box
instances. We extend a JPanel
because we want to override the paintComponent
method. The only reason you should ever extend a Swing component, or any Java class, is if you want to override one or more of the class methods.
The paintComponent
method paints. It doesn't try and do anything else. Since this method gets called every time the GUI is repainted, you want to do the minimum amount of processing possible. All other processing besides painting goes somewhere else.
Finally, here's the complete runnable code.
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.AbstractAction;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
public class BoxMotionGUI implements Runnable {
public static void main(String[] args) {
SwingUtilities.invokeLater(new BoxMotionGUI());
}
private Boxes boxes;
private DrawingPanel drawingPanel;
public BoxMotionGUI() {
this.boxes = new Boxes();
}
@Override
public void run() {
JFrame frame = new JFrame("Multiple Components GUI");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
drawingPanel = new DrawingPanel(boxes);
frame.add(drawingPanel, BorderLayout.CENTER);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
Timer timer = new Timer(20, new AbstractAction() {
private static final long serialVersionUID = 1L;
@Override
public void actionPerformed(ActionEvent e) {
for (Box box : boxes.getBoxes()) {
box.update(boxes.getDrawingPanelWidth(),
boxes.getDrawingPanelHeight());
drawingPanel.repaint();
}
}
});
timer.setInitialDelay(0);
timer.start();
}
public class DrawingPanel extends JPanel {
private static final long serialVersionUID = 1L;
private final Boxes boxes;
public DrawingPanel(Boxes boxes) {
this.boxes = boxes;
this.setPreferredSize(new Dimension(
boxes.getDrawingPanelWidth(),
boxes.getDrawingPanelHeight()));
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
for (Box box : boxes.getBoxes()) {
g2d.setColor(box.getColor());
Rectangle rectangle = box.getRectangle();
g2d.fillRect(rectangle.x, rectangle.y,
rectangle.width, rectangle.height);
}
}
}
public class Boxes {
private final int drawingPanelWidth;
private final int drawingPanelHeight;
private final List<Box> boxes;
public Boxes() {
this.drawingPanelWidth = 600;
this.drawingPanelHeight = 400;
this.boxes = new ArrayList<>();
addBoxesFactory();
}
private void addBoxesFactory() {
Rectangle rectangle = new Rectangle(10, 10, 50, 50);
Point point = new Point(3, 3);
this.boxes.add(new Box("box1", Color.BLACK, rectangle, point));
rectangle = new Rectangle(100, 100, 50, 50);
point = new Point(-3, -3);
this.boxes.add(new Box("box2", Color.GREEN, rectangle, point));
}
public int getDrawingPanelWidth() {
return drawingPanelWidth;
}
public int getDrawingPanelHeight() {
return drawingPanelHeight;
}
public List<Box> getBoxes() {
return boxes;
}
}
public class Box {
private final Color color;
private final Point boxMotion;
private final Rectangle rectangle;
private final String name;
public Box(String name, Color color, Rectangle rectangle,
Point boxMotion) {
this.name = name;
this.color = color;
this.rectangle = rectangle;
this.boxMotion = boxMotion;
}
public void update(int width, int height) {
this.rectangle.x += boxMotion.x;
this.rectangle.y += boxMotion.y;
int boxWidth = rectangle.x + rectangle.width;
int boxHeight = rectangle.y + rectangle.height;
if (rectangle.x < 0) {
rectangle.x = -rectangle.x;
boxMotion.x = -boxMotion.x;
}
if (boxWidth > width) {
rectangle.x = width - rectangle.width - boxMotion.x;
boxMotion.x = -boxMotion.x;
}
if (rectangle.y < 0) {
rectangle.y = -rectangle.y;
boxMotion.y = -boxMotion.y;
}
if (boxHeight > height) {
rectangle.y = height - rectangle.height - boxMotion.y;
boxMotion.y = -boxMotion.y;
}
}
public Color getColor() {
return color;
}
public Rectangle getRectangle() {
return rectangle;
}
public String getName() {
return name;
}
}
}
paint()
andupdate()
. That is done when using AWT. For custom painting in Swing you just override thepaintComponent()
method. – Keeling