BACKGROUND
I'm currently working on a project where I want to be able to draw a triangle, circle and a rectangle on a JPanel and move them around.
When a figure is moved, it should end up "at the top" so that it covers other figures that are in an overlapping position and when a figure is moved, the "top" is selected; if several figures cover the position of the mouse pointer, the top one is selected.
MY PROBLEM
I cannot figure out how to fix the issue of my shapes not ending up on top of each other if I drag them to the same position. They just stay in the same layers and if i stack them all on top of each other I still pick up the rectangle first.
They just stay in the same layers...
That's because of the order in which you are drawing them:
drawRec.paintComponent(g);
drawCirc.paintComponent(g);
drawTri.paintComponent(g);
Which will result in the drawTri to always be painted on top of the others (because you always paint it last). In a similar way, drawRec will be painted on the bottom of the others (since you are painting it always first). Then drawCirc will be painted in the middle. There is no dynamic order (let me put it) of the painting here. It doesn't matter what you drag or not, they are always going to be painted in that sequence.
A solution might be to have them in a list or array of some sort where when you drag one shape, you put it last in the list, and move the others before it. If you combine that with painting all shapes sequentialy from the list, then you will have your desired result.
...if i stack them all on top of each other I still pick up the rectangle first.
That's because of how your mousePressed in the ClickListener class works: it first of all checks if the rectangle is clicked and then the others. That means that if the rectangle overlaps another shape, then the rectangle is always going to be prioritized.
One solution might again be putting all your shapes in a data structure where you can modify their selection order. For example a list or array where let's say that the closer to the top is a shape then the later it will appear in the list. Then, when the user clicks somewhere, you will check the shapes starting from the last one in the list going to first one. If you find something, you immediately break the loop and select what you found. If you don't find anything, then the user clicked the panel somewhere where there are no shapes currently.
I am almost certain there must be more efficient data structures than a list or array for this problem (because you have to iterate in linear time all shapes to find the one clicked), but I am not an expert on collision detection, so, in order to keep things simple, I am going to stick with it. But there is also one other operation we want to do: change the painting and the selection order of the clicked shape. Long story short, we can use a LinkedHashSet for the job, because:
It maintains an order of the shapes.
We can quickly and efficiently change the order by first removing the clicked shape from its current postion (constant time) and then adding it back to the LinkedHashSet (also constant time), essentialy placing it last in the insertion order. So that automatically means that we have to use the last element in the set as the top most one. And that's good, because when painting we can iterate over all shapes in the set in the order in which they are found in it, so the last shape will be painted last (which means on top of all others). The same stands for the selection of a shape when clicking: we iterate over all elements, and select the last one which we found containing the user's click point. If LinkedHashSet had a descending iterator (by insertion order) then we could also optimize a little the searching of shapes at a given click point, but it doesn't (at least for Java 8 in which the following demonstration/example code applies), so I will stick with iterating from the start, checking all shapes every time and keeping the last one found containing the click point.
Finally, I would advise you to use the API class java.awt.Shape for the shapes since this enables you to create arbitrary shapes and get the contains method to check if a point lies inside them, the drawing/filling capability, the bounds, the path iterator, and so on...
Summarizing all the above, with an example code:
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.Point;
import java.awt.Shape;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Objects;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class NonComponentMain {
public static Shape createRectangle(final double width,
final double height) {
return new Rectangle2D.Double(0, 0, width, height);
}
public static Shape createEllipse(final double width,
final double height) {
return new Ellipse2D.Double(0, 0, width, height);
}
public static Shape createCircle(final double radius) {
return createEllipse(radius + radius, radius + radius);
}
public static class MoveableShapeHolder {
private final Shape shape;
private final Paint paint;
private final Rectangle2D originalBounds;
private double offsetX, offsetY;
public MoveableShapeHolder(final Shape shape,
final Paint paint) {
this.shape = Objects.requireNonNull(shape);
this.paint = paint;
offsetX = offsetY = 0;
originalBounds = shape.getBounds2D();
}
public void paint(final Graphics2D g2d) {
final AffineTransform originalAffineTransform = g2d.getTransform();
final Paint originalPaint = g2d.getPaint();
g2d.translate(offsetX, offsetY);
if (paint != null)
g2d.setPaint(paint);
g2d.fill(shape);
g2d.setPaint(originalPaint);
g2d.setTransform(originalAffineTransform);
}
public void moveTo(final double newBoundsCenterX,
final double newBoundsCenterY) {
offsetX = newBoundsCenterX - originalBounds.getCenterX();
offsetY = newBoundsCenterY - originalBounds.getCenterY();
}
public void moveBy(final double dx,
final double dy) {
offsetX += dx;
offsetY += dy;
}
public boolean contains(final Point2D pt) {
return shape.contains(pt.getX() - offsetX, pt.getY() - offsetY);
}
public Point2D getTopLeft() {
return new Point2D.Double(offsetX + originalBounds.getX(), offsetY + originalBounds.getY());
}
public Point2D getCenter() {
return new Point2D.Double(offsetX + originalBounds.getCenterX(), offsetY + originalBounds.getCenterY()); //Like 'getTopLeft' but with adding half the size.
}
public Point2D getBottomRight() {
return new Point2D.Double(offsetX + originalBounds.getMaxX(), offsetY + originalBounds.getMaxY()); //Like 'getTopLeft' but with adding the size of the bounds.
}
}
public static class DrawPanel extends JPanel {
private class MouseDrag extends MouseAdapter {
private MoveableShapeHolder current;
private Point origin;
private Point2D center;
#Override
public void mousePressed(final MouseEvent e) {
current = null;
center = null;
final Point evtLoc = e.getPoint();
for (final MoveableShapeHolder moveable: moveables)
if (moveable.contains(evtLoc))
current = moveable; //Keep the last moveable found to contain the click point! It's important to be the last one, because the later the moveable appears in the collection, the closer to top its layer.
if (current != null) { //If a shape was clicked...
//Initialize MouseDrag's state:
origin = e.getPoint();
center = current.getCenter();
//Move to topmost layer:
moveables.remove(current); //Remove from its current position.
moveables.add(current); //Move to last (topmost layer).
//Rapaint panel:
repaint();
}
}
#Override
public void mouseDragged(final MouseEvent e) {
if (current != null) { //If we are dragging something (and not empty space), then:
current.moveTo(center.getX() + e.getX() - origin.x, center.getY() + e.getY() - origin.y);
repaint();
}
}
#Override
public void mouseReleased(final MouseEvent e) {
current = null;
origin = null;
center = null;
}
}
private final LinkedHashSet<MoveableShapeHolder> moveables;
public DrawPanel() {
moveables = new LinkedHashSet<>();
final MouseAdapter ma = new MouseDrag();
super.addMouseMotionListener(ma);
super.addMouseListener(ma);
}
/**
* Warning: all operations on the returned value must be made on the EDT.
* #return
*/
public Collection<MoveableShapeHolder> getMoveables() {
return moveables;
}
#Override
protected void paintComponent(final Graphics g) {
super.paintComponent(g);
moveables.forEach(moveable -> moveable.paint((Graphics2D) g)); //Topmost moveable is painted last.
}
#Override
public Dimension getPreferredSize() {
if (isPreferredSizeSet())
return super.getPreferredSize();
final Dimension preferredSize = new Dimension();
moveables.forEach(moveable -> {
final Point2D max = moveable.getBottomRight();
preferredSize.width = Math.max(preferredSize.width, (int) Math.ceil(max.getX()));
preferredSize.height = Math.max(preferredSize.height, (int) Math.ceil(max.getY()));
});
return preferredSize;
}
}
private static void createAndShowGUI() {
final DrawPanel drawPanel = new DrawPanel();
final Collection<MoveableShapeHolder> moveables = drawPanel.getMoveables();
MoveableShapeHolder moveable = new MoveableShapeHolder(createRectangle(100, 50), Color.RED);
moveable.moveTo(100, 75);
moveables.add(moveable);
moveable = new MoveableShapeHolder(createCircle(40), Color.GREEN);
moveable.moveTo(125, 100);
moveables.add(moveable);
moveable = new MoveableShapeHolder(createRectangle(25, 75), Color.BLUE);
moveable.moveTo(125, 75);
moveables.add(moveable);
final JFrame frame = new JFrame("Click to drag");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(drawPanel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
public static void main(final String[] args) {
SwingUtilities.invokeLater(NonComponentMain::createAndShowGUI);
}
}
All the above apply in the case you want to work with a single custom component painting all shapes. If instead you want, you can create a custom component for each shape and use a JLayeredPane for positioning them on top of each other. But then you would probably need a custom LayoutManager too (in order to handle the location of each component).
I created the following GUI.
I created a Shape class. I used a java.awt.Point to hold the center point and a java.awt.Polygon to hold the actual shape. I was able to use the Polygon contains method to see if I was mouse clicking inside the polygon.
I created a ShapeModel class to hold a java.util.List of Shape instances. Creating the proper application model is so important when creating a Swing GUI.
I created a JFrame and a drawing JPanel. The drawing JPanel draws the List of Shape instances. Period. The MouseAdapter will take care of recalculating the polygon and repainting the JPanel.
The trick is in the MouseAdapter mousePressed method. I delete the selected Shape instance and add the selected Shape instance back to the List. This moves the selected shape to the top of the Z-order.
Here's the complete runnable code. I made all the classes inner classes so I could post this code as one block.
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.Polygon;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class MoveShapes implements Runnable {
public static void main(String[] args) {
SwingUtilities.invokeLater(new MoveShapes());
}
private final DrawingPanel drawingPanel;
private final ShapeModel shapeModel;
public MoveShapes() {
this.shapeModel = new ShapeModel();
this.drawingPanel = new DrawingPanel();
}
#Override
public void run() {
JFrame frame = new JFrame("Move Shapes");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(drawingPanel, BorderLayout.CENTER);
frame.pack();
frame.setLocationByPlatform(true);
frame.setVisible(true);
}
public void repaint() {
drawingPanel.repaint();
}
public class DrawingPanel extends JPanel {
private static final long serialVersionUID = 1L;
public DrawingPanel() {
this.setBackground(Color.WHITE);
this.setPreferredSize(new Dimension(600, 500));
MoveListener listener = new MoveListener();
this.addMouseListener(listener);
this.addMouseMotionListener(listener);
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(
RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
for (Shape shape : shapeModel.getShapes()) {
g2d.setColor(shape.getColor());
g2d.fillPolygon(shape.getShape());
}
}
}
public class MoveListener extends MouseAdapter {
private Point pressedPoint;
private Shape selectedShape;
#Override
public void mousePressed(MouseEvent event) {
this.pressedPoint = event.getPoint();
this.selectedShape = null;
List<Shape> shapes = shapeModel.getShapes();
for (int i = shapes.size() - 1; i >= 0; i--) {
Shape shape = shapes.get(i);
if (shape.getShape().contains(pressedPoint)) {
selectedShape = shape;
break;
}
}
if (selectedShape != null) {
shapes.remove(selectedShape);
shapes.add(selectedShape);
}
}
#Override
public void mouseReleased(MouseEvent event) {
moveShape(event.getPoint());
}
#Override
public void mouseDragged(MouseEvent event) {
moveShape(event.getPoint());
}
private void moveShape(Point point) {
if (selectedShape != null) {
int x = point.x - pressedPoint.x;
int y = point.y - pressedPoint.y;
selectedShape.incrementCenterPoint(x, y);
drawingPanel.repaint();
pressedPoint = point;
}
}
}
public class ShapeModel {
private final List<Shape> shapes;
public ShapeModel() {
this.shapes = new ArrayList<>();
this.shapes.add(new Shape(100, 250, Color.BLUE, ShapeType.TRIANGLE));
this.shapes.add(new Shape(300, 250, Color.RED, ShapeType.RECTANGLE));
this.shapes.add(new Shape(500, 250, Color.BLACK, ShapeType.CIRCLE));
}
public List<Shape> getShapes() {
return shapes;
}
}
public class Shape {
private final Color color;
private Point centerPoint;
private Polygon shape;
private final ShapeType shapeType;
public Shape(int x, int y, Color color, ShapeType shapeType) {
this.centerPoint = new Point(x, y);
this.color = color;
this.shapeType = shapeType;
createPolygon(shapeType);
}
public void incrementCenterPoint(int x, int y) {
centerPoint.x += x;
centerPoint.y += y;
createPolygon(shapeType);
}
private void createPolygon(ShapeType shapeType) {
this.shape = new Polygon();
switch (shapeType) {
case TRIANGLE:
int angle = 30;
int radius = 100;
for (int i = 0; i < 3; i++) {
Point point = toCartesianCoordinates(angle, radius);
shape.addPoint(point.x, point.y);
angle += 120;
}
break;
case RECTANGLE:
angle = 45;
radius = 100;
for (int i = 0; i < 4; i++) {
Point point = toCartesianCoordinates(angle, radius);
shape.addPoint(point.x, point.y);
angle += 90;
}
break;
case CIRCLE:
radius = 75;
for (angle = 0; angle < 360; angle++) {
Point point = toCartesianCoordinates(angle, radius);
shape.addPoint(point.x, point.y);
}
break;
}
}
private Point toCartesianCoordinates(int angle, int radius) {
double theta = Math.toRadians(angle);
int x = (int) Math.round(Math.cos(theta) * radius) + centerPoint.x;
int y = (int) Math.round(Math.sin(theta) * radius) + centerPoint.y;
return new Point(x, y);
}
public Color getColor() {
return color;
}
public Point getCenterPoint() {
return centerPoint;
}
public Polygon getShape() {
return shape;
}
}
public enum ShapeType {
TRIANGLE, RECTANGLE, CIRCLE
}
}
I cannot figure out how to fix the issue of my shapes not ending up on top of each other if I drag them to the same position
Components on a panel are painted based on the components ZOrder. The component with the lowest ZOrder is painted last.
So in the mousePressed metthod of your MouseListener when you select a component to drag you can change its ZOrder:
Component child = e.getComponent();
child.getParent().setComponentZOrder(child, 0);
Edit:
drawRec.paintComponent(g);
drawCirc.paintComponent(g);
drawTri.paintComponent(g);
I didn't notice that code before.
Never invoke paintComponent() directly.
Swing has a parent/child relationship. You just need to add each of the 3 shape panels to the parent panel. The parent panel will then paint the child components based on the ZOrder I described above. Get rid of those 3 lines of code.
Related
I'm drawing Circles on JFrame using JComponent (AWT/SWING) and I want to make sure that when the user resizes the frame, that certain calculations are made and circles are drawn on screen dynamically (whether if it's bigger, smaller, moved to left or right etc.). I implemented the ComponentAdapter event and componentResized method however I'm struggling with coming up with something that is dynamic. Here's my code:
CircleViewer Class
import javax.swing.JFrame;
import java.awt.event.*;
public class CircleViewer
{
public static void main(String[] args)
{
final JFrame frame = new JFrame("Circle Shapes");
final CirclePanel panel = new CirclePanel();
// Class for Mouse Listener which implements the necessary interfaces
class MousePressListener implements MouseListener, MouseMotionListener
{
public void mouseClicked(MouseEvent event) { }
public void mouseEntered(MouseEvent event) { }
public void mouseExited(MouseEvent event) { }
public void mouseWheelMoved(MouseWheelEvent event) { }
public void mouseMoved(MouseEvent event) { }
public void mousePressed(MouseEvent event) { }
#Override
public void mouseDragged(MouseEvent event)
{
var x = event.getX();
var y = event.getY();
panel.moveTo(x, y);
}
#Override
public void mouseReleased(MouseEvent event)
{
panel.finalMove();
}
}
panel.addComponentListener(new ComponentAdapter() {
public void componentResized(ComponentEvent event)
{
panel.frameResizeCalculation(frame.getWidth(), frame.getHeight());
}
});
MousePressListener listener = new MousePressListener();
panel.addMouseListener(listener);
panel.addMouseMotionListener(listener);
frame.setSize(FRAME_WIDTH, FRAME_HEIGHT);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(panel);
frame.setVisible(true);
}
public static final int FRAME_WIDTH = 700;
public static final int FRAME_HEIGHT = 500;
}
CirclePanel Class
import java.awt.Graphics;
import java.awt.Graphics2D;
import javax.swing.JComponent;
import java.util.ArrayList;
import java.awt.Color;
import java.lang.Math;
import java.awt.BasicStroke;
import java.awt.Stroke;
public class CirclePanel extends JComponent
{
private int mouseX;
private int mouseY;
private ArrayList<Circle> circleList;
private final BasicStroke dashLine = new BasicStroke(1,
BasicStroke.CAP_BUTT,
BasicStroke.JOIN_BEVEL,
0, new float[]{6}, 0);
private Circle newCircle;
private final Color newCircleColor = Color.RED;
private final Color finalCircleColor = Color.BLUE;
public CirclePanel()
{
this.circleList = new ArrayList<Circle>();
this.mouseX = 0;
this.mouseY = 0;
}
public void moveTo(int x, int y)
{
mouseX = x;
mouseY = y;
if (newCircle == null)
{
newCircle = new Circle(x,y,0);
}
else
{
int dX = newCircle.get(0) - mouseX;
int dY = newCircle.get(1) - mouseY;
newCircle.set(2, (int)Math.sqrt(dX*dX + dY*dY));
}
repaint();
}
public void finalMove()
{
if (newCircle != null)
{
circleList.add(newCircle);
newCircle = null;
repaint();
}
}
// Do something here and change X-Y coordinates and radius of the circles and finally call repaint() method of Graphics2D
public void frameResizeCalculation(int width, int height)
{
var dX = CircleViewer.FRAME_WIDTH - width;
var dY = CircleViewer.FRAME_HEIGHT - height;
}
public void paintComponent(Graphics g)
{
g.setColor(finalCircleColor);
for (Circle circle : circleList)
{
drawCircle(g, circle);
}
Circle c = newCircle;
if (c != null)
{
g.setColor(newCircleColor);
drawCircle(g, c);
Graphics2D g2 = (Graphics2D)g.create();
g2.setStroke(dashLine);
g2.drawLine(c.get(0), c.get(1), mouseX, mouseY);
g2.dispose();
}
}
public void drawCircle(Graphics g, Circle c)
{
g.drawOval(c.get(0) - c.get(2), c.get(1) - c.get(2), c.get(2) * 2, c.get(2) * 2);
}
}
and lastly, the Circle
public class Circle
{
private int x;
private int y;
private int radius;
public Circle(int x, int y, int radius)
{
this.x = x;
this.y = y;
this.radius = radius;
}
public int get(int option)
{
switch (option)
{
case 0:
return this.x;
case 1:
return this.y;
case 2:
return this.radius;
}
return 0;
}
public void set(int option, int value)
{
switch (option)
{
case 0: //set x
this.x = value;
break;
case 1:
this.y = value;
break;
case 2:
this.radius = value;
break;
}
}
}
that certain calculations are made and circles are drawn on screen dynamically (whether if it's bigger, smaller, moved to left or right etc.).
Well you need to define what YOU want to happen when the frame is resized. We can't tell you what to do.
However, before you worry about that you need to restructure your classes to make it possible to have the dynamic painting.
I see the following issues with the basic code:
Forget about the frame size. That size is irrelevant to the custom painting that will be done in your CirclePanel. That is the size of the CirclePanel is NOT the same as the size of your frame, since the frame size includes the frame borders and title bar. Your logic should be based on the size of the panel, not the frame.
When doing custom painting it is also the responsibility of the component to override the getPreferredSize() method to return the preferred size of the component. Then in your code you add the panel to the frame and then invoke the pack() method on the frame.
You call your class CirclePanel, but you extend JComponent. Why? Give your class a proper name. If you want to extend JComponent then call your class CircleComponent. If you want to call your class CirclePanel, then extend JPanel. But you also need to understand the difference between extending JComponent and JPanel. All Swing components are responsible for clearing the background of the component BEFORE doing any painting. A JPanel does this for you automatically, you just invoke super.paintComponent(...) at the start. A JComponent does NOT clear the background so you must clear it by setting the color of the Graphics object and then invoke fillRect(0, 0, getWidth(), getHeight()) to paint the background.
The Circle object should contain all the information needed to paint itself. You already have the x, y, radius values. I would suggest you also need a Color property so each Circle can be a different color.
The Circle object should then know how to paint itself using its properties. Therefore your Circle class should have a method, lets say draw(Graphics grapics). You then use the properties of the class to draw the oval. So this means the paintComponent() method would invoke the draw(...) method of the Circle class and you would remove the drawOval(...) method you currently have.
A "getter" method does not take parameters. If you feel other classes need to know the x, y, radius properties then create getX(), getY() and getRadiout() methods. I would suggest you don't need the get()/set() methods.
I suggest you first need to implement the above suggestions before making the painting dynamic.
Next, you don't need a ComponentListener added to the panel. Instead you need to add logic to the paintComponent(...) method of your CirclePanel class. The paintComponent() method will be invoked automatically every time the size of the panel changes. The basic logic would be to determine a "multiplier" to be used when painting each Circle.
So you can use the getPreferredSize() method to get the preferred width and you can use the getWidth() method of the panel to get the current size. So your multiplier would be:
double multiplierX = getWidth() / getPreferredSize().x;
Now this information needs to be passed to the Circle objects draw(...) method, so the method signature would become draw(Graphics g, double multiplierX). When you invoke the drawOval(...) method you apply the multiplier to the "x" parameter. This should cause the Circles to shift in the horizontal direction as the frame is resized.
You would then repeat the above step for the multiplierY to have the Circles shift in a vertical direction.
You would then need to decide how you want to affect the radius?
So I've been stuck on this problem for a while now and I'm desperate for help. Please help me. I've got 3 classes:
Circle is just suppose to draw a circle in the frame created by Frame with random starting position (and defind the radius).
Frame is the mainclass with methods such as addCircle(), bounce(), start(), stop(), run() (moves the circles) and quit(). This class also creates the frame in which the circles are added to.
Interfa is just for now a inteface frame where I define the radius, number of circles and Frame size.
No matter what I try I cannot add more than two circle (one is colored and one is not):
The "recursive way":
private static void addCircle(int n){
Circle[] circles = new Circle[n+10];
if (n > 0){
circles[circleAdd] = new Circle();
frame.add(circles[circleAdd]);
circleAdd = circleAdd + 1;
addCircle(n-1);
}
}
Normal itterative way
private static void addCircles(int n){
ArrayList<Circle> circles = new ArrayList<Circle>();
for(int i = 0; i<=n;i++){
circles.add(new Circle());
frame.add(circles.get(i));
}
}
This is how I create my Frame:
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
public Class Frame{
private static JFrame frame;
private static int circleAdd = 0;
private static JPanel fra;
public static void mainFrame(){
frame = new JFrame();
frame.setSize(500,500);
frame.setVisible(true);
fra = new JPanel();
frame.add(fra);
...
//addCircle and addCircles
...
public static void main..
}
}
This is my circle:
import java.awt.*;
import javax.swing.*;
import java.util.Random;
public class Circle extends JPanel{
private Random random = new Random();
public void paint(Graphics g){
int randX = random.nextInt(250)+50;
int randY = random.nextInt(250)+50;
g.drawOval(randX,randY,50,50);
g.setColor(Color.ORANGE);
g.fillOval(100,100,50,50);
}
}
I would suggest that your general approach is wrong. Instead of using a JPanel as the element, you should have a JPanel capable of painting any number of "circles". The Graphics2D API is capable of drawing complex shapes (including ovals).
The main issues I can see are:
JFrame by default is using a BorderLayout, this only allows a single component to be placed in each of the five available positions
Layout managers rely on the preferred/minimum/maximumSize hints to make determinations about the size of the components. They are also responsible for deciding on where the component should be placed. In your current implementation, this would mean that it's possible for you to paint beyond the visible range of the component
Overriding paint is not recommend, and failing to call super.paint could cause a number of unexpected and difficult to diagnose issues
Painting can occur at any time, so using random values in the paint method will cause the UI to constantly change
Instead, you could define your own Circle class which takes the location and size you want and simply acts as a container
public class Circle {
private int x;
private int y;
private int radius;
private Ellipse2D shape;
public Circle(int x, int y, int radius) {
this.x = x;
this.y = y;
this.radius = radius;
this.shape = new Ellipse2D.Double(x, y, radius * 2, radius * 2);
}
public int getX() {
return x;
}
public int getY() {
return y;
}
public int getRadius() {
return radius;
}
public Rectangle getBounds() {
return shape.getBounds();
}
public void paint(Graphics2D g2d) {
g2d.setColor(Color.ORANGE);
g2d.fill(shape);
}
}
This is simply a container class, it represents the information need to generate the desired outcome. It has a convince method which is capable of then painting the shape itself.
You would then need to create a List of these shapes and paint them to your component
public class TestPane extends JPanel {
private List<Circle> circles = new ArrayList<>(10);
private Dimension size;
public TestPane() {
Random random = new Random();
int maxX = 0;
int maxY = 0;
for (int index = 0; index < 10; index++) {
int randX = random.nextInt(250) + 50;
int randY = random.nextInt(250) + 50;
circles.add(new Circle(randX, randY, 25));
maxX = Math.max(maxX, randX + 50);
maxY = Math.max(maxY, randY + 50);
}
size = new Dimension(maxX, maxY);
}
#Override
public Dimension getPreferredSize() {
return size;
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
for (Circle circle : circles) {
Graphics2D g2d = (Graphics2D) g.create();
circle.paint(g2d);
g2d.dispose();
}
}
}
One of the things you seem to lack understanding in is how painting actually works in Swing.
Start by having a look at Performing Custom Painting and Painting in AWT and Swing for more details.
A deeper understanding of how layout managers and the component hierarchy work also wouldn't hurt
I am currently making a tile based game. Everything so far is working fine. However, I want the player to be able to add objects, like stone or wood to the screen when he/she presses the mouse button. I attempted this myself but it is not working. Here is what I have done, but is not working:
This is my KeyInput class, where all the keyboard and mouse events take place.
public static ArrayList<StoneTile> sTile = new ArrayList<StoneTile>();
public KeyInput(Handler handler) {
this.handler = handler;
}
public void tick(LinkedList<Square> object) {}
public void mousePressed(MouseEvent e){
int mx = e.getX();
int my = e.getY();
System.out.println("Pressed (X,Y): " + mx + " " + my);
sTile.add(new StoneTile(1,mx,my));
if(sTile.add(new StoneTile(1,mx,my))){
System.out.println("ADDED");
}
}
public void mouseReleased(MouseEvent e){
System.out.println("Released");
}
Here is my StoneTile class, this is what I want to add to screen:
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.util.LinkedList;
public class StoneTile extends Tile {
Textures tex;
public StoneTile(int id,int x,int y) {
super(Textures.stoneArray[0], id);
Tile.x = x;
Tile.y = y;
}
public static void main(String[] args) {
}
public Rectangle getBounds(){
return new Rectangle(x,y,Tile.TILEWIDTH,Tile.TILEHEIGHT);
}
}
The Textures.stoneArray[0] is simply the image that I want to add to the screen.
The Tile.(instance variable, like x, y, TILEWIDTH, and TILEHEIGHT) is simply a Tile class that contains all the render methods for the tiles (grass, stone, etc). If anything is unclear I will clarify or if you need any code provided, then I will add it in.
Note - The ArrayList was just an idea that I had in mind, if there are more efficient ways of doing this or any better ideas, I am open to all of them.
Here is where I set the MouseListener. I set it in an init() method and then called in a run() method (last line):
private void init() {
BufferedImageLoader loader = new BufferedImageLoader();
level = loader.loadImage("level.png");
world = new worldLoader("res/worlds/world1.txt");
handler = new Handler();
WIDTH = getWidth();
HEIGHT = getHeight();
cam = new Camera(handler, Game.WIDTH / 2, Game.HEIGHT / 2);
setWIDTH(getWidth());
setHEIGHT(getHeight());
tex = new Textures();
//backGround = loader.loadImage("/background.jpg");
handler.addObject(new Coin(100, 100, handler, ObjectId.Coin));
handler.addObject(new newStoneTile(20,20,ObjectId.newStoneTile));
handler.addObject(new player_Square(100,100, handler, ObjectId.player_Square));
//handler.addObject(new OneUp(300, 150, handler, ObjectId.OneUp));
this.addKeyListener(new KeyInput(handler));
this.addMouseListener(new KeyInput(handler));
}
jcomponent, is this what you meant?
public class Window {
private static final long serialVersionUID = -6482107329548182911L;
static final int DimensionX = 600;
static final int DimensionY = 600;
public Window(int w, int h, String title, Game game) {
game.setPreferredSize(new Dimension(w, h));
game.setMaximumSize(new Dimension(w, h));
game.setMinimumSize(new Dimension(w, h));
JFrame frame = new JFrame();
frame.add(game);
frame.pack();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
game.start();
}
}
Perhaps the best way to try and answer this question is to give a small example.
Essentially what needs to happen is the following (assuming my understanding of the problem is correct):
User clicks on the JComponent/JPanel to determine where to place a Tile. This will cause a MouseEvent that needs to be listened for and handled.
The JComponent/JPanel needs a MouseListener implementation which will create a new Tile object and add it to the List of the Tile objects. Once this is complete the JComponent/JPanel needs to know to repaint(). You do not override repaint() but rather paintComponent(Graphics g), which will be called by repaint() (eventually).
The paintComponent(Graphics g) method will iterate over the List of Tile objects, drawing them to the JComponent/JPanel using the Graphics context for the component.
To illustrate this I have simplified your problem. Note this isn't the best way to solve the problem since the Model (game logic) and the GUI should be separated, ideally using Model View Controller / Observer pattern.
First and most importantly is the GamePanel class, which extends JPanel. It's sole role in this example is to display the game graphically and handle mouse clicks. i.e. handling the list of tasks noted above.
GamePanel
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JPanel;
public class GamePanel extends JPanel {
private List<Tile> tiles; // Stores the Tile objects to be displayed
/**
* Sole constructor for GamePanel.
*
* #param width the width of the game panel
* #param height the height of the game panel
*/
public GamePanel(int width, int height) {
tiles = new ArrayList<>();
setPreferredSize(new Dimension(width, height));
// Implement mouse events for when the JPanel is 'clicked', only using the
// mouse released operation in this case.
addMouseListener(new MouseListener() {
#Override
public void mouseReleased(MouseEvent e) {
// On mouse release, add a StoneTile (in this case) to the tiles List
tiles.add(new StoneTile(e.getX(), e.getY()));
// Repaint the JPanel, calling paint, paintComponent, etc.
repaint();
}
#Override
public void mouseClicked(MouseEvent e) {
// Do nothing
}
#Override
public void mousePressed(MouseEvent e) {
// Do nothing
}
#Override
public void mouseEntered(MouseEvent e) {
// Do nothing
}
#Override
public void mouseExited(MouseEvent e) {
// Do nothing
}
});
}
/**
* Draws the Tiles to the Game Panel.
*
* #param g the Graphics context in which to paint
*/
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g); // Make sure you do this
// For this example, using black as the color to draw
g.setColor(Color.BLACK);
// Iterate over the tile list and draw them to the JPanel
for (Tile tile : tiles) {
Rectangle tileRect = tile.getBounds();
g.fillRect(tileRect.x, tileRect.y, tileRect.width, tileRect.height);
}
}
}
Second is the GameFrame, which extends JFrame. This is just a basic JFrame which adds a GamePanel object. I've also included the main method which will ensure the GUI is initialized on the Event Dispatch Thread.
GameFrame
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
public class GameFrame extends JFrame {
private static final String TITLE = "Tile Game"; // Game Frame Window Title
private final JPanel gamePanel;
/**
* Sole constructor for GameFrame.
*
* #param width the width of the game in pixels
* #param height the height of the game in pixels
*/
public GameFrame(int width, int height) {
gamePanel = new GamePanel(width, height);
}
/**
* Performs final configuration and shows the GameFrame.
*/
public void createAndShow() {
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setTitle(TITLE);
add(gamePanel);
pack();
setVisible(true);
}
/**
* Entry point for the program.
*
* #param args not used
*/
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
#Override
public void run() {
GameFrame gameFrame = new GameFrame(640, 480);
gameFrame.createAndShow();
}
});
}
}
Finally, the other classes used in the example for completeness, Tile and StoneTile. Personally I don't see much benefit from using Rectangle inside the model, but each to their own and I wanted to keep the example somewhat similar to your currently implementation.
Tile
import java.awt.Rectangle;
public abstract class Tile {
private int x, y, width, height;
public Tile(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
public Rectangle getBounds() {
return new Rectangle(x, y, width, height);
}
}
StoneTile
public class StoneTile extends Tile {
public StoneTile(int x, int y) {
super(x, y, 100, 100);
}
}
One final comment. I notice that your sTile ArrayList is static, this should probably not be the case as it belongs to the class rather than a particular instance of the class.
I am writing a program which involves creating a JFrame and drawing a circle inside it using drawOval() from the Graphics class. I have reached a problem where I am trying to create a point at the centre of the JFrame, and then draw my circle with this pont being the x and y coordinates of the circle. Here is my code so far:
import java.awt.Graphics;
import javax.swing.JFrame;
import java.awt.event.*;
import java.awt.geom.Point2D;
import java.awt.Point;
class MouseJFrameMotion extends JFrame implements MouseMotionListener{
int circleXcenter;
int circleYcenter;
int circleRadius = 50;
boolean show = false;
public MouseJFrameMotion(){
addMouseMotionListener(this);
}
public void paint(Graphics g){
super.paint(g);
if(show){
g.drawOval(circleXcenter,circleYcenter, circleRadius*2,circleRadius*2);
}
}
public void mouseDragged(MouseEvent e){
}
Point frameCenter = new Point((this.getWidth()/2), (this.getHeight()/2));
public void mouseMoved(MouseEvent e){
int xLocation = e.getX();
int yLocation = e.getY();
show = true;
circleXcenter = (int) frameCenter.getX();
circleYcenter = (int) frameCenter.getY();
repaint();
}
}
public class GrowingCircle {
public static void main(String[] args) {
MouseJFrameMotion myMouseJFrame = new MouseJFrameMotion();
myMouseJFrame.setSize(500, 500);
myMouseJFrame.setVisible(true);
}
}
As you can see in the main() function, I set the size of the JFrame to 500x500. However, when the circle is drawn, it's x and y coordinates are (0,0) when I expect them to be (250, 250) based on Point frameCenter after repaint() is called. Where am I going wrong?
So two things...
Don't override paint of JFrame, there a JRootPane, contentPane and other components between the user and the frames surface which can interfere with the painting. Instead, use a JPanel and override its paintComponent method
At the time Point frameCenter = new Point((this.getWidth()/2), (this.getHeight()/2)); is evaluated, the frame's size is 0x0, you need to reevaluate the frameCenter before you paint the circle. When you do this will depend on how dynamic you want the change to be
I think you need both repaint() and revalidate() method
When you are constructing the class MouseJFrameMotion, the variable frameCenter is defined and set width and height 0 and it will never change. So what you can do is to calculate the frame center when you are drawing.
public void mouseMoved(MouseEvent e) {
int xLocation = e.getX();
int yLocation = e.getY();
show = true;
Point frameCenter = new Point((this.getWidth() / 2), (this.getHeight() / 2));
circleXcenter = (int) frameCenter.getX();
circleYcenter = (int) frameCenter.getY();
repaint();
}
I want a point to move towards the mouse position. Currently the point just is the mouse location. But I want it so that the bigger the distance between mouse and center of the screen, the less force get´s added to the point. So if the mouse is enough distance away from the center, the point will just stop moving further outwards because it´s force away from the center is to small. The center should always be the point which is looked at. So no repositioning.
I am really stuck here because I am pretty young and I didn´t have had vector calculation in school (I think this is what you need here). I have no idea how to do what I am trying to archive. The only thing I tried was using the distance between center and mouse (Math.hypot(width / 2 - mouseX , height / 2 - mouseY)). But it didn´t work.
This is my minimal reproducible example:
import javax.swing.*;
import java.awt.*;
public class Example extends JPanel {
private static final int size = 500;
public Example() {
this.setPreferredSize(new Dimension(size, size));
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.fillOval(getWidth() / 2 - 3, getHeight() / 2 - 3, 6, 6); // center
Point point = MouseInfo.getPointerInfo().getLocation();
SwingUtilities.convertPointFromScreen(point, this);
// Do calculations with the point here
g2d.fillOval(point.x - 5, point.y - 5, 10, 10);
repaint();
}
private static void createFrame() {
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.add(new Example());
f.pack();
f.setVisible(true);
}
public static void main(String[] args) {
EventQueue.invokeLater(Example::createFrame);
}
}
It would look like this:
You can declare an attraction ratio value, which will strech the circle to the center of the screen, based on its value.
You need also be able to know for each cursor position how far it is from the center in percentages.
import javax.swing.*;
import java.awt.*;
public class Example extends JPanel {
private static final int SIZE = 500;
private static final int CENTER_CIRCLE_RADIUS = 3;
private static final int CIRCLE_RADIUS = 5;
private static final double ATTRACTION_RATIO = 0.75;
public Example() {
this.setPreferredSize(new Dimension(SIZE, SIZE));
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
final int xCenterPoint = getWidth() / 2;
final int yCenterPoint = getHeight() / 2;
g2d.fillOval(xCenterPoint - CENTER_CIRCLE_RADIUS, yCenterPoint - CENTER_CIRCLE_RADIUS,
CENTER_CIRCLE_RADIUS * 2, CENTER_CIRCLE_RADIUS * 2); // center
Point point = MouseInfo.getPointerInfo().getLocation();
SwingUtilities.convertPointFromScreen(point, this);
final double xNearToCenter = 1 - ((double) (xCenterPoint - point.x) / xCenterPoint);
final double yNearToCenter = 1 - ((double) (yCenterPoint - point.y) / yCenterPoint);
g2d.fillOval((int) (xCenterPoint * (ATTRACTION_RATIO + xNearToCenter * (1 - ATTRACTION_RATIO))),
(int) (yCenterPoint * (ATTRACTION_RATIO + yNearToCenter * (1 - ATTRACTION_RATIO))),
CIRCLE_RADIUS * 2, CIRCLE_RADIUS * 2);
repaint();
}
private static void createFrame() {
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Example panel = new Example();
f.add(panel);
f.pack();
f.setVisible(true);
}
public static void main(String[] args) {
EventQueue.invokeLater(Example::createFrame);
}
}
I modified your code somewhat to create the following GUI.
The circle will follow your mouse around the drawing JPanel.
The first thing I did was create a Circle class. This class holds the center point, radius, and color of the circle. I use a Point2D class to hold the center point so I can move the circle in fractional pixel increments. This makes the animation smoother.
Next, I used your code to create a JFrame and a drawing JPanel. I like to separate the JFrame code from the JPanel code. It makes it easier for me to focus on one Swing component at a time.
The drawing JPanel draws the circle. Period. The controller classes are responsible for calculating the new position of the circle and calling the repaint method of the drawing JPanel.
I created a MouseMotionListener to listen for the mouse movement. This class keeps track of the mouse position.
I created a Swing Timer to animate the motion of the circle.
I used polar geometry to calculate the direction that the circle moves. The polar geometry is in the MoveListener class, the actionPerformed method. I use an arc-tangent to calculate the angle to the mouse position in radians, then take the cosine to get the X movement, and the sine to get the Y movement.
Here's the complete runnable code.
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.Point2D;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
public class MouseMoveExample implements Runnable {
public static void main(String[] args) {
EventQueue.invokeLater(new MouseMoveExample());
}
private final int size = 500;
private Circle circle;
private DrawingPanel drawingPanel;
public MouseMoveExample() {
this.circle = new Circle(new Point(size / 2, size / 2), 12,
Color.BLACK);
}
#Override
public void run() {
JFrame f = new JFrame("Mouse Move Example");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.drawingPanel = new DrawingPanel();
f.add(drawingPanel);
f.pack();
f.setLocationByPlatform(true);
f.setVisible(true);
}
public class DrawingPanel extends JPanel {
private static final long serialVersionUID = 1L;
public DrawingPanel() {
this.setBackground(Color.WHITE);
this.setPreferredSize(new Dimension(size, size));
this.addMouseMotionListener(new MoveListener());
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g.create();
g2d.setColor(circle.getColor());
Point centerPoint = circle.getCenterPoint();
int radius = circle.getRadius();
int diameter = radius + radius;
g2d.fillOval(centerPoint.x - radius, centerPoint.y - radius,
diameter, diameter);
}
}
public class MoveListener extends MouseAdapter {
private MoveTimer moveTimer;
private Timer timer;
public MoveListener() {
this.moveTimer = new MoveTimer();
this.timer = new Timer(20, moveTimer);
this.timer.start();
}
#Override
public void mouseMoved(MouseEvent event) {
moveTimer.setEventPoint(event.getPoint());
}
}
public class MoveTimer implements ActionListener {
private Point eventPoint;
public void setEventPoint(Point eventPoint) {
this.eventPoint = eventPoint;
}
#Override
public void actionPerformed(ActionEvent event) {
Point centerPoint = circle.getCenterPoint();
if (eventPoint != null) {
double theta = Math.atan2(eventPoint.y - centerPoint.y,
eventPoint.x - centerPoint.x);
int increment = 1;
double x = Math.cos(theta) * increment;
double y = Math.sin(theta) * increment;
circle.incrementCenterPoint(x, y);
drawingPanel.repaint();
}
}
}
public class Circle {
private final int radius;
private final Color color;
private Point2D centerPoint;
public Circle(Point centerPoint, int radius, Color color) {
setCenterPoint(centerPoint);
this.radius = radius;
this.color = color;
}
public Point getCenterPoint() {
return new Point((int) Math.round(centerPoint.getX()),
(int) Math.round(centerPoint.getY()));
}
public void setCenterPoint(Point centerPoint) {
this.centerPoint = new Point2D.Double(
centerPoint.x, centerPoint.y);
}
public void incrementCenterPoint(double x, double y) {
centerPoint.setLocation(centerPoint.getX() + x,
centerPoint.getY() + y);
}
public int getRadius() {
return radius;
}
public Color getColor() {
return color;
}
}
}