Я создаю Uno в качестве последнего проекта на уроке CompSci.
Карты в playerHandPanel сделаны из JButtons. Кнопки должны отображаться последовательно в том порядке, в котором они были розданы, но также должны перекрываться, чтобы на панели помещалось больше начальных 7 карт.
Мой учитель предложил мне использовать JLayeredPane вместо JPanel в качестве контейнера, потому что предметы можно размещать на разных слоях.
Это не сработало, потому что я использовал BoxLayout для автоматического размещения их с помощью LINE_AXIS, и этот макет полностью игнорирует слои и размещает их так, как если бы это был обычный JPanel.
BoxLayout игнорирует слои:
Скорее, они должны перекрываться вот так:
Вот как настраивается 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, потому что думал, что смогу сгруппировать карточки по слоям определенным образом. Я не знаю, как это объяснить, но это не имеет большого значения, потому что это не сработало.
Это хорошее предложение, но у меня уже почти вся остальная часть основной игры пройдена, а на выполнение задания у меня осталась всего неделя. Многие вещи уже основаны на том, что они являются JButton и используют ImageIcons. Думаю, я смогу попробовать и посмотреть, как далеко я смогу продвинуться.
Ознакомьтесь с Макетом перекрытия. Думаю, вы также можете использовать FlowLayout с отрицательным горизонтальным зазором. Вам также потребуется переопределить isOptimizedDrawingEnabled(), чтобы вернуть false на панель.
Проблема (для HovercraftOfEels): веб-редактор, который я использую, когда я не посещаю класс CompSci, не может импортировать классы из других источников (например, JavaFX). Я также не могу загрузить другую IDE для использования на школьном ноутбуке. Я могу использовать только классы, импортированные из Java или Javax (и, возможно, некоторые другие, я не уверен).
@Crashtestdummy ни одно из моих предложений не предполагало использование внешних источников.
@HovercraftFullOfEels Rectangle2D импортирован из «javafx.geometry»?
Нет. Он импортирован из java.awt.geom.Rectangle2D.
Я никогда не использовал эти объекты Shape. Я также не понимаю, как мне следует создавать карты Rectangle2D вместо JButtons, когда единственный способ их создания — это Rectangle2D.add(), который является абстрактным. Я думаю, это означает, что я не могу назначить имя переменной каждому отдельному прямоугольнику.
Извините, я не понимаю, что вы имеете в виду под вышесказанным. Как показывает API, Rectangle2D имеет конкретные реализации. Например, Rectangle2D myRect = new Rectangle2D.Double(0, 0, 100, 60);
@camickr извини, я просто пропустил твой комментарий, это, наверное, самый простой способ




Одним из возможных решений является создание собственного менеджера компоновки. Например, если вы хотите расположить группу JLabels, представляющих игральные карты Uno, так, чтобы они перекрывали друг друга, причем последний добавленный элемент располагался над предыдущими картами и немного справа, вы могли бы создать макет, который выглядит примерно так:
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 «рукой».
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-порядка. Затем вызываются повторная проверка и перерисовка, чтобы сообщить менеджерам компоновки о необходимости компоновки компонентов и приказать компоненту перерисоваться, удалив грязные пиксели.
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();
}
}
}
}
В нем находится колода карт Уно и стопка сброса. При нажатии на колоду карта удаляется, добавляется и отображается в стопке сброса. При нажатии на стопку сброса текущая отображаемая карта выбрасывается на стеклянную панель, и пользователь может ее перетащить мышью. При выпуске все слушатели уведомляются об освобожденной карте (значке изображения), чтобы родительский компонент мог поместить карту на игровую панель.
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 на панель колоды, чтобы его можно было уведомить, если карта была перетянута и отпущена. В прослушивателе карта помещается на игровую панель.
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 меняется при добавлении карт в руку.
Я бы посоветовал вам не использовать JLayeredPane, JButtons или любые другие более тяжелые компоненты для представления ваших карточек. Вместо этого я бы сделал их облегченными объектами Shape, которые наследуются от одного из интерфейсов Shape с использованием классов, таких как Rectangle2D, и которые отображают BufferedImage, по сути превращая их в спрайты. С помощью Shape вы можете определить, был ли щелкнут спрайт, вы можете установить z-порядок по порядку, в котором они отрисовываются. Вы также можете анимировать движение, инициируемое щелчком и перетаскиванием.