Могу ли я использовать BoxLayout в JLayeredPane для индивидуального размещения элементов на каждом слое?

Я создаю Uno в качестве последнего проекта на уроке CompSci.
Карты в playerHandPanel сделаны из JButtons. Кнопки должны отображаться последовательно в том порядке, в котором они были розданы, но также должны перекрываться, чтобы на панели помещалось больше начальных 7 карт.

Мой учитель предложил мне использовать JLayeredPane вместо JPanel в качестве контейнера, потому что предметы можно размещать на разных слоях.
Это не сработало, потому что я использовал BoxLayout для автоматического размещения их с помощью LINE_AXIS, и этот макет полностью игнорирует слои и размещает их так, как если бы это был обычный JPanel.

BoxLayout игнорирует слои:
Могу ли я использовать BoxLayout в JLayeredPane для индивидуального размещения элементов на каждом слое?

Скорее, они должны перекрываться вот так:
Могу ли я использовать BoxLayout в JLayeredPane для индивидуального размещения элементов на каждом слое?

Вот как настраивается playerHandPanel:

    public void setupImgs()
    {
        //gets the dealt hand and sets up the buttons/imgs
        for (int i = 0; i < deck.getPlayerHand().size(); i++)
        {
            setupHand(new JButton(deck.getHandImgs().get(i)), i);
        }
    }
    public void setupHand(JButton img, int index)
    {
        //TODO allow more than 7 cards in hand
        //TODO jlayeredpane to allow overlap by using layers
        //boxlayout puts them in sequential but ignores layers >:(

        playerHandPanel = new JLayeredPane();
        playerHandPanel.setBounds(250, 350, 1400, 300);
        playerHandPanel.setBackground(Color.GRAY);
        playerHandPanel.setLayout(new BoxLayout(playerHandPanel, BoxLayout.LINE_AXIS));
        //using the 

        if (index == 0)
        {
            //playerHand1
            ph1 = new JButton("", img.getIcon());
            ph1.setSize(150, 240);
            ph1.setLocation(0, 0);
            ph1.setMargin(new Insets(1,1,1,1));
            ph1.addActionListener(this);
            ph1.addMouseListener(this);//moves up when card focused
            playerHandPanel.add(ph1, JLayeredPane.DEFAULT_LAYER);
            //placed on bottom-most layer
        }

        if (index == 1)
        {
            ph2 = new JButton("", img.getIcon());
            ph2.setSize(150, 240);
            ph2.setLocation(20, 0);
            ph2.setMargin(new Insets(1,1,1,1));
            ph2.addActionListener(this);
            ph2.addMouseListener(this);
            playerHandPanel.add(ph2, JLayeredPane.PALETTE_LAYER);
            //placed on second lowest layer

        }
        
        if (index == 2)
        {
            ph3 = new JButton("", img.getIcon());
            ph3.setSize(150, 240);
            ph3.setLocation(50, 0);
            ph3.setMargin(new Insets(1,1,1,1));
            ph3.addActionListener(this);
            ph3.addMouseListener(this);
            playerHandPanel.add(ph3, JLayeredPane.MODAL_LAYER);
            //placed on third lowest layer
        }
        //continued to 7...
        window.add(playerHandPanel);
        playerHandPanel.setVisible(true);
    }

Я попробовал использовать один или два других макета, но так и не смог понять, как они работают. В конце концов я просто вернулся в BoxLayout. Я попробовал GroupLayout, потому что думал, что смогу сгруппировать карточки по слоям определенным образом. Я не знаю, как это объяснить, но это не имеет большого значения, потому что это не сработало.
Могу ли я использовать BoxLayout в JLayeredPane для индивидуального размещения элементов на каждом слое?

Я бы посоветовал вам не использовать JLayeredPane, JButtons или любые другие более тяжелые компоненты для представления ваших карточек. Вместо этого я бы сделал их облегченными объектами Shape, которые наследуются от одного из интерфейсов Shape с использованием классов, таких как Rectangle2D, и которые отображают BufferedImage, по сути превращая их в спрайты. С помощью Shape вы можете определить, был ли щелкнут спрайт, вы можете установить z-порядок по порядку, в котором они отрисовываются. Вы также можете анимировать движение, инициируемое щелчком и перетаскиванием.

Hovercraft Full Of Eels 10.05.2024 17:55

Это хорошее предложение, но у меня уже почти вся остальная часть основной игры пройдена, а на выполнение задания у меня осталась всего неделя. Многие вещи уже основаны на том, что они являются JButton и используют ImageIcons. Думаю, я смогу попробовать и посмотреть, как далеко я смогу продвинуться.

Crashtestdummy 10.05.2024 18:19

Ознакомьтесь с Макетом перекрытия. Думаю, вы также можете использовать FlowLayout с отрицательным горизонтальным зазором. Вам также потребуется переопределить isOptimizedDrawingEnabled(), чтобы вернуть false на панель.

camickr 10.05.2024 18:58

Проблема (для HovercraftOfEels): веб-редактор, который я использую, когда я не посещаю класс CompSci, не может импортировать классы из других источников (например, JavaFX). Я также не могу загрузить другую IDE для использования на школьном ноутбуке. Я могу использовать только классы, импортированные из Java или Javax (и, возможно, некоторые другие, я не уверен).

Crashtestdummy 10.05.2024 19:04

@Crashtestdummy ни одно из моих предложений не предполагало использование внешних источников.

Hovercraft Full Of Eels 10.05.2024 19:06

@HovercraftFullOfEels Rectangle2D импортирован из «javafx.geometry»?

Crashtestdummy 10.05.2024 19:10

Нет. Он импортирован из java.awt.geom.Rectangle2D.

Hovercraft Full Of Eels 10.05.2024 19:11

Я никогда не использовал эти объекты Shape. Я также не понимаю, как мне следует создавать карты Rectangle2D вместо JButtons, когда единственный способ их создания — это Rectangle2D.add(), который является абстрактным. Я думаю, это означает, что я не могу назначить имя переменной каждому отдельному прямоугольнику.

Crashtestdummy 10.05.2024 19:49

Извините, я не понимаю, что вы имеете в виду под вышесказанным. Как показывает API, Rectangle2D имеет конкретные реализации. Например, Rectangle2D myRect = new Rectangle2D.Double(0, 0, 100, 60);

Hovercraft Full Of Eels 10.05.2024 20:19

@camickr извини, я просто пропустил твой комментарий, это, наверное, самый простой способ

Crashtestdummy 13.05.2024 16:54
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
10
69
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Одним из возможных решений является создание собственного менеджера компоновки. Например, если вы хотите расположить группу JLabels, представляющих игральные карты Uno, так, чтобы они перекрывали друг друга, причем последний добавленный элемент располагался над предыдущими картами и немного справа, вы могли бы создать макет, который выглядит примерно так:

UnoCardHandLayout.java

import java.awt.*;

// layout manager that lays out Uno cards in a hand
// the cards are JLabels that are added to a JPanel
// and are placed one over the other, but leaving a GAP between them
public class UnoCardHandLayout implements LayoutManager {
    private static final int DEFAULT_GAP = 60;
    private int gap; // gap between cards

    public UnoCardHandLayout() {
        this(DEFAULT_GAP);
    }

    public UnoCardHandLayout(int gap) {
        this.gap = gap;
    }

    @Override
    public void addLayoutComponent(String name, Component comp) {
    }

    @Override
    public void removeLayoutComponent(Component comp) {
    }

    @Override
    public Dimension preferredLayoutSize(Container parent) {
        int width = 0;
        int height = 0;
        for (Component comp : parent.getComponents()) {
            Dimension pref = comp.getPreferredSize();
            width = Math.max(width, pref.width);
            height = Math.max(height, pref.height);
        }
        Insets insets = parent.getInsets();
        width += insets.left + insets.right + gap;
        height += insets.top + insets.bottom + gap;
        return new Dimension(width, height);
    }

    @Override
    public Dimension minimumLayoutSize(Container parent) {
        return preferredLayoutSize(parent);
    }

    @Override
    public void layoutContainer(Container parent) {
        Insets insets = parent.getInsets();
        int x = insets.left;
        int y = insets.top;
        for (int i = parent.getComponentCount() - 1; i >= 0; i--) {
            Component comp = parent.getComponent(i);
            Dimension pref = comp.getPreferredSize();
            comp.setBounds(x, y, pref.width, pref.height);
            x += gap;
        }

    }
}

Этот менеджер компоновки добавляет компоненты, начиная с последнего компонента в массиве компонентов (первого добавленного) и добавляя зазор на расстояние gap между следующим вышележащим компонентом. Это можно использовать с GamePanel JPanel, который держит карту Uno «рукой».

GamePanel.java

import java.awt.Color;
import java.awt.Dimension;
import java.util.ArrayList;
import java.util.List;

import javax.swing.*;
import javax.swing.border.*;

public class GamePanel extends JPanel {
    private List<JLabel> cardLabels = new ArrayList<>();
    private static final int MIN_PREF_WIDTH = 1200;
    private static final int MIN_PREF_HEIGHT = 300;
    private static final Color BKG_COLOR = new Color(0, 80, 0); // game table dark green
    private static final int CARD_GAP = 60;

    public GamePanel() {
        setLayout(new UnoCardHandLayout(CARD_GAP)); // !! important
        setBackground(BKG_COLOR);
        Border border = BorderFactory.createTitledBorder(BorderFactory.createLineBorder(Color.WHITE), "Game Panel",
                TitledBorder.DEFAULT_JUSTIFICATION, TitledBorder.DEFAULT_POSITION, null, Color.WHITE);
        setBorder(border);
    }

    @Override
    public Dimension getPreferredSize() {
        // return a size that is at least the minimum preferred size.
        Dimension superSize = super.getPreferredSize();
        int width = Math.max(superSize.width, MIN_PREF_WIDTH);
        int height = Math.max(superSize.height, MIN_PREF_HEIGHT);
        return new Dimension(width, height);
    }

    public void addUnoCard(JLabel cardLabel) {
        cardLabels.add(cardLabel);
        add(cardLabel, 0); // add to the top of the z-order
        revalidate();
        repaint();
    }
}

Когда добавляется карта Uno, путем вызова addUnoCard(...) карта добавляется в 0-ю позицию компонента, в начало z-порядка. Затем вызываются повторная проверка и перерисовка, чтобы сообщить менеджерам компоновки о необходимости компоновки компонентов и приказать компоненту перерисоваться, удалив грязные пиксели.

UnoDeckPanel.java

import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Point;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.swing.*;

public class UnoDeckPanel extends JPanel {
    public static final String CARD_DROP = "card drop";
    private static final int GAP = 10;
    private Icon emptyIcon;
    private List<Icon> unoDeckIcons;
    private List<Icon> cardPileIcons = new ArrayList<>();
    private JLabel deckLabel = null;
    private JLabel cardPileLabel = null;

    public UnoDeckPanel(Icon backIcon, Icon emptyIcon, List<Icon> icons) {
        this.unoDeckIcons = new ArrayList<>(icons);
        this.emptyIcon = emptyIcon;
        Collections.shuffle(unoDeckIcons);
        JPanel innerPanel = new JPanel();
        innerPanel.setLayout(new GridLayout(1, 0, GAP, GAP));
        innerPanel.setBorder(BorderFactory.createEmptyBorder(GAP, 20 * GAP, GAP, 20 * GAP));
        deckLabel = new JLabel(backIcon);
        innerPanel.add(deckLabel);
        cardPileLabel = new JLabel(emptyIcon);
        innerPanel.add(cardPileLabel);

        setLayout(new GridBagLayout());
        add(innerPanel);

        deckLabel.addMouseListener(new UnoDeckListener());
        CardPileListener cardPileListener = new CardPileListener();
        cardPileLabel.addMouseListener(cardPileListener);
        cardPileLabel.addMouseMotionListener(cardPileListener);
    }

    private class UnoDeckListener extends MouseAdapter {
        @Override
        public void mousePressed(MouseEvent e) {
            if (unoDeckIcons.isEmpty()) {
                JOptionPane.showMessageDialog(UnoDeckPanel.this, "Deck is empty");
                return;
            }
            Icon icon = unoDeckIcons.remove(0);
            cardPileLabel.setIcon(icon);
            cardPileIcons.add(icon);

            if (unoDeckIcons.isEmpty()) {
                deckLabel.setIcon(emptyIcon);
            }
        }
    }

    private class CardPileListener extends MouseAdapter {
        private Icon poppedIcon = null;
        JLabel poppedLabel = null;

        public void mousePressed(MouseEvent e) {
            if (cardPileLabel.getIcon() != emptyIcon) {
                // first get the icon from the cardPileLabel, the top "card"
                poppedIcon = cardPileIcons.remove(cardPileIcons.size() - 1);
                // set the new top card from the icon underneath the one removed
                cardPileLabel
                        .setIcon(cardPileIcons.isEmpty() ? emptyIcon : cardPileIcons.get(cardPileIcons.size() - 1));

                // get the glass pane and add the popped label
                JComponent glassPane = (JComponent) UnoDeckPanel.this.getRootPane().getGlassPane();
                glassPane.setVisible(true);
                glassPane.setLayout(null); // null so we can drag the label
                poppedLabel = new JLabel(poppedIcon);
                glassPane.add(poppedLabel);
                poppedLabel.setSize(poppedLabel.getPreferredSize());

                // set the location of the popped label, centered on the mouse position
                Point currentPoint = e.getLocationOnScreen();
                Point glassPaneLocation = glassPane.getLocationOnScreen();
                int halfWidth = poppedLabel.getWidth() / 2;
                int halfHeight = poppedLabel.getHeight() / 2;
                Point p = new Point(currentPoint.x - glassPaneLocation.x - halfWidth,
                        currentPoint.y - glassPaneLocation.y - halfHeight);

                poppedLabel.setLocation(p);
                glassPane.revalidate();
                glassPane.repaint();
            }
        }

        public void mouseReleased(MouseEvent e) {
            if (poppedIcon != null) {
                // get the glass pane and remove the popped label
                JComponent glassPane = (JComponent) UnoDeckPanel.this.getRootPane().getGlassPane();
                glassPane.removeAll();
                glassPane.setVisible(false);

                // notify anyone listening to the card drop property that a card has been
                // dropped
                // and pass in the icon that was dropped
                UnoDeckPanel.this.firePropertyChange(CARD_DROP, null, poppedIcon);

                // reset things
                poppedIcon = null;
                poppedLabel = null;
            }
        }

        public void mouseDragged(MouseEvent e) {
            if (poppedIcon != null) {
                JComponent glassPane = (JComponent) UnoDeckPanel.this.getRootPane().getGlassPane();
                Point currentPoint = e.getLocationOnScreen();
                Point glassPaneLocation = glassPane.getLocationOnScreen();
                int halfWidth = poppedLabel.getWidth() / 2;
                int halfHeight = poppedLabel.getHeight() / 2;
                Point p = new Point(currentPoint.x - glassPaneLocation.x - halfWidth,
                        currentPoint.y - glassPaneLocation.y - halfHeight);
                poppedLabel.setLocation(p);
                glassPane.revalidate();
                glassPane.repaint();
            }
        }
    }

}

В нем находится колода карт Уно и стопка сброса. При нажатии на колоду карта удаляется, добавляется и отображается в стопке сброса. При нажатии на стопку сброса текущая отображаемая карта выбрасывается на стеклянную панель, и пользователь может ее перетащить мышью. При выпуске все слушатели уведомляются об освобожденной карте (значке изображения), чтобы родительский компонент мог поместить карту на игровую панель.

UnoMainGui.java

import java.awt.BorderLayout;
import java.awt.MouseInfo;
import java.awt.Point;
import java.beans.*;
import java.util.List;
import javax.swing.*;

// main JPanel that holds both the deck panel and the game panel
public class UnoMainGui extends JPanel {
    private UnoDeckPanel deckPanel;
    private GamePanel gamePanel = new GamePanel();

    public UnoMainGui(List<Icon> icons) {
        deckPanel = new UnoDeckPanel(UnoMain.UNO_BACK_ICON, UnoMain.UNO_EMPTY_ICON, icons);

        setLayout(new BorderLayout(2, 2));
        add(deckPanel, BorderLayout.PAGE_START);
        add(gamePanel, BorderLayout.CENTER);

        // if we drop a card from the deck panel, add it to the game panel
        // this does not check for invalid drops (e.g., off of the game panel)
        deckPanel.addPropertyChangeListener(UnoDeckPanel.CARD_DROP, new DeckPanelListener());
    }

    // user has dropped a card dragged off the deck
    private class DeckPanelListener implements PropertyChangeListener {
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            Icon icon = (Icon) evt.getNewValue();
            if (icon == null) {
                return;

            }
            JLabel cardLabel = new JLabel(icon);
            cardLabel.setSize(cardLabel.getPreferredSize());

            // get the location of the mouse relative to the game panel's coordinate system
            Point mousePointOnScreen = MouseInfo.getPointerInfo().getLocation();
            Point gamePanelLocationOnScreen = gamePanel.getLocationOnScreen();
            int halfWidth = cardLabel.getWidth() / 2;
            int halfHeight = cardLabel.getHeight() / 2;
            Point p = new Point(mousePointOnScreen.x - gamePanelLocationOnScreen.x - halfWidth,
                    mousePointOnScreen.y - gamePanelLocationOnScreen.y - halfHeight);
            cardLabel.setLocation(p);

            gamePanel.addUnoCard(cardLabel);
        }
    }
}

Это позволит настроить и запустить графический интерфейс. Он добавляет прослушиватель PropertyChangeListener на панель колоды, чтобы его можно было уведомить, если карта была перетянута и отпущена. В прослушивателе карта помещается на игровую панель.

Окончательно, UnoMain.java

import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import javax.imageio.ImageIO;
import javax.swing.*;

public class UnoMain {
    public static final String UNO_CARD_SHEET = "https://upload.wikimedia.org/wikipedia/commons/"
            + "thumb/9/95/UNO_cards_deck.svg/2389px-UNO_cards_deck.svg.png";
    public static final int COLUMNS = 14;
    public static final int ROWS = 8;
    public static Icon UNO_BACK_ICON = null;
    public static Icon UNO_EMPTY_ICON = null;

    public static void main(String[] args) {
        // get a Uno card sprite sheet from a public source
        BufferedImage unoImage = null;
        try {
            URL unoUrl = new URL(UNO_CARD_SHEET);
            unoImage = ImageIO.read(unoUrl);
        } catch (IOException e) {
            e.printStackTrace();
            System.exit(1);
        }

        // and then subdivide the sprite sheet into individual icons
        double width = unoImage.getWidth() / (double) COLUMNS;
        double height = unoImage.getHeight() / (double) ROWS;

        Icon[][] icons = new Icon[ROWS][COLUMNS];

        for (int row = 0; row < ROWS; row++) {
            for (int col = 0; col < COLUMNS; col++) {
                BufferedImage img = unoImage.getSubimage((int) (col * width), (int) (row * height), (int) width,
                        (int) height);
                Icon icon = new ImageIcon(img);
                icons[row][col] = icon;
            }
        }

        // put some of them into an ArrayList
        List<Icon> unoDeckIconList = new ArrayList<>();
        for (int row = 0; row < ROWS / 2; row++) {
            for (int col = 0; col < COLUMNS - 1; col++) {
                unoDeckIconList.add(icons[row][col]);
            }
        }

        // back of a UNO card
        UNO_BACK_ICON = icons[0][COLUMNS - 1];

        // empty card
        BufferedImage emptyImage = new BufferedImage((int) width, (int) height, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = emptyImage.createGraphics();
        g2d.setColor(Color.BLACK);
        g2d.fillRect(0, 0, (int) width, (int) height);
        g2d.dispose();
        UNO_EMPTY_ICON = new ImageIcon(emptyImage);

        // create the Swing GUI on the event thread
        SwingUtilities.invokeLater(() -> {
            UnoMainGui mainGui = new UnoMainGui(unoDeckIconList);
            JFrame frame = new JFrame("UNO Card Sheet");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.add(new JScrollPane(mainGui));
            frame.pack();
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
        });

    }
}
Ответ принят как подходящий

Это то, что мне было нужно, спасибо @camickr за комментарий. Это очень просто и, как я уже сказал, у меня не так много времени, чтобы пробовать что-то совершенно новое.
«Оцените макет перекрытия. Думаю, вы также можете использовать FlowLayout с отрицательным горизонтальным зазором. Вам также нужно будет переопределить метод isOptimizedDrawingEnabled(), чтобы он возвращал false на панели». -@camickr

Я также добавлю правило, согласно которому Hgap меняется при добавлении карт в руку.

Другие вопросы по теме