Java swing 如何分层自定义绘画

Java swing how to layer custom paintings

背景

我目前正在做一个项目,我希望能够在 JPanel 上绘制三角形、圆形和矩形并四处移动它们。

移动图形时,它应该在“顶部”结束,以便它覆盖处于重叠位置的其他图形,移动图形时,选择“顶部”;如果有多个图形覆盖了鼠标指针的位置,则选择最上面的一个。

我的问题

我不知道如何解决将形状拖动到相同位置时它们不会彼此重叠的问题。它们只是停留在同一层,如果我将它们全部堆叠在一起,我仍然会先拿起矩形。

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

面板上的组件是根据组件ZOrder绘制的。 ZOrder 最低的组件最后绘制。

因此,在 MouseListenermousePressed 方法中,当您 select 拖动一个组件时,您可以更改其 ZOrder:

Component child = e.getComponent();
child.getParent().setComponentZOrder(child, 0);

编辑:

drawRec.paintComponent(g);
drawCirc.paintComponent(g);
drawTri.paintComponent(g);

我之前没有注意到那个代码。

切勿直接调用 paintComponent()。

Swing 具有 parent/child 关系。您只需将 3 个形状面板中的每一个添加到父面板。然后父面板将根据我上面描述的 ZOrder 绘制子组件。摆脱那 3 行代码。

They just stay in the same layers...

那是因为您绘制它们的顺序:

drawRec.paintComponent(g);
drawCirc.paintComponent(g);
drawTri.paintComponent(g);

这将导致 drawTri 总是被绘制在其他的之上(因为你总是最后绘制它)。以类似的方式,drawRec 将被绘制在其他人的底部(因为您总是先绘制它)。然后drawCirc会画在中间。这里没有绘画的动态顺序(让我说吧)。不管你拖不拖,它们总是会按顺序绘制。

一种解决方案可能是将它们放在某种列表或数组中,当您拖动一个形状时,将其放在列表的最后,然后将其他形状移到它之前。如果您将其与从列表中按顺序绘制所有形状相结合,那么您将获得所需的结果。


...if i stack them all on top of each other I still pick up the rectangle first.

这是因为 ClickListener class 中的 mousePressed 的工作原理:它首先检查矩形是否被单击,然后检查其他矩形。这意味着如果矩形与另一个形状重叠,则矩形总是优先。

一个解决方案可能再次将所有形状放入一个数据结构中,您可以在其中修改它们的 selection 顺序。例如一个列表或数组,假设越靠近顶部的形状越晚出现在列表中。然后,当用户单击某处时,您将检查从列表中的最后一个到第一个的形状。如果你找到了什么,你会立即打破循环并 select 你找到的东西。如果您没有找到任何东西,那么用户在当前没有形状的地方点击了面板。


我几乎可以肯定,对于这个问题,肯定有比列表或数组更有效的数据结构(因为你必须在线性时间内迭代所有形状才能找到被点击的形状),但我不是碰撞方面的专家检测,所以,为了简单起见,我将坚持使用它。但是还有一个我们想要做的其他操作:改变点击形状的绘画和 selection 顺序。长话短说,我们可以使用 LinkedHashSet 来完成这项工作,因为:

  1. 它保持形状的顺序。
  2. 我们可以快速有效地更改顺序,方法是首先从当前位置(恒定时间)移除点击的形状,然后将其添加回 LinkedHashSet(也是恒定时间),本质上将其放在最后插入顺序。所以这自动意味着我们必须使用集合中的最后一个元素作为最上面的一个。这很好,因为在绘制时我们可以按照它们在其中找到的顺序遍历集合中的所有形状,因此最后一个形状将被最后绘制(这意味着在所有其他形状之上)。这同样代表单击时形状的 selection:我们遍历所有元素,select 我们发现包含用户点击点的 last 元素.如果 LinkedHashSet 有一个降序迭代器(按插入顺序),那么我们也可以在给定的点击点优化搜索形状,但它没有(至少对于 Java 8 其中以下 demonstration/example 代码适用),因此我将坚持从一开始就进行迭代,每次检查所有形状并保留找到的最后一个包含点击点的形状。

最后,我建议您对形状使用 API class java.awt.Shape,因为这使您能够创建任意形状并使用 contains 方法来检查点是否位于它们内部、drawing/filling 能力、边界、路径迭代器等等...

总结以上所有内容,并附上示例代码:

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);
    }
}

以上所有内容都适用于您希望使用单个自定义组件绘制所有形状的情况。相反,如果您愿意,您可以为每个形状创建一个自定义组件,并使用 JLayeredPane 将它们放置在彼此之上。但是你可能也需要一个自定义 LayoutManager(为了处理每个组件的位置)。

我创建了以下 GUI。

我创建了一个Shapeclass。我用 java.awt.Point 来保持中心点,用 java.awt.Polygon 来保持实际形状。我能够使用 Polygon contains 方法来查看我是否在多边形内部单击鼠标。

我创建了一个 ShapeModel class 来保存 java.util.ListShape 个实例。创建 Swing GUI 时,创建正确的应用程序模型非常重要。

我创建了一个 JFrame 和一个绘图 JPanel。绘图 JPanel 绘制了 ListShape 个实例。时期。 MouseAdapter 将负责重新计算多边形并重新绘制 JPanel

技巧就在MouseAdapter mousePressed 方法中。我删除了选定的 Shape 实例并将选定的 Shape 实例添加回 List。这会将所选形状移动到 Z 顺序的顶部。

这是完整的可运行代码。我将所有 classes 都放在 classes 中,这样我就可以 post 将这段代码作为一个块。

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
    }

}