I can not resize the two rectangles, but only the right one. What should I add to my code ?
I want also that when the lower edge of the left rectangle is moved, it also moves the upper edge of the right rectangle.
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.*;
public class Resizing extends JPanel {
Rectangle rect = new Rectangle(100,100,150,150);
Rectangle rect2 = new Rectangle(300,100,150,150);
#Override
public void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D)g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
g2.setColor(new Color(0, 0, 200));
g2.fill(rect);
g2.setColor(new Color(0, 0, 200));
g2.fill(rect2);
}
public static void main(String[] args) {
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Resizing essai = new Resizing();
Resizer1 rectangle = new Resizer1(essai);
essai.addMouseListener(rectangle);
essai.addMouseMotionListener(rectangle);
f.add(essai);
Resizing test2 = new Resizing();
Resizer2 rectangle2 = new Resizer2(test2);
test2.addMouseListener(rectangle2);
test2.addMouseMotionListener(rectangle2);
f.add(test2);
f.setSize(600,400);
f.setLocation(100,100);
f.setVisible(true);
}
}
class Resizer1 extends MouseAdapter {
Resizing component;
boolean dragging = false;
// Give user some leeway for selections.
final int PROX_DIST = 3;
public Resizer1(Resizing r) {
component = r;
}
#Override
public void mousePressed(MouseEvent e) {
if(component.getCursor() != Cursor.getDefaultCursor()) {
// If cursor is set for resizing, allow dragging.
dragging = true;
}
}
#Override
public void mouseReleased(MouseEvent e) {
dragging = false;
}
#Override
public void mouseDragged(MouseEvent e) {
if(dragging){
Point p = e.getPoint();
Rectangle r = component.rect;
int type = component.getCursor().getType();
int dy = p.y - r.y;
switch(type) {
case Cursor.N_RESIZE_CURSOR:
int height = r.height - dy;
r.setRect(r.x, r.y+dy, r.width, height);
break;
case Cursor.S_RESIZE_CURSOR:
height = dy;
r.setRect(r.x, r.y, r.width, height);
break;
default:
System.out.println("unexpected type: " + type);
}
component.repaint();
}
}
#Override
public void mouseMoved(MouseEvent e) {
Point p = e.getPoint();
if(!isOverRect(p)) {
if(component.getCursor() != Cursor.getDefaultCursor()) {
// If cursor is not over rect reset it to the default.
component.setCursor(Cursor.getDefaultCursor());
}
return;
}
// Locate cursor relative to center of rect.
int outcode = getOutcode(p);
Rectangle r = component.rect;
switch(outcode) {
case Rectangle.OUT_TOP:
if(Math.abs(p.y - r.y) < PROX_DIST) {
component.setCursor(Cursor.getPredefinedCursor(
Cursor.N_RESIZE_CURSOR));
}
break;
case Rectangle.OUT_BOTTOM:
if(Math.abs(p.y - (r.y+r.height)) < PROX_DIST) {
component.setCursor(Cursor.getPredefinedCursor(
Cursor.S_RESIZE_CURSOR));
}
break;
default: // center
component.setCursor(Cursor.getDefaultCursor());
}
}
/**
* Make a smaller Rectangle and use it to locate the
* cursor relative to the Rectangle center.
*/
private int getOutcode(Point p) {
Rectangle r = (Rectangle)component.rect.clone();
r.grow(-PROX_DIST, -PROX_DIST);
return r.outcode(p.x, p.y);
}
/**
* Make a larger Rectangle and check to see if the
* cursor is over it.
*/
private boolean isOverRect(Point p) {
Rectangle r = (Rectangle)component.rect.clone();
r.grow(PROX_DIST, PROX_DIST);
return r.contains(p);
}
}
class Resizer2 extends MouseAdapter {
Resizing component;
boolean dragging = false;
// Give user some leeway for selections.
final int PROX_DIST = 3;
public Resizer2(Resizing r) {
component = r;
}
#Override
public void mousePressed(MouseEvent e2) {
if(component.getCursor() != Cursor.getDefaultCursor()) {
// If cursor is set for resizing, allow dragging.
dragging = true;
}
}
#Override
public void mouseReleased(MouseEvent e2) {
dragging = false;
}
#Override
public void mouseDragged(MouseEvent e2) {
if(dragging){
Point p = e2.getPoint();
Rectangle r = component.rect2;
int type = component.getCursor().getType();
int dy = p.y - r.y;
switch(type) {
case Cursor.N_RESIZE_CURSOR:
int height = r.height - dy;
r.setRect(r.x, r.y+dy, r.width, height);
break;
case Cursor.S_RESIZE_CURSOR:
height = dy;
r.setRect(r.x, r.y, r.width, height);
break;
default:
System.out.println("unexpected type: " + type);
}
component.repaint();
}
}
#Override
public void mouseMoved(MouseEvent e2) {
Point p = e2.getPoint();
if(!isOverRect(p)) {
if(component.getCursor() != Cursor.getDefaultCursor()) {
// If cursor is not over rect reset it to the default.
component.setCursor(Cursor.getDefaultCursor());
}
return;
}
// Locate cursor relative to center of rect.
int outcode = getOutcode(p);
Rectangle r = component.rect2;
switch(outcode) {
case Rectangle.OUT_TOP:
if(Math.abs(p.y - r.y) < PROX_DIST) {
component.setCursor(Cursor.getPredefinedCursor(
Cursor.N_RESIZE_CURSOR));
}
break;
case Rectangle.OUT_BOTTOM:
if(Math.abs(p.y - (r.y+r.height)) < PROX_DIST) {
component.setCursor(Cursor.getPredefinedCursor(
Cursor.S_RESIZE_CURSOR));
}
break;
default: // center
component.setCursor(Cursor.getDefaultCursor());
}
}
/**
* Make a smaller Rectangle and use it to locate the
* cursor relative to the Rectangle center.
*/
private int getOutcode(Point p) {
Rectangle r = (Rectangle)component.rect2.clone();
r.grow(-PROX_DIST, -PROX_DIST);
return r.outcode(p.x, p.y);
}
/**
* Make a larger Rectangle and check to see if the
* cursor is over it.
*/
private boolean isOverRect(Point p) {
Rectangle r = (Rectangle)component.rect2.clone();
r.grow(PROX_DIST, PROX_DIST);
return r.contains(p);
}
}
Problems I see on inspection of your code:
You are adding 2 JPanels to your JFrame, but only one will show since they're being added in a default fashion to a BorderLayout-using container. Edit: I see now why you're doing this, but as explained below, you should not be doing this. Only create one Resizing object and add it once to the JFrame. The one Resizing will show both rectangles, and the single MouseAdapter should be coded to allow you to interact with both rectangles.
You are not making your Rectangle fields private and are allowing outside classes (namely Resizer1) to directly access and minipulate your fields. You'd be better off using public methods that allow other classes to selectively query your class for its state or to change your class's state.
Don't use two Resizer classes, Resizer1 and Resizer2, and it is this as well as your adding two Resizing objects to the JFrame that are in fact the main reason for your problems. Instead, use just one Resizer class, and use it as a single MouseAdapter added to a single Resizing object. Then in this single class, allow both rectangles to be changed.
Please post comments if you have any questions.
You ask:
Thank you, but how can I allow both rectangles to be changed ? In class Resizer, there is only one component (r) ?
There is only one component, but it holds two rectangles, and it knows the difference between the two rectangles since it has two rectangle variables.
Edit 2
Consider:
Editing your original question and adding your new code to the bottom, deleting your redundant new question.
Creating a non-GUI object, say called MyRectangle, that holds a Rectangle object.
This new class can have methods that allow your to pass in a Point or an x and y positions, and return information to let the calling code know if the mouse is over the top or bottom edge (your code already does this, so this should be no problem for you).
This new class will have mutator (setter) methods that allow outside classes set its rectangle y position and height.
The new class will have a fill method that accepts a Graphics2D parameter and uses it to fill the Rectangle that it holds.
Then give your Resizer class a List<MyRectangle>, actually an ArrayList of them, say called myRectangleList, and you can add two MyRectangle objects to it
Then give Resizer a getMyRectangleList method that returns the list.
Then in your MouseAdapter, iterate through the List to see if the mouse is over an edge
etc...
e.g.,
class MyRectangle {
private Rectangle rect;
private String name;
public MyRectangle(int x, int y, int width, int height, String name) {
rect = new Rectangle(x, y, width, height);
this.name = name;
}
public void fill(Graphics2D g2) {
g2.fill(rect);
}
public int getOutcode(Point p) {
// return ... do what you need to figure tihs out
}
public boolean isOverRect(Point p) {
// return ... do what you need to figure tihs out
}
public String getName() {
return name;
}
#Override
public String toString() {
return name + " " + rect.toString();
}
}
And then something like:
public class Resizing2 extends JPanel {
private static final int PREF_W = 600;
private static final int PREF_H = 400;
private static final Color RECT_COLOR = Color.blue;
private List<MyRectangle> rectangleList = new ArrayList<>();
public Resizing2() {
rectangleList.add(new MyRectangle(100, 100, 150, 150, "Rect 1"));
rectangleList.add(new MyRectangle(300, 100, 150, 150, "Rect 2"));
}
public List<MyRectangle> getRectangleList() {
return rectangleList;
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.setColor(RECT_COLOR);
for (MyRectangle rect : rectangleList) {
rect.fill(g2);
}
}
public Dimension getPreferredSize() {
return new Dimension(PREF_W, PREF_H);
}
// ..... etc...
Related
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.
I'm working on a Venn Diagram GUI application in Java which requires me to create elements (JLabels) that can be dragged and then dropped in the circular Venn Diagram sections. Presently I have two (custom made) circular J Panels that are overlapping.
Two Questions Arise: 1) How can I create J Labels which can move around the screen and ultimately snap into certain bounded spots of the Label and fit within bounds. 2) How am going to create a layout on the J Panels themselves that will only allow the text elements to fit in the certain spots and not overlap.
Here's what my GUI looks like so far. I've marked it up to show you a general idea of the interface and how I would like the functionality to work
Here you go... About 463 lines of code to get you started...
public class Final {
public static enum MouseEventFlag {
KEY_ALT, KEY_ALTGRAPH, KEY_CTRL, KEY_META, KEY_SHIFT, BUTTON_LEFT, BUTTON_MIDDLE, BUTTON_RIGHT;
public static EnumSet<MouseEventFlag> buttonSetOf(final MouseEvent mevt) {
final EnumSet<MouseEventFlag> set = EnumSet.noneOf(MouseEventFlag.class);
if (SwingUtilities.isLeftMouseButton(mevt)) set.add(BUTTON_LEFT);
if (SwingUtilities.isMiddleMouseButton(mevt)) set.add(BUTTON_MIDDLE);
if (SwingUtilities.isRightMouseButton(mevt)) set.add(BUTTON_RIGHT);
return set;
}
}
//Converts EnumSet to mask:
public static long pack(final EnumSet<?> set) { //Supports Enums with up to 64 values.
//return set.stream().mapToLong(e -> (1L << e.ordinal())).reduce(0L, (L1, L2) -> L1 | L2);
long L = 0;
for (final Enum e: set)
L = L | (1L << e.ordinal());
return L;
}
//Converts Enums to mask:
public static <E extends Enum<E>> long pack(final E... ez) { //Supports Enums with up to 64 values.
long L = 0;
for (final Enum e: ez)
L = L | (1L << e.ordinal());
return L;
}
public static Color transparent(final Color c, final int alpha) {
return new Color(c.getRed(), c.getGreen(), c.getBlue(), alpha);
}
public static void setOperatedSize(final Dimension input, final BiFunction<Integer, Integer, Integer> operator, final Dimension inputOutput) {
inputOutput.setSize(operator.apply(input.width, inputOutput.width), operator.apply(input.height, inputOutput.height));
}
//Prompts user to input some text.
public static void inputTitle(final Component parent, final String titleID, final Consumer<String> consumer) {
final String userIn = JOptionPane.showInputDialog(parent, "Enter " + titleID.toLowerCase() + "'s title:", "Enter title", JOptionPane.QUESTION_MESSAGE);
if (userIn != null) {
if (userIn.isEmpty())
JOptionPane.showMessageDialog(parent, titleID + "'s name cannot be empty...", "Oups!", JOptionPane.INFORMATION_MESSAGE);
else
consumer.accept(userIn);
}
}
//Applies an action to every child component of the container recursively as well as to the given Container.
public static void consumeComponentsRecursively(final Container container, final Consumer<Component> consumer) {
for (final Component child: container.getComponents())
if (child instanceof Container)
consumeComponentsRecursively((Container) child, consumer);
else
consumer.accept(child);
consumer.accept(container);
}
public static Dimension getGoodEnoughSize(final Component comp, final Dimension defaultSize) {
final Dimension dim = new Dimension(defaultSize);
if (comp != null) { // && comp.isVisible()) {
/*Start with default size, and then listen to max and min
(if both max and min are set, we prefer the min one):*/
if (comp.isMaximumSizeSet())
setOperatedSize(comp.getMaximumSize(), Math::min, dim);
if (comp.isMinimumSizeSet())
setOperatedSize(comp.getMinimumSize(), Math::max, dim);
}
return dim;
}
public static class ManualLayout implements LayoutManager, Serializable {
public Dimension getLayoutComponentSize(final Component comp) {
return getGoodEnoughSize(comp, (comp.getWidth() <= 0 && comp.getHeight() <= 0)? comp.getPreferredSize(): comp.getSize());
}
#Override public void addLayoutComponent(final String name, final Component comp) { }
#Override public void removeLayoutComponent(final Component comp) { }
#Override public Dimension preferredLayoutSize(final Container parent) { return minimumLayoutSize(parent); } //Preferred and minimum coincide for simplicity.
#Override
public Dimension minimumLayoutSize(final Container parent) {
final Component[] comps = parent.getComponents();
if (comps == null || comps.length <= 0)
return new Dimension();
final Rectangle totalBounds = new Rectangle(comps[0].getLocation(), getLayoutComponentSize(comps[0]));
for (int i = 1; i < comps.length; ++i)
totalBounds.add(new Rectangle(comps[i].getLocation(), getLayoutComponentSize(comps[i])));
return new Dimension(totalBounds.x + totalBounds.width, totalBounds.y + totalBounds.height);
}
#Override
public void layoutContainer(final Container parent) {
for (final Component comp: parent.getComponents())
comp.setSize(getLayoutComponentSize(comp)); //Just set the size. The locations are taken care by the class's client supposedly.
}
}
public static abstract class RectangularPanel<R extends RectangularShape> extends JPanel {
private R shape;
private Area cache; /*Use a cache so as not to have to create new Areas every time we need
to intersect the clip (see 'createClip' method). Also, the client can modify the current
shape but not the cache upon which the shape and painting of the panel depend.*/
public RectangularPanel(final double width, final double height) {
super();
super.setOpaque(false);
cache = new Area(shape = createShape(0, 0, width, height));
super.addComponentListener(new ComponentAdapter() {
#Override
public void componentResized(final ComponentEvent cevt) {
cache = new Area(shape = createShape(0, 0, getWidth(), getHeight()));
revalidate();
repaint();
}
});
}
protected abstract R createShape(final double x, final double y, final double width, final double height);
protected abstract double getArcWidth();
protected abstract double getArcHeight();
protected final R getShape() { return shape; }
#Override public boolean contains(final int x, final int y) { return cache.contains(x, y); }
protected Shape createClip(final Shape originalClip) {
if (originalClip == null)
return cache;
final Area clip = new Area(originalClip);
clip.intersect(cache);
return clip;
}
#Override
public void paint(final Graphics g) { //print() and update() rely on paint(), so we only need to override this one...
g.setClip(createClip(g.getClip()));
super.paint(g);
}
}
public static class VennTool implements Runnable {
protected static final Object LAYER_USER_CONTROLS = JLayeredPane.DEFAULT_LAYER, LAYER_VENN_SET = JLayeredPane.PALETTE_LAYER, LAYER_VENN_LABEL = JLayeredPane.MODAL_LAYER;
public class VennDrawPanel extends JPanel {
private final JLayeredPane pane;
private final int drawingOffsetY, drawingOffsetX;
private final JCheckBox attachMode, collisionMode;
private final JPanel ctrl;
public VennDrawPanel(final GraphicsConfiguration gconf) {
super(new BorderLayout());
pane = new JLayeredPane();
super.add(pane, BorderLayout.CENTER);
final ManualLayout layout = new ManualLayout();
pane.setLayout(layout);
final Dimension prefsz = new Dimension(gconf.getBounds().getSize());
prefsz.width = (2 * prefsz.width) / 3;
prefsz.height = (2 * prefsz.height) / 3;
final JButton createLabel = new JButton("Create label");
final JButton createSet = new JButton("Create set");
attachMode = new JCheckBox("Attach mode", true);
collisionMode = new JCheckBox("Collision mode", true);
ctrl = new JPanel(new FlowLayout(FlowLayout.LEADING, 0, 5));
ctrl.add(createLabel);
ctrl.add(createSet);
ctrl.add(attachMode);
ctrl.add(collisionMode);
drawingOffsetX = layout.getLayoutComponentSize(ctrl).width;
prefsz.width = Math.max(prefsz.width, drawingOffsetX);
drawingOffsetY = prefsz.height / 8;
pane.setPreferredSize(prefsz);
pane.add(ctrl, LAYER_USER_CONTROLS);
createLabel.addActionListener(e -> inputTitle(this, "Label", VennTool.this::createLabel));
createSet.addActionListener(e -> inputTitle(this, "Set", VennTool.this::createSet));
}
protected void setControlsEnabled(final boolean enable) { consumeComponentsRecursively(ctrl, c -> c.setEnabled(enable)); }
public boolean isAttachModeSelected() { return attachMode.isSelected(); }
public boolean isCollisionModeSelected() { return collisionMode.isSelected(); }
protected Point getCreationLocation() { return new Point(drawingOffsetX + 50, drawingOffsetY / 2); }
public void addSet(final VennSet set) {
set.setLocation(getCreationLocation());
pane.add(set, LAYER_VENN_SET, 0);
pane.revalidate();
pane.repaint();
}
public void addLabel(final VennLabel label) {
label.setLocation(getCreationLocation());
pane.add(label, LAYER_VENN_LABEL, 0);
pane.revalidate();
pane.repaint();
}
}
protected static class VennBorder extends LineBorder {
public VennBorder(final int thickness) { super(Color.BLACK, thickness, true); }
#Override
public void paintBorder(final Component c, final Graphics g, final int x, final int y, final int width, final int height) {
if (c instanceof VennControl) {
final VennControl ctrl = (VennControl) c;
Graphics2D g2d = (Graphics2D) g.create();
try {
g2d.setColor(ctrl.getBorderColor());
final int t2 = thickness + thickness;
final int aw = (int) Math.round(Math.min(width, ctrl.getArcWidth())), ah = (int) Math.round(Math.min(height, ctrl.getArcHeight()));
final Path2D path = new Path2D.Float(Path2D.WIND_EVEN_ODD);
path.append(new RoundRectangle2D.Float(x, y, width, height, aw, ah), false);
path.append(new RoundRectangle2D.Double(x + thickness, y + thickness, width - t2, height - t2, aw, ah), false);
g2d.fill(path);
}
finally {
g2d.dispose();
}
}
else
super.paintBorder(c, g, x, y, width, height);
}
}
public static <C1 extends VennControl<C1, C2, ?, ?>, C2 extends VennControl<C2, C1, ?, ?>> void attach(final C1 c1, final C2 c2) { //Utility method.
c1.getAttachments().add(c2);
c2.getAttachments().add(c1);
}
public static <C1 extends VennControl<C1, C2, ?, ?>, C2 extends VennControl<C2, C1, ?, ?>> void detach(final C1 c1, final C2 c2) { //Utility method.
c1.getAttachments().remove(c2);
c2.getAttachments().remove(c1);
}
protected abstract class VennControl<C1 extends VennControl<C1, C2, R1, R2>, C2 extends VennControl<C2, C1, R2, R1>, R1 extends RectangularShape, R2 extends RectangularShape> extends RectangularPanel<R1> {
private Color bg;
private boolean highlighted, selected;
private final LinkedHashSet<C2> attachments;
public VennControl(final String title, final double width, final double height) {
super(width, height);
super.setLayout(new GridBagLayout());
super.add(new JLabel(Objects.toString(title), JLabel.CENTER));
super.setBorder(new VennBorder(2));
super.setSize((int) Math.ceil(width), (int) Math.ceil(height));
attachments = new LinkedHashSet<>();
bg = transparent(Color.LIGHT_GRAY, 127);
highlighted = selected = false;
final MouseAdapter relocationMA = new MouseAdapter() {
private Point moveAnchor;
private LinkedHashSet<C2> intersections;
#Override
public void mousePressed(final MouseEvent mevt) {
if (pack(MouseEventFlag.buttonSetOf(mevt)) == pack(MouseEventFlag.BUTTON_LEFT) && moveAnchor == null && intersections == null) {
VennTool.this.drawPanel.setControlsEnabled(false);
moveAnchor = mevt.getPoint();
setSelected(true);
intersections = findIntersections(0, 0);
intersections.forEach(c2 -> c2.setHighlighted(true));
}
}
#Override
public void mouseDragged(final MouseEvent mevt) {
final int dx = mevt.getX() - moveAnchor.x, dy = mevt.getY() - moveAnchor.y;
final boolean attach = VennTool.this.drawPanel.isAttachModeSelected(), collisions = VennTool.this.drawPanel.isCollisionModeSelected();
final LinkedHashSet<C2> newIntersections = findIntersections(dx, dy);
if (MouseEventFlag.buttonSetOf(mevt).contains(MouseEventFlag.BUTTON_LEFT) && moveAnchor != null && intersections != null && (!attach || newIntersections.containsAll(getAttachments())) && (!collisions || !collides(dx, dy))) {
setLocation(getX() + dx, getY() + dy);
LinkedHashSet<C2> setHighlight = (LinkedHashSet<C2>) intersections.clone();
setHighlight.removeAll(newIntersections);
if (!attach)
setHighlight.forEach(c2 -> detach(c2, (C1) VennControl.this));
setHighlight.forEach(c2 -> c2.setHighlighted(false));
setHighlight = (LinkedHashSet<C2>) newIntersections.clone();
setHighlight.removeAll(intersections);
setHighlight.forEach(c2 -> c2.setHighlighted(true));
intersections = newIntersections;
}
}
#Override
public void mouseReleased(final MouseEvent mevt) {
if (pack(MouseEventFlag.buttonSetOf(mevt)) == pack(MouseEventFlag.BUTTON_LEFT) && moveAnchor != null && intersections != null) {
intersections.forEach(c2 -> c2.setHighlighted(false));
final VennDrawPanel vdp = VennTool.this.drawPanel;
if (vdp.isAttachModeSelected())
intersections.forEach(c2 -> attach(c2, (C1) VennControl.this));
moveAnchor = null;
intersections = null;
setSelected(false);
vdp.setControlsEnabled(true);
}
}
};
super.addMouseListener(relocationMA);
super.addMouseMotionListener(relocationMA);
}
protected LinkedHashSet<C2> findIntersections(final double dx, final double dy) {
final R1 r1tmp = getShape();
final R1 r1 = createShape(getX() + dx + r1tmp.getX(), getY() + dy + r1tmp.getY(), r1tmp.getWidth(), r1tmp.getHeight());
final LinkedHashSet<C2> intersections = new LinkedHashSet<>(), possibilities = getPossibleIntersections();
possibilities.forEach(c2 -> {
final R2 r2tmp = c2.getShape();
final R2 r2 = c2.createShape(c2.getX() + r2tmp.getX(), c2.getY() + r2tmp.getY(), r2tmp.getWidth(), r2tmp.getHeight());
if (intersect(r1, r2))
intersections.add(c2);
});
return intersections;
}
public LinkedHashSet<C2> getAttachments() { return attachments; }
protected abstract boolean intersect(final R1 r1, final R2 r2);
protected abstract LinkedHashSet<C2> getPossibleIntersections();
protected abstract boolean collides(final double dx, final double dy);
public void setHighlighted(final boolean highlighted) {
if (highlighted != this.highlighted) {
this.highlighted = highlighted;
repaint();
}
}
public void setSelected(final boolean selected) {
if (selected != this.selected) {
this.selected = selected;
repaint();
}
}
public void setColor(final Color c) {
if (!bg.equals(c)) {
bg = Objects.requireNonNull(c);
repaint();
}
}
public Color getBorderColor() {
return selected? Color.GREEN: (highlighted? Color.CYAN: Color.BLACK);
}
#Override
protected void paintComponent(final Graphics g) {
super.paintComponent(g);
g.setColor(bg);
g.fillRect(0, 0, getWidth(), getHeight());
}
}
protected class VennLabel extends VennControl<VennLabel, VennSet, Rectangle2D, Ellipse2D> {
public VennLabel(final String title) { super(title, 0, 0); }
#Override protected Rectangle2D createShape(double x, double y, double width, double height) { return new Rectangle2D.Double(x, y, width, height); }
#Override protected double getArcWidth() { return 0; }
#Override protected double getArcHeight() { return 0; }
#Override protected boolean intersect(final Rectangle2D r1, final Ellipse2D r2) { return r2.intersects(r1); }
#Override protected LinkedHashSet<VennSet> getPossibleIntersections() { return VennTool.this.sets; }
#Override
protected boolean collides(final double dx, final double dy) {
Rectangle2D tmp = getShape();
final Rectangle2D thisShape = createShape(getX() + dx + tmp.getX(), getY() + dy + tmp.getY(), tmp.getWidth(), tmp.getHeight());
for (final VennLabel label: VennTool.this.labels)
if (label != this) {
tmp = label.getShape();
tmp = label.createShape(label.getX() + tmp.getX(), label.getY() + tmp.getY(), tmp.getWidth(), tmp.getHeight());
if (tmp.intersects(thisShape))
return true;
}
return false;
}
}
protected class VennSet extends VennControl<VennSet, VennLabel, Ellipse2D, Rectangle2D> {
public VennSet(final String title, final double radius) {
super(title, radius + radius, radius + radius);
final Dimension sz = super.getPreferredSize();
sz.width = sz.height = Math.max(Math.max(sz.width, super.getWidth()), Math.max(sz.height, super.getHeight()));
super.setSize(sz);
}
#Override protected Ellipse2D createShape(double x, double y, double width, double height) { return new Ellipse2D.Double(x, y, width, height); }
#Override protected double getArcWidth() { return getWidth(); }
#Override protected double getArcHeight() { return getHeight(); }
#Override protected boolean intersect(final Ellipse2D r1, final Rectangle2D r2) { return r1.intersects(r2); }
#Override protected LinkedHashSet<VennLabel> getPossibleIntersections() { return VennTool.this.labels; }
#Override protected boolean collides(final double dx, final double dy) { return false; } //Never collides with anything.
}
private final JFrame frame;
private final VennDrawPanel drawPanel;
private final LinkedHashSet<VennSet> sets;
private final LinkedHashSet<VennLabel> labels;
public VennTool(final GraphicsConfiguration gconf) {
drawPanel = new VennDrawPanel(gconf);
frame = new JFrame("Collisionless Venn Tool", gconf);
sets = new LinkedHashSet<>();
labels = new LinkedHashSet<>();
}
public void createSet(final String title) {
final VennSet set = new VennSet(title, 100);
sets.add(set);
drawPanel.addSet(set);
drawPanel.revalidate();
drawPanel.repaint();
}
public void createLabel(final String title) {
final VennLabel label = new VennLabel(title);
labels.add(label);
drawPanel.addLabel(label);
drawPanel.revalidate();
drawPanel.repaint();
}
#Override
public void run() {
if (SwingUtilities.isEventDispatchThread()) {
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.getContentPane().add(drawPanel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
else
SwingUtilities.invokeLater(this);
}
}
public static void main(final String[] args) {
final VennTool tool = new VennTool(GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration());
SwingUtilities.invokeLater(() -> {
tool.createSet("Drag this set!");
tool.createLabel("Drag this label!");
});
tool.run();
}
}
Why so many lines?
Mainly because you said what you have implemented but did not provide any code. So I had to design everything from scratch.
From a custom layout manager, to custom shaped panels and borders, to the actual VennTool, VennDrawPanel, VennSet and VennLabel representations.
So what does it include?
Here is something like a class diagram which is my best effort so far for your requirements:
Boxes represent classes, pointers represent inheritance and nested boxes represent nested classes. Black text represents class names and cyan text represents comments/details.
The code starts with seemingly unrelated classes, but they act something like a framework to work with in the VennTool.
To start, MouseEventFlag enum is just there to convert MouseEvents to enum constants so as to test which button/key is pressed for each mouse event.
Then there are some static methods that do general things like packing EnumSets to long masks, converting colors to transparent, obtaining and operating on Component sizes etc...
Then there is the custom LayoutManager class, named ManualLayout. As the name suggests, you have to put the Components of a Container with a ManualLayout to where you want them to be with (for example) setBounds or setLocation methods. Then the layout calculates a preferred size for each Component in the Container in order to return minimum and preferred layout sizes. The layoutContainer method simply sets the sizes of the included Components and does not move their location. The ManualLayout takes into account all sizes of each Component (ie current size, preferred, minimum and maximum size). Instance of such a layout is used to lay out the Components in the drawing panel (will talk about that in this post).
Then there is the RectangularPanel class which is a custom shaped JPanel which supports any java.awt.geom.RectangularShape shape to shape out the panel itself and paint it. This is a superclass of the components which compose a Venn set (such as the Venn set itself and the labels added to the drawing panel).
Then there starts the VennTool class. This class implements Runnable to run the simulation. It encloses all the classes representing Venn components/elements as well as the drawing panel. Client classes are only supposed to see the VennTool class (which provides methods for creating Venn sets and labels) and nothing else (except if they are subclasses of it). Handling and modifying the Venn sets is up to the user. You can of course modify the class to return all the Venn components already in the simulation at any time you need to save them as a project for example later, but such a functionality is not implemented here. Just add some getters and file I/O to do this yourself.
Then, inside the VennTool now, is the VennDrawPanel which is the drawing panel which I was talking about all above, which is responsible for the layout of the whole simulation. Here we add the Venn sets, labels and user controls (such as buttons to let the users define their actions). The VennDrawPanel has a JLayeredPane which uses a ManualLayout, achieving different layers and locations for each component. In particular, the first layer is composed by the buttons. On top of that layer are the Venn sets and finally on top of that are the Venn labels. As a future idea, you can implement a new layer on which the dragged component will go upon dragging (for example you may want a Venn set which the user is currently dragging to be at the layer which is above all others).
Then, there is the custom Border class, named (you guessed it) VennBorder. It is enclosed in VennTool only to show the scope of it and to allow it interact with each Venn component. It's purely inpired by and also extending javax.swing.border.LineBorder class.
Then, there is the VennControl class which represents each Venn component (ie VennSets and VennLabels). It is responsible to consume mouse guestures (such as dragging a Venn set around), for collision detection and resolution of labels and also responsible for attaching/detaching sets to labels and labels to sets.
The custom VennBorder and VennControl classes are defined by a shape of type java.awt.geom.RectangularShape. This allows that each subclass can be for example of Ellipse2D shape (as in the case of VennSets which are circular) or a Rectangle2D (as in the case of VennLabels). It also defines an arc width and height to support RoundRectangle2D but that just came along the road to support both Ellipse2D and Rectangle2D in a single panel/border.
Finally, there is the VennSet and VennLabel classes which represent a Venn set and a label to be added to a Venn set correspondingly.
Attach/Detach mode:
While enabled, each mouse release will attach the label you hold with all overlapping sets (or the set you hold with all overlapping labels). It will also not let labels/sets go non-overlapping with their attached sets/labels. Disable this mode in order to move any VennControl freely around detaching it on mouse release.
Collision detection mode:
As the name suggests, when enabled, labels will collide and not overlap with each other. While when disabled, they will not.
I am currently making a RPG hack and slash in java with just the standard libraries.
For these games , I had made a CustomButton class in an independent package that I just copy and paste in almost all of my games and then make some game-specific modifications.
This is the class::
import java.awt.Canvas;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.WritableRaster;
import objects.Collectable;
public final class CustomButton implements MouseListener {
// private BufferedImage image;
private String text = "";
private boolean pressed = false;
private int x;
private int y;
private int width;
private int height;
private Color currentColor, defaultColor, hoverColor, pressColor;
private Point mousePoint = new Point(0, 0);
private ButtonListener btnListener = null;
private Canvas canvasObject;
private BufferedImage image;
private BufferedImage darkImage;
private String actionCommand = "default";
private Collectable object;
private boolean enabled;
/*
* private CustomButton(Canvas canvasObject,JFrame frame) { this.x=100;
* this.y=100; this.width=100; this.height=100;
*
* canvasObject.addMouseListener(this); currentColor=new Color(255,255,255);
* defaultColor=new Color(255,255,255); hoverColor=new Color(255,255,255);
* pressColor=new Color(255,255,255); }
*/
public CustomButton(int x, int y, int width, int height, Canvas canvasObject) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.canvasObject = canvasObject;
canvasObject.addMouseListener(this);
currentColor = Color.GREEN;
currentColor = new Color(255, 255, 255);
defaultColor = new Color(255, 255, 255);
hoverColor = new Color(255, 255, 255);
pressColor = new Color(255, 255, 255);
enabled = true;
}
public CustomButton(int x, int y, int width, int height, Canvas canvasObject, BufferedImage image,
Collectable object) {
this.image = image;
this.darkImage = darkenImage(image);
this.x = x;
this.y = y;
this.width = width;
this.height = height;
canvasObject.addMouseListener(this);
currentColor = Color.GREEN;
currentColor = new Color(255, 255, 255);
defaultColor = new Color(255, 255, 255);
hoverColor = new Color(255, 255, 255);
pressColor = new Color(255, 255, 255);
this.canvasObject = canvasObject;
this.object = object;
enabled = true;
}
public void render(Graphics g) {
if (image == null) {
g.setColor(currentColor);
if (!pressed)
g.fillRect(this.x, this.y, width, height);
else
g.fill3DRect(this.x, this.y, width, height, true);
g.setColor(Color.BLACK);
g.drawString(text, this.x + 10, this.y + 15);
} else {
if (enabled) {
g.drawImage(image, x, y, width, height, null);
} else {
g.drawImage(darkImage, x, y, width, height, null);
}
}
}
public Rectangle getBounds() {
return new Rectangle(x, y, width, height);
}
public void tick() {
mousePoint = getMouseLocation();
changeColor();
}
private Point getMouseLocation() {
int x = 0;
int y = 0;
try {
x = (int) (canvasObject.getMousePosition().getX());
y = (int) (canvasObject.getMousePosition().getY());
} catch (NullPointerException nl) {
}
return new Point(x, y);
}
public void changeColor() {
if (!pressed) {
if (getBounds().contains(mousePoint))
currentColor = hoverColor;
else
currentColor = defaultColor;
} else {
currentColor = pressColor;
}
}
public void addButtonListener(ButtonListener btnListener) {
this.btnListener = btnListener;
}
public void mouseEntered(MouseEvent e) {
}
public void mouseExited(MouseEvent e) {
}
public void mouseClicked(MouseEvent e) {
if (enabled) {
if (btnListener != null) {
if (getBounds().contains(mousePoint)) {
ButtonID id = ButtonID.UNDETERMINABLE;
if (e.getButton() == MouseEvent.BUTTON1) id = ButtonID.LEFT;
if (e.getButton() == MouseEvent.BUTTON2) id = ButtonID.RIGHT;
btnListener.buttonClicked(new ButtonEvent(id, object, actionCommand));
}
}
}
}
public void mousePressed(MouseEvent e) {
if (getBounds().contains(mousePoint)) pressed = true;
}
public void mouseReleased(MouseEvent e) {
pressed = false;
}
public void setActionCommand(String actionCommand) {
this.actionCommand = actionCommand;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public void setDefaultColor(Color c) {
defaultColor = c;
}
public void setHoverColor(Color c) {
hoverColor = c;
}
public void setPressColor(Color c) {
pressColor = c;
}
public Collectable getObject() {
return object;
}
public void setObject(Collectable object) {
this.object = object;
}
public void destroy() {
canvasObject.removeMouseListener(this);
}
public void disable() {
enabled = false;
}
public void enable() {
enabled = true;
}
public boolean isEnabled() {
return enabled;
}
private BufferedImage darkenImage(BufferedImage image) {
int width = image.getWidth();
int height = image.getHeight();
image = deepCopy(image);
WritableRaster raster = image.getRaster();
for (int xx = 0; xx < width; xx++) {
for (int yy = 0; yy < height; yy++) {
int[] pixels = raster.getPixel(xx, yy, (int[]) null);
pixels[0] -= 50;
pixels[1] -= 50;
pixels[2] -= 50;
pixels[0] = Math.max(pixels[0], 0);
pixels[1] = Math.max(pixels[0], 0);
pixels[2] = Math.max(pixels[0], 0);
raster.setPixel(xx, yy, pixels);
}
}
return image;
}
private BufferedImage deepCopy(BufferedImage bi) {
ColorModel cm = bi.getColorModel();
boolean isAlphaPremultiplied = cm.isAlphaPremultiplied();
WritableRaster raster = bi.copyData(null);
return new BufferedImage(cm, raster, isAlphaPremultiplied, null);
}
}
As you can probably see from the mouseClicked() method that a event is sent to the ButtonListener class.The ButtonListener interface is also declared in the package.
This button is drawn on the canvas itself.For example, in the levelmap, there is a button in the lower-right corner, which on clicking will open the inventory.Let this button be called btn_INV.
Until now,I have been taking inputs for moving the player through the keyboard.But I plan to change keyInput to mouse input where the player will move to the tile where the user clicks on.
For that I have to make a class , sayMouseInput that implements MouseListener.Now the problem is that when I click btn_INV , not only will the button generate an event, but also, since the button is actually drawn on the canvas, the MouseInput class will also get an event as to the tile to which the player wants to moving.Now, I thought that when the MouseInput class gets a MouseEvent, it will check with the buttons as whenever there is a mouse click on the canvas, the buttons always gets informed, although it may not generate a ButtonEvent, as you can see from the code.But this is a rather bad method and highly inefficient.So , I want another method for this.
NOTE : I thought of creating another canvas for displaying the HUD and the btn_INV and other such buttons, but that does not really solve the issue as much as bypass it.
I think there's two ways to solve this problem :
The first will be to divide your game screen into two parts : one for the buttons, the other for the tiles, so you can test whilecatching a MouseEvent if the click is positionned on the tiles or not. Note that the tiles can be placed on a new button. This solution is easy to implement but you won't be able to place buttons on your tile's area.
The second will be to create a "ButtonManager". Your game class will have this button manager that will listen for mouse event and send then to the buttons. Buttons won't listen directly to this event. They will say one after one if the click is on their bounds and if no button have been fired it means that the click occured on the tiles. This method is little bit more difficult to implement but allows you to create a priority beetween buttons and so buttons would be able to have thei bounds intersecting !
Hope it helped you !
Generally speaking, concepts like button listeners are used in applications, not games.
Games generally work with a game loop which will have an update and a draw phase. During the update phase, user input is caught (well... more checked than caught, but I'll get to that in a moment) and interpreted.
So you'd just have one big mouse listener that would then check to see which button might be pressed or if the mouse is within the game area so the character should get the move command.
Of course technically that mouse listener shouldn't directly do anything, as it is bound to the swing thread. It should just change some variables (like: "left mouse button is down, position is x, y") that will then be checked in your update phase, which will then actually work with it. That way, you're no longer dependant on the swing thread (which isn't ideal for games anyway) and - almost as important - your game logic is completely predictable.
However, you'd still have to make sure your draw method gets called regularly and actually paints what and when you want it to paint. That's besides the question though, so I won't go into more detail on it.
Keep in mind that even swing isn't just some magical construct. When the mouse is clicked, swing will also iterate through all elements you have and see if one of them should get an event. If you write your own Buttons, that's what you're gonna have to do, too.
Giving each button it's own Mouse Listener will only get confusing the bigger your game gets. It will also destroy your game loop as mouse events can be thrown and caught at any time regardless of what you're doing at the moment. If your game logic happens in a seperate thread from swing (which it should), it's even possible that you get an event while you're processing something. Which will screw up your result.
This may also lead to a couple of very odd one-time errors that you don't understand and can't reproduce. So be very careful with that.
I am trying to create application where user can draw lines in Canvas. User can select direction from dropdown list and enter length of line. First line starts from center of Canvas, the next one starts where the previous one ended and so on - user can draw multiple lines one by one and all lines are connected.
I have two classes - TurtleApplet which creates GUI and Canvas with program logic:
public class TurtleApplet extends JApplet implements ActionListener
{
private JComboBox direction;
private JRadioButton activeButton, passiveButton;
private Button drawButton;
private ButtonGroup group;
private TextField pixels;
private Canvas canvas;
private JPanel panel;
private JPanel panelRadio;
private Button quitPr;
public void init()
{
//directions
String[] directionStrings = { "Right", "Left", "Up", "Down"};
direction = new JComboBox(directionStrings);
//direction.setSelectedIndex(4);
//Buttons
activeButton = new JRadioButton("Aktīvs");
passiveButton = new JRadioButton("Neaktīvs");
quitPr = new Button("Iziet");
//Canvas
canvas = new Canvas();
//canvas.setSize(600, 500);
//canvas.setBackground(Color.red);
canvas.setBorder(BorderFactory.createTitledBorder("Turtle drawing"));
//Panels
panel = new JPanel();
panelRadio =new JPanel();
panel.setLayout(new FlowLayout());
panelRadio.setLayout(new FlowLayout());
//actionListener
activeButton.addActionListener(this);
passiveButton.addActionListener(this);
activeButton.setSelected(true);
quitPr.addActionListener(this);
//Add radiobuttons
group = new ButtonGroup();
group.add(activeButton);
group.add(passiveButton);
//Add Buttons to panel
panelRadio.add(activeButton);
panelRadio.add(passiveButton);
//textfield
pixels = new TextField(12);
//Draw button
drawButton = new Button("Zīmēt");
drawButton.addActionListener(this);
direction.addActionListener(this);
panel.add(panelRadio);
panel.add(pixels);
panel.add(direction);
panel.add(drawButton);
panel.add(quitPr);
getContentPane().add(panel,"North");
getContentPane().add(canvas, "Center");
setSize(650,550);
}
public void actionPerformed( ActionEvent e)
{
if ( e.getSource() == activeButton ) {
drawButton.setVisible(true);
pixels.setEditable(true);
} else if (e.getSource() == passiveButton) {
drawButton.setVisible(false);
pixels.setEditable(false);
} else if(e.getSource() == quitPr){
System.exit(0);
}else if(e.getSource() == drawButton){
int y = Integer.parseInt(pixels.getText());
canvas.addPatt(direction.getSelectedIndex(), Integer.parseInt(pixels.getText()));
repaint();
}
//repaint();
}
}
public class Canvas extends JPanel {
private static final int RIGHT=0, LEFT=1, UP=2, DOWN=3;
public static final int WIDTH=600, HEIGHT=500;
private int direction = 0 ;
private int pixels;
//rivate List points;
public Polygon t = new Polygon();
//public Dimension d = getSize();
public int x = WIDTH/2;
public int y = HEIGHT/2;
public Canvas() {
setSize(WIDTH, HEIGHT);
}
public void addPatt(int pat, int lev) {
direction = pat;
pixels = lev;
}
public void paintComponent(Graphics g) {
switch (direction) {
case LEFT:
drawLineLeft(g, pixels);
break;
case RIGHT:
drawLineRight(g, pixels);
break;
case UP:
drawLineUp(g, pixels);
break;
case DOWN:
drawLineDown(g, pixels);
break;
}
}
private void drawLineLeft(Graphics g, int pix){
if(pix > 0){
g.drawLine(x, y, x-10*pix, y);//left
x =x -10*pix;
}
}
private void drawLineUp(Graphics g, int pix){
if(pix > 0){
g.drawLine(x, y, x, y-10*pix);//up
y = y-10*pix;
}
}
private void drawLineRight(Graphics g, int pix){
if(pix > 0){
g.drawLine(x, y, x+10*pix, y);//right
x = x+10*pix;
}
}
private void drawLineDown(Graphics g, int pix){
if(pix > 0){
g.drawLine(x, y, x, y+10*pix);// down
y = y+10*pix;
}
}
}
Applet works, but the problem is to save previous lines, when new one is drawn. When user enters direction and length of line and presses the button, new line appears on the screen, but the previous one disappears. I know that the problem is with paintComponent method, but I don't know how to exactly correct my code to make all lines visible. I was suggested to store point coordinates in array and then paint lines by looping throught array in paintComponent, but I don't know how to achieve this. Maybe there is better solution?
As I said in your last question, the case state is like an if-else, you only ever allow it to draw a single line. You need to maintain a List of "lines" which can be iterated over each time the paintComponent method is called
Because a line is represented by multiple properties, it's best to encapsulate that information into a simple class or POJO
public enum Direction {
UP, DOWN, LEFT, RIGHT
}
public class Line {
private Direction direction;
private int length;
public Line(Direction direction, int length) {
this.direction = direction;
this.length = length;
}
public Direction getDirection() {
return direction;
}
public int getLength() {
return length;
}
}
Here, I've separated the direction properties into a simple enum, this allows you to reference the properties more easily elsewhere within your program
Then you maintain a List of Lines, which when paintComponent is called, you simply re-iterate over and repaint...
public class Canvas extends JPanel {
public static final int WIDTH = 600, HEIGHT = 500;
public int x = WIDTH / 2;
public int y = HEIGHT / 2;
private List<Line> lines;
public Canvas() {
lines = new ArrayList<>(25);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(WIDTH, HEIGHT);
}
public void addPatt(Direction direction, int length) {
lines.add(new Line(direction, length));
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
for (Line line : lines) {
switch (line.getDirection()) {
case UP:
drawLineUp(g, line.getLength());
break;
case DOWN:
drawLineDown(g, line.getLength());
break;
case LEFT:
drawLineLeft(g, line.getLength());
break;
case RIGHT:
drawLineDown(g, line.getLength());
break;
}
}
}
private void drawLineLeft(Graphics g, int pix) {
if (pix > 0) {
g.drawLine(x, y, x - 10 * pix, y);//left
x = x - 10 * pix;
}
}
private void drawLineUp(Graphics g, int pix) {
if (pix > 0) {
g.drawLine(x, y, x, y - 10 * pix);//up
y = y - 10 * pix;
}
}
private void drawLineRight(Graphics g, int pix) {
if (pix > 0) {
g.drawLine(x, y, x + 10 * pix, y);//right
x = x + 10 * pix;
}
}
private void drawLineDown(Graphics g, int pix) {
if (pix > 0) {
g.drawLine(x, y, x, y + 10 * pix);// down
y = y + 10 * pix;
}
}
}
Remember, painting in Swing is destructive, you are expected to repaint the entire state of the component every time the paintComponent is called.
See Painting in AWT and Swing and Performing Custom Painting for more details about how painting works
I was suggested to store point coordinates in array and then paint lines by looping throught array in paintComponent, but I don't know how to achieve this.
It depends on your exact requirement.
If you need the ability to add/remove lines, then this might be the best approach.
If you only need the ability to add lines then you might want to paint the lines directly to a BufferedImage and then just display the BufferedImage as an Icon on a JLabel.
Check out Custom Painting Approaches which compares the two approaches and provides working examples of both approaches.
I have a JFrame containing 3 JPanels; Options, menu, canvas. In options there are a number of JButtons representing shapes. The aim is to click on the JButton of a shape e.g. rectangle, then click anywhere on the canvas and the shape will be drawn there.
For some reason, the shape does not always get drawn, it is only drawn when I click somewhere in the top left area of the canvas. Also the shape seems to randomly change size depending on where I click.
Here are some of my code snippets, it's probably a small error but I just can't seem to find it.
Shape:
public class Shape extends JPanel {
protected int xLocation;
protected int yLocation;
protected int numberOfSides;
protected String areaInfo;
protected String perimeterInfo;
public int getXLocation() {
return xLocation;
}
public void setXLocation(int xLocation) {
this.xLocation = xLocation;
}
public int getYLocation() {
return yLocation;
}
public void setYLocation(int yLocation) {
this.yLocation = yLocation;
}
public int getNumberOfSides() {
return numberOfSides;
}
public Shape(int xLocation, int yLocation, int numberOfSides) {
this.xLocation = xLocation;
this.yLocation = yLocation;
this.numberOfSides = numberOfSides;
}
}
Rectangle:
import java.awt.Color;
import java.awt.Graphics;
public class Rectangle extends Shape {
private int width;
private int height;
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
public Rectangle(int xLocation, int yLocation, int width, int height ) {
super(xLocation, yLocation, 4);
this.width = width;
this.height = height;
this.areaInfo = "Multiply width * height";
this.perimeterInfo = "Add the lengths of each side";
}
public void paint(Graphics g){
g.setColor(Color.BLACK);
g.fillRect(xLocation, yLocation, width, height);
}
}
Canvas:
public class DrawingCanvas extends JPanel implements Serializable{
private ArrayList<Shape> shapeList;
OptionsPanel options;
public void addShape(Shape shape){
shapeList.add(shape);
this.add(shape);
this.repaint();
}
public DrawingCanvas(){
shapeList = new ArrayList<Shape>();
}
}
Frame:
public class DrawingFrame extends JFrame implements MouseListener, MouseMotionListener {
private OptionsPanel options;
private DrawingCanvas canvas;
private MenuBar menu;
Shape s; //shape to be manipulated
public DrawingFrame(){
options = new OptionsPanel();
canvas = new DrawingCanvas();
menu = new MenuBar();
//options.setBounds(0, 0, 100, 500);
options.setBackground(Color.GREEN);
canvas.setBackground(Color.yellow);
menu.setSize(1000,200);
menu.setBackground(Color.magenta);
this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
this.setSize(1000,500);
this.setTitle("Drawing Application");
this.setLayout(new BorderLayout());
this.getContentPane().add(options, BorderLayout.WEST);
this.getContentPane().add(canvas, BorderLayout.CENTER);
this.getContentPane().add(menu, BorderLayout.PAGE_START);
this.setVisible(true);
options.createRectangleButton.addMouseListener(this);
options.createSquareButton.addMouseListener(this);
options.createCircleButton.addMouseListener(this);
options.createTriangleButton.addMouseListener(this);
options.clearButton.addMouseListener(this);
canvas.addMouseListener(this);
canvas.addMouseMotionListener(this);
}
#Override
public void mouseClicked(MouseEvent e) {
boolean createShape = true;
if(e.getSource().equals(options.createRectangleButton)){
createShape = true;
s = new Rectangle(50,50,400,200);
s.addMouseListener(this);
s.addMouseMotionListener(this);
}
if (e.getSource().equals(canvas) && createShape == true){
s.setXLocation(e.getX());
s.setYLocation(e.getY());
createShape = false;
canvas.addShape(s);
}
Absent a complete example, it's hard to say. I'd expect your DrawingCanvas to override paintComponent() in order to render the accumulated Shape instances in shapeList. You might compare your approach to that shown in GaphPanel, cited here.
The code you provided is not complete, but anyway the problem is in your mouseClicked method, if you change your second if to something like the following for example:
if (e.getSource().equals(canvas) && createShape == true){
int x = e.getX();
int y = e.getY();
s = new Rectangle(x,y,x+50,y+50);
canvas.addShape(s);
}
then a rectangle of width & height 50 will be painted whenever you click on the canvas, depending on your x, y location (you could change the fixed width/height by using a variable based on user input). Also, I'm not sure what you're trying to do in your first if section where you're adding a MouseListener to a newly created shape that is not added to the canvas, I guess there's something else you want to do...
I had to overwrite the canvas class' paint method; call super.paint in the canvas class and repaint each shape individually
public void paint(Graphics g){
super.paint(g);
for(int i=0;i<shapeList.size();i++){
((Shape)shapeList.get(i)).paint(g);
}
}