首页 > 解决方案 > 如何创建一个可拖动的 JLabel 捕捉到面板上的某些边界

问题描述

我正在使用 Java 开发一个维恩图 GUI 应用程序,它需要我创建可以拖放到圆形维恩图部分中的元素 (JLabels)。目前我有两个重叠的(定制的)圆形 J 面板。

出现了两个问题:

1)我如何创建可以在屏幕上移动并最终捕捉到标签的某些有界点并适合边界的 J 标签。

2) 如何在 J 面板本身上创建一个布局,只允许文本元素适合特定位置而不重叠。

这是我的 GUI 到目前为止的样子。我已将其标记为向您展示界面的一般概念以及我希望该功能如何工作

标签: javaswingdraggablejlabelvenn-diagram

解决方案


给你……大约 463 行代码让你开始……

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

为什么这么多行?

主要是因为你说了你已经实现但没有提供任何代码。所以我不得不从头开始设计一切。从自定义布局管理器到自定义形状的面板和边框,再到实际的VennToolVennDrawPanelVennSetVennLabel表示。

那么它包括什么?

这是一个类似于类图的东西,这是迄今为止我为满足您的要求所做的最大努力:

类图

框表示类,指针表示继承,嵌套框表示嵌套类。黑色文本代表类名,青色文本代表注释/详细信息。

代码从看似不相关的类开始,但它们的作用类似于在 VennTool 中使用的框架。

首先,MouseEventFlag枚举只是将MouseEvents 转换为枚举常量,以便测试每个鼠标事件按下哪个按钮/键。

然后有一些静态方法可以做一般的事情,比如将EnumSets 打包成long蒙版,将颜色转换为透明,获取和操作Component大小等......

然后是自定义LayoutManager类,名为ManualLayout. 顾名思义,您必须将 a 的Components 和 aContainer放在ManualLayout您希望它们与(例如)setBoundssetLocation方法一起出现的位置。然后布局计算每个的首选大小,ComponentContainer返回最小和首选布局大小。该layoutContainer方法只是设置包含Component的 s 的大小并且不移动它们的位置。ManualLayout考虑到每个的所有大小(Component即当前大小、首选大小、最小和最大大小)。这种布局的实例用于在绘图面板中布置Components(将在本文中讨论)。

然后RectangularPanel是一个自定义形状的类,JPanel它支持任何java.awt.geom.RectangularShape形状来塑造面板本身并对其进行绘制。这是组成维恩集的组件的超类(例如维恩集本身和添加到绘图面板的标签)。

然后开始VennTool上课。此类实现Runnable运行模拟。它包含代表维恩组件/元素的所有类以及绘图面板。客户端类只应该看到VennTool类(它提供了创建维恩集和标签的方法)而没有其他(除非它们是它的子类)。处理和修改维恩集由用户决定。当然,您可以随时修改类以返回模拟中已经存在的所有维恩组件,例如以后需要将它们保存为项目,但此处未实现此类功能。只需添加一些 getter 和文件 I/O 即可自己执行此操作。

然后,在VennToolnow里面,就是VennDrawPanel我上面所说的绘图面板,它负责整个模拟的布局。在这里,我们添加了维恩集、标签和用户控件(例如让用户定义其操作的按钮)。VennDrawPanelhas a其中JLayeredPane使用 a ManualLayout,为每个组件实现不同的层和位置。特别是,第一层由按钮组成。该层的顶部是维恩集,最后是维恩标签。作为未来的想法,您可以实现一个新层,拖动的组件将在其上继续拖动(例如,您可能希望用户当前拖动的维恩集位于高于所有其他层的层)。

然后,有一个自定义Border类,命名为(你猜对了)VennBorder。包含它VennTool只是为了显示它的范围并允许它与每个 Venn 组件交互。它纯粹受javax.swing.border.LineBorder类的启发和扩展。

然后,有VennControl代表每个维恩分量(即VennSets 和VennLabels)的类。它负责使用鼠标手势(例如拖动 Venn 集),用于标签的碰撞检测和解析,还负责将集附加/分离到标签和标签到集。

customVennBorderVennControlclasses 由 type 的形状定义java.awt.geom.RectangularShape。这允许每个子类可以是例如Ellipse2D形状(如VennSet圆形的 s 的情况)或 a Rectangle2D(如VennLabels 的情况)。它还定义了要支持的弧宽和高度,RoundRectangle2D但这只是沿着道路出现,以支持两个Ellipse2D面板Rectangle2D/边框。

最后,还有代表维恩集的VennSetVennLabel类,以及相应地添加到维恩集的标签。

附加/分离模式:

启用后,每次鼠标释放都会将您持有的标签与所有重叠集(或您持有的所有重叠标签)附加在一起。它也不会让标签/集与其附加的集/标签不重叠。禁用此模式以便VennControl在鼠标释放时自由移动任何分离它。

碰撞检测模式:

顾名思义,启用后,标签​​将相互碰撞而不重叠。禁用时,它们不会。


推荐阅读