so I am working on a Solitaire project and am currently severely stuck on the GUI part. I am trying to do drag and drop using Rob Camick's OverlapLayout (source code here) but it doesn't work out how I want it to.
I can pick up the card and move it just fine, but when I release it, it doesn't drop down where I want it to and the card disappears. How to fix this?
Attached is a simplified version of my code. When that code is run, as seen in the attached image, the card is dragged from one stack to another, but then it automatically moves to the bottom of the screen instead of on top of the card it was dropped on.
This is the class with the problem (I believe that it has to do with the way I created the mainStacks using the for loop, because when I was dragging, the drag layer went behind some of the stacks themselves):
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.border.*;
public class Test extends JPanel implements MouseListener, MouseMotionListener {
//instance variables
private static final int CARD_WIDTH = 73;
private static final int CARD_HEIGHT = (int)(CARD_WIDTH*1.49137931+.5);
private static final int MAIN_STACKS_X_DIFF = 35;
private static final int MAIN_STACKS_Y_DIFF = 30;
private final Dimension cardSize = new Dimension(CARD_WIDTH, CARD_HEIGHT);
private JPanel mainStacks, mainWrapper;
private Component card;
private int xAdj, yAdj;
private Deck deckOfCards;
//methods
public Test() {
JFrame gameScreen = new JFrame();
gameScreen.setTitle("Solitaire");
gameScreen.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
gameScreen.add(this);
deckOfCards = new Deck();
deckOfCards.shuffleDeck();
mainStacks = makeMainStacks();
mainWrapper = new JPanel();
mainWrapper.setBackground(new Color(0,153,0));
mainWrapper.add(mainStacks);
gameScreen.setLayout(new BorderLayout());
gameScreen.add(mainWrapper, BorderLayout.CENTER);
setLayout(null);
gameScreen.setSize(800,600);
gameScreen.setResizable(false);
gameScreen.getContentPane().setBackground(new Color(0,153,0));
gameScreen.setVisible( true );
}
private JPanel makeMainStacks() {
mainStacks = new JPanel(new GridLayout(0,7,MAIN_STACKS_X_DIFF, MAIN_STACKS_Y_DIFF));
for (int stack = 0; stack < 7; stack++) {
JPanel mainStack = new JPanel( new OverlapLayout( new Point(0, 22)));
mainStack.setBackground( new Color(0,153,0));
mainStack.setBounds(10,0,CARD_WIDTH, CARD_HEIGHT);
mainStack.setBorder( new EmptyBorder(10, 0, CARD_WIDTH, CARD_HEIGHT));
mainStack.setPreferredSize(new Dimension(CARD_WIDTH, CARD_HEIGHT + 250));
mainStacks.add(mainStack);
for (int cardInStack = 0; cardInStack <= stack; cardInStack++) {
Card placementCard = deckOfCards.drawCard(); //draws card
placementCard.setIsFaceUp(false);
//checking if the card is the last card in the stack
if (stack == cardInStack) {
System.out.println(stack + cardInStack);
placementCard.setIsFaceUp(true); //card is facing up
//sizes the image and sets it to placementCard
ImageIcon placementCardFace = new ImageIcon(placementCard.getImagePath());
Image placementCardScaled = placementCardFace.getImage().getScaledInstance(CARD_WIDTH, CARD_HEIGHT, Image.SCALE_SMOOTH);
JLabel cardLabel = new JLabel(new ImageIcon(placementCardScaled));
mainStack.add(cardLabel);
} //end of if statement
//if statement to check whether the card should be revealed or not
if (!placementCard.getIsFaceUp()) { //hide cardFront
//sizes the image and sets it to placementCard
ImageIcon placementCardBack = new ImageIcon(Card.getBackImagePath());
Image placementCardScaled = placementCardBack.getImage().getScaledInstance(CARD_WIDTH, CARD_HEIGHT, Image.SCALE_SMOOTH);
JLabel cardLabel = new JLabel(new ImageIcon(placementCardScaled));
cardLabel.setPreferredSize(cardSize);
mainStack.add(cardLabel);
} //end of if statement
}
mainStack.addMouseListener(this);
mainStack.addMouseMotionListener(this);
}
JPanel mainStacksWrapper = new JPanel();
mainStacksWrapper.add(mainStacks);
mainStacksWrapper.setBackground(new Color(0,153,0));
mainStacks.setBackground(new Color(0,153,0));
return mainStacksWrapper;
}
@Override
public void mousePressed(MouseEvent e) {
try {
JPanel stack = (JPanel)e.getComponent();
card = stack.getComponent(0);
if (card instanceof JPanel){}
Point stackPos = card.getParent().getLocation();
xAdj = stackPos.x-e.getX();
yAdj = stackPos.y-e.getY();
card.setLocation(e.getX() + xAdj + 35, e.getY() + yAdj);
System.out.println(xAdj + yAdj);
JLayeredPane lp = getRootPane().getLayeredPane();
lp.add(card,JLayeredPane.DRAG_LAYER);
setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
} catch (ArrayIndexOutOfBoundsException err) {
System.err.println("No card in stack. Cannot execute move.");
}
}
@Override
public void mouseDragged(MouseEvent e) {
if (card==null){}
int x = e.getX() + xAdj + 35;
int y = e.getY() + yAdj;
card.setLocation(x,y);
}
@Override
public void mouseReleased(MouseEvent e) {
setCursor(null);
if (card == null) return;
// Make sure the card is no longer painted on the layered pane
card.setVisible(false);
// check to see if card was dragged to a stack
JPanel stackPanel = null;
Point cardLocation = card.getLocation();
cardLocation.y -= mainStacks.getParent().getLocation().y;
for (int i = 0; i < mainStacks.getComponentCount(); i++)
{
JPanel panel = (JPanel)mainStacks.getComponent(i);
if (panel.getBounds().contains(cardLocation))
{
stackPanel = panel;
break;
}
}
if (stackPanel == null)
mainStacks.add( card );
else
stackPanel.add( card );
card.setVisible(true);
}
@Override
public void mouseClicked(MouseEvent e) {}
@Override
public void mouseMoved(MouseEvent e) {}
@Override
public void mouseEntered(MouseEvent e) {}
@Override
public void mouseExited(MouseEvent e) {}
public static void main(String[] args) {
new Test();
}
}
These next few classes are just additional classes to help the code compile:
import java.util.ArrayList;
import java.util.Collections;
public class Deck {
//instance variables
private final ArrayList<Card> cards;
private int drawCardIndex;
//methods
/**
* This constructor for the Deck class creates all 52 cards and adds them to cards List.
*/
public Deck() {
cards = new ArrayList<>(); //creating new deck
for (Suit suit: Suit.values()) { //loops through suits
for (Rank rank: Rank.values()) { //loops through ranks
cards.add(new Card(suit, rank)); //adds a unique Card to cards
}
}
drawCardIndex = 0; //initializes index
} //end of constructor
/**
* This method returns the size of the deck.
* @return int - size of deck
*/
public int getSize() {
return cards.size();
} //end of getSize method
/**
* This method returns a drawn card in the deck of cards.
* @return Card - a new drawn card in the deck of cards.
*/
public Card drawCard() {
if (drawCardIndex<cards.size()) { //checks if all of the cards have been drawn
Card card = cards.get(drawCardIndex); //draws a card
drawCardIndex++; //increments
return card; //return statement
}
return null; //returns null if all of the cards have already been drawn
} //end of drawCard method
/**
* This method shuffles the deck of cards.
*/
public void shuffleDeck() {
Collections.shuffle(cards); //mixes ArrayList
resetDrawCardIndex(); //resets the drawCardIndex variable
} //end of shuffleDeck method
/**
* This method resets the drawCardIndex.
*/
public void resetDrawCardIndex() {
drawCardIndex = 0;
} //end of resetDrawCardIndex method
/**
* This method removes a card from the deck.
* @param card the card to be removed from the deck
*/
public void removeCardFromDeck(Card card) {
//checks if the stack is empty
cards.remove(card); //removes the card from the deck
} //end of removeCardFromStack method
/**
* This method checks if all of the cards have been drawn.
* @return Boolean true or false based on if all the cards have been drawn or not.
*/
public boolean isEmpty() {
return cards.size() <= drawCardIndex;
} //end of isEmpty method
} //end of Deck class
public class Card {
//instance variables
private final Suit SUIT;
private final Rank RANK;
private boolean isFaceUp;
//methods
/**
* This is the constructor method for the Card class.
* @param suit - the suit to be assigned to the card.
* @param rank - the rank to be assigned to the card.
*/
public Card(Suit suit, Rank rank) {
this.SUIT = suit;
this.RANK = rank;
this.isFaceUp = false;
} //end of constructor
/**
* This method gives the user the suit of the Card.
* @return Suit - the suit of the Card.
*/
public Suit getSuit() {
return this.SUIT;
} //end of getSuit method
/**
* This method gives the user the rank of the Card.
* @return Rank - the rank of the Card.
*/
public Rank getRank() {
return this.RANK;
} //end of getRank method
/**
* This method gives whether the Card is faced up or not.
* @return Boolean - true or false based on if the Card is facing up.
*/
public boolean getIsFaceUp() {
return this.isFaceUp;
} //end of getIsFaceUp method
/**
* This method sets the isFaceUp variable to true if the card is facing up in the game.
*/
public void setIsFaceUp(boolean b) {
this.isFaceUp = b;
} //end of setIsFaceUp method
/**
* This method determines if the Card is from a red suit.
* @return Boolean - true or false if card is red
*/
public boolean isRed() {
return this.SUIT == Suit.HEARTS || this.SUIT == Suit.DIAMONDS;
} //end of isRed method
/**
* This method determines if the Card is from a black suit.
* @return a Boolean true or false if card is black
*/
public boolean isBlack() {
return this.SUIT == Suit.CLUBS || this.SUIT == Suit.SPADES;
} //end of isBlack method
/**
* This method determines if the card can be stacked on the card behind it.
* @return a Boolean true or false if it can be stacked
*/
public boolean stackable(Card card) {
return this.isRed() != card.isRed();
} //end of stackable method
/**
* This method gives the name of each card.
* @return a string that has two letters- the first letter of the rank and the first letter of the suit.
*/
public String getName() {
String number = ""; //variable declaration
switch(RANK) { //switch to assign rank name
case ACE -> number = "A"; //case
case TWO -> number = "2"; //case
case THREE -> number = "3"; //case
case FOUR -> number = "4"; //case
case FIVE -> number = "5"; //case
case SIX -> number = "6"; //case
case SEVEN -> number = "7"; //case
case EIGHT -> number = "8"; //case
case NINE -> number = "9"; //case
case TEN -> number = "10"; //case
case JACK -> number = "J"; //case
case QUEEN -> number = "Q"; //case
case KING -> number = "K"; //case
} //end of switch
String suitName = ""; //variable declaration
switch(SUIT) { //switch to assign suit name
case HEARTS -> suitName = "H"; //case
case DIAMONDS -> suitName = "D"; //case
case SPADES -> suitName = "S"; //case
case CLUBS -> suitName = "C"; //case
} //end of switch
return number + suitName; //return statement
} //end of getName method
/**
* This method gives the path to a corresponding image.
* @return String - returns the path of the image corresponding with the card.
*/
public String getImagePath() {
return "C:\\ICS3U_SUMMATIVE_SOLITAIRE\\src\\cards\\" + getName().substring(1,getName().length()) + "\\" + getName() + ".jpg"; //return statement
} //end of getImage method
/**
* This method gives the file path to the back of the card image.
* @return String the file path to the card back image
*/
public static String getBackImagePath() {
return "C:\\ICS3U_SUMMATIVE_SOLITAIRE\\src\\CardBack.jpg";
} //end of getImageBackPath method
@Override
/**
* This method returns the description about the instance variables.
* @return String - rank and suit of card along with image path.
*/
public String toString() {
return this.RANK + " of " + this.SUIT + " ---- " + getImagePath();
}
} //end of Card class
public enum Suit {
//enum fields
/**
* Hearts, Diamonds,
*/
HEARTS,DIAMONDS,
/**
* Spades, Clubs
*/
SPADES,CLUBS;
}
public enum Rank {
//enum fields
/**
* 2,3,4,5,6,7,8,9,10
*/
TWO,THREE,FOUR,FIVE,SIX,SEVEN,EIGHT,NINE,TEN,
/**
* A,J,Q,K
*/
ACE,JACK,QUEEN,KING;
}
I have reviewed past suggestions from SO, but those do not seem to help my problem. Also have tried altering the y values to see if that helps, but it didn't.
A mouseEvent
reports it's location relative to the component which generated the event, that is, the top/left position (0x0) is relative to the component.
Since you are moving components between coordinate contexts, you need to convert the event point accordingly, luckily, there is a simple way to do this - SwingUtilities.convertMouseEvent(Component, Point, Component)
@Override
public void mousePressed(MouseEvent e) {
try {
JPanel stack = (JPanel) e.getComponent();
card = stack.getComponent(0);
if (card instanceof JPanel) {
}
Point cardInWorld = SwingUtilities.convertPoint(card.getParent(), card.getLocation(), getRootPane().getLayeredPane());
Point pointInWorld = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), getRootPane().getLayeredPane());
Point stackPos = card.getParent().getLocation();
xAdj = pointInWorld.x - cardInWorld.getLocation().x;
yAdj = pointInWorld.y - cardInWorld.getLocation().y;
JLayeredPane lp = getRootPane().getLayeredPane();
lp.add(card, JLayeredPane.DRAG_LAYER);
card.setLocation(cardInWorld);
setCursor(Cursor.getPredefinedCursor(Cursor.MOVE_CURSOR));
} catch (ArrayIndexOutOfBoundsException err) {
System.err.println("No card in stack. Cannot execute move.");
}
}
@Override
public void mouseDragged(MouseEvent e) {
if (card == null) {
}
Point pointInWorld = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), getRootPane().getLayeredPane());
int x = pointInWorld.x - xAdj;
int y = pointInWorld.y - yAdj;
card.setLocation(x, y);
}
This will cause the card to maintained relative to the mouse cursor - so it's position (relative to the mouse cursor) won't change. This makes it so that the mouse cursor's point is the source of the drop, not the card's position.
Walking a container's contents like this...
@Override
public void mouseReleased(MouseEvent e) {
setCursor(null);
if (card == null) return;
// Make sure the card is no longer painted on the layered pane
card.setVisible(false);
// check to see if card was dragged to a stack
JPanel stackPanel = null;
Point cardLocation = card.getLocation();
cardLocation.y -= mainStacks.getParent().getLocation().y;
for (int i = 0; i < mainStacks.getComponentCount(); i++)
{
JPanel panel = (JPanel)mainStacks.getComponent(i);
if (panel.getBounds().contains(cardLocation))
{
stackPanel = panel;
break;
}
}
if (stackPanel == null)
mainStacks.add( card );
else
stackPanel.add( card );
card.setVisible(true);
}
is always a bad idea and took me some significant time to suss out.
The source of the problem starts right here...
private JPanel makeMainStacks() throws IOException {
mainStacks = new JPanel(new GridLayout(0, 7, MAIN_STACKS_X_DIFF, MAIN_STACKS_Y_DIFF));
//...
JPanel mainStacksWrapper = new JPanel();
mainStacksWrapper.add(mainStacks);
mainStacksWrapper.setBackground(new Color(0, 153, 0));
mainStacks.setBackground(new Color(0, 153, 0));
return mainStacksWrapper;
}
You add mainStacks
into a "wrapper" container, and then assign it to mainStacks
(I'm so confused).
The problem with this is, when you walk the container's contents, it only contains a single child. The original mainStacks
, which actually contains all the child stacks you actually want.
A better solution would be to add each stack component to a List
and simply iterate over it instead, that way, you're not going to get caught out when the UI structure changes.
Start by creating a new instance field...
private List<JPanel> stacks = new ArrayList<>(7);
Then in your makeMainStacks
add each stack
to it...
private JPanel makeMainStacks() throws IOException {
JPanel mainStacks = new JPanel(new GridLayout(0, 7, MAIN_STACKS_X_DIFF, MAIN_STACKS_Y_DIFF));
mainStacks.setBackground(Color.MAGENTA);
for (int stack = 0; stack < 7; stack++) {
JPanel mainStack = new JPanel(new OverlapLayout(new Point(0, 22)));
mainStack.setBounds(10, 0, CARD_WIDTH, CARD_HEIGHT);
mainStack.setBorder(
new CompoundBorder(new LineBorder(Color.RED), new EmptyBorder(10, 0, CARD_WIDTH, CARD_HEIGHT))
);
mainStack.setPreferredSize(new Dimension(CARD_WIDTH, CARD_HEIGHT + 250));
mainStacks.add(mainStack);
stacks.add(mainStack);
for (int cardInStack = 0; cardInStack <= stack; cardInStack++) {
Card placementCard = deckOfCards.drawCard(); //draws card
placementCard.setIsFaceUp(false);
//checking if the card is the last card in the stack
if (stack == cardInStack) {
System.out.println(stack + cardInStack);
placementCard.setIsFaceUp(true); //card is facing up
//sizes the image and sets it to placementCard
ImageIcon placementCardFace = new ImageIcon(placementCard.getImagePath());
Image placementCardScaled = placementCardFace.getImage().getScaledInstance(CARD_WIDTH, CARD_HEIGHT, Image.SCALE_SMOOTH);
JLabel cardLabel = new JLabel(new ImageIcon(placementCardScaled));
mainStack.add(cardLabel);
} //end of if statement
//if statement to check whether the card should be revealed or not
if (!placementCard.getIsFaceUp()) { //hide cardFront
//sizes the image and sets it to placementCard
ImageIcon placementCardBack = new ImageIcon(Card.getBackImagePath());
Image placementCardScaled = placementCardBack.getImage().getScaledInstance(CARD_WIDTH, CARD_HEIGHT, Image.SCALE_SMOOTH);
JLabel cardLabel = new JLabel(new ImageIcon(placementCardScaled));
cardLabel.setPreferredSize(cardSize);
mainStack.add(cardLabel);
} //end of if statement
}
mainStack.addMouseListener(this);
mainStack.addMouseMotionListener(this);
}
JPanel mainStacksWrapper = new JPanel();
mainStacksWrapper.add(mainStacks);
mainStacksWrapper.setBackground(new Color(0, 153, 0));
mainStacks.setBackground(new Color(0, 153, 0));
return mainStacksWrapper;
}
And then finally, in mouseReleased
, convert the MouseEvent
's point to each stack's parents coordinate context and see if you have a hit or not ...
@Override
public void mouseReleased(MouseEvent e) {
setCursor(null);
if (card == null) {
return;
}
// Make sure the card is no longer painted on the layered pane
card.setVisible(false);
JPanel stackPanel = null;
for (JPanel stack : stacks) {
// Check the bounds of each stack against it's parents
// coordinates context
Point localPoint = SwingUtilities.convertPoint(e.getComponent(), e.getPoint(), stack.getParent());
if (stack.getBounds().contains(localPoint)) {
System.out.println("-->");
stackPanel = stack;
break;
}
}
if (stackPanel == null) {
mainStacks.add(card);
} else {
stackPanel.add(card);
stackPanel.revalidate();
stackPanel.repaint();
}
card.setVisible(true);
}
For what it's worth, I wouldn't have done it this way, for all the reasons you're discovering. I would have followed a custom painting route, but I doubt you have time for that now.