0

I want to output a row of cards at the bottom of the JPanel dynamically, meaning that if I add a card the row will adjust and center out automatically. In addition, when the user hovers over the card, I need the card to be popped up a little. I used pure Graphics objects to do this, however, it is very, very inefficient and lags a lot. Is it more efficient to use overlapping JPanels, although I don't know how to use them?

Here is my code now:

import javax.imageio.ImageIO;
import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Stack;

public class Launcher extends JFrame implements MouseMotionListener {
    private static final int MAX_CARD_SPAN = 900;
    private static final int CARD_WIDTH = 104;
    private static final int CARD_HEIGHT = 146;
    private static final int drawCardY = 500;
    private static final int drawSelectedCardY = 450;
    private static Deck deck;
    private static Stack<Card> userDeck;
    private static int currentX;
    private static int initialX;
    private static int offset;
    private static int overlapIndex;

    public static void main(String[] args) throws IOException {
        deck = new Deck();
        userDeck = new Stack<>();
        currentX = 80;
        initialX = 0;
        overlapIndex = -2;
        new Launcher();
    }

    public Launcher() throws IOException {
        this.setTitle("UNO");
        this.setSize(1000, 650);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setResizable(false);
        this.setVisible(true);

        addMouseMotionListener(this);

        for (int i = 0; i < 10; i++)
            userDeck.add(deck.getDeck().pop());

        repaint();
    }

    public static int getValue(String name) {
        return -1;
    }

    public void paint(Graphics g) {
        if (overlapIndex != -1)
            try {
                setBackground(Color.WHITE);
                offset = 80;

                if (userDeck.size() > 10)
                    offset = MAX_CARD_SPAN / userDeck.size();

                initialX = (MAX_CARD_SPAN - offset * userDeck.size()) / 2 + offset / 2;
                currentX = initialX;

                for (int i = 0; i < userDeck.size(); i++) {
                    BufferedImage bufferedImage = ImageIO.read(getClass().getResourceAsStream("\\images\\" + userDeck.get(i).getName() + ".jpg"));
                    g.drawImage(bufferedImage, currentX, drawCardY, null);
                    currentX += offset;
                }

                if (overlapIndex > -1)
                    g.drawImage(ImageIO.read(getClass().getResourceAsStream("\\images\\" + userDeck.get(overlapIndex - 1).getName() + ".jpg")), offset * (overlapIndex - 1) + initialX, drawSelectedCardY, null);

                overlapIndex = -1;
            } catch (Exception e) {
            }
    }

    public void paintCardOverlap(Graphics g) throws
    }

    public void cardOverlap(int x, int y) {
        overlapIndex = x / offset;
        repaint();
    }

    public void mouseMoved(MouseEvent e) {
        if (inRange(e.getX(), e.getY()))
            cardOverlap(e.getX(), e.getY());
        else
            overlapIndex = -1;
    }

    public static boolean inRange(int x, int y) {
        int sideSpace = (MAX_CARD_SPAN - offset * userDeck.size()) / 2 + offset / 2;

        return ((x >= sideSpace) && (x <= MAX_CARD_SPAN - sideSpace + CARD_WIDTH) && (y >= 600 - CARD_HEIGHT));
    }

    public void mouseDragged(MouseEvent e) {
    }
}
Andrew Thompson
  • 168,117
  • 40
  • 217
  • 433
XHyper
  • 3
  • 2
  • One mistake is that you're using a `Stack`. The Javadoc recommends that you use an `ArrayDeque` instead. This won't fix your lag, but it's a common issue. – Jacob G. May 06 '17 at 01:42

1 Answers1

3

It lags a lot because you're reading in images from within a painting method, something that you never want to do. This is wasteful because it means that you re-read the images each time painting is called, and it also slows the perceived responsiveness of your GUI since the painting is always done slowly. Don't do this. Read the images into an ArrayList or other field, and use painting for painting only.

Other unrelated problems

  • Gross over-use of static modifiers -- something that can increase the coupling of your code and thus your bugs
  • You're painting in the wrong painting method. You should not draw in paint but rather in the paintComponent method of a JPanel
  • You are not calling the super's painting method within your own override.
  • You're ignoring exceptions with that empty catch block, the coding equivalent of driving your car with your eyes closed. At least print the exception's stacktrace so you know why one has been thrown
  • You're starting a Swing GUI off of the Swing event thread, something that can lead to hard to debug intermittent Swing threading exceptions.
  • ...

Playing with some old code of mine that no longer works because the link to the card images is dead. This one uses a new link and works, but kinks need to be worked out of it. The example below does not draw directly within a component's painting method, but rather creates images and then ImageIcons, and places them within draggable JLabels.

It displays as:

Card Game Example

import java.awt.Color;
import java.awt.Component;
import java.awt.Dialog.ModalityType;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;

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

@SuppressWarnings("serial")
public class CardFun extends JLayeredPane {
    private static final int PREF_W = 1200;
    private static final int PREF_H = 900;
    private static final Color BASE_COLOR = new Color(0, 80, 0);
    private static final int CARD_COUNT = 20;
    private static final int WIDTH_SHOWING = 20;
    private static final String PATH = "https://raw.githubusercontent.com/hayeah/playing-cards-assets/master/png/";
    private static final String BACK = "back.png";
    private Map<Card, Icon> cardIconMap = new HashMap<>();
    private Icon backIcon;
    private Deck deck = new Deck();
    private JPanel basePane = new JPanel(null);
    private JDialog optionDlg;

    public CardFun() throws IOException {
        LoadImageWorker worker = new LoadImageWorker();
        worker.addPropertyChangeListener(new LoadImageWorkerListener());
        worker.execute();
        basePane.setSize(getPreferredSize());
        basePane.setBackground(BASE_COLOR);
        add(basePane, JLayeredPane.DEFAULT_LAYER);

        final CardMouseAdapter mouseAdapter = new CardMouseAdapter(this, basePane);
        addMouseListener(mouseAdapter);
        addMouseMotionListener(mouseAdapter);

        JProgressBar progBar = new JProgressBar();
        progBar.setIndeterminate(true);
        JOptionPane optionPane = new JOptionPane(progBar, JOptionPane.PLAIN_MESSAGE);
        optionDlg = optionPane.createDialog(null, "Waiting for Images to Load");
        optionDlg.setModalityType(ModalityType.APPLICATION_MODAL);

        optionDlg.setVisible(true);
    }

    @Override
    public Dimension getPreferredSize() {
        if (isPreferredSizeSet()) {
            return super.getPreferredSize();
        }
        return new Dimension(PREF_W, PREF_H);
    }

    public Icon getBackIcon() {
        return backIcon;
    }

    public void setBackIcon(Icon backIcon) {
        this.backIcon = backIcon;
    }

    class LoadImageWorkerListener implements PropertyChangeListener {
        @Override
        public void propertyChange(PropertyChangeEvent evt) {
            if (evt.getNewValue() == SwingWorker.StateValue.DONE) {
                @SuppressWarnings("rawtypes")
                SwingWorker worker = (SwingWorker) evt.getSource();
                try {
                    if (optionDlg != null) {
                        optionDlg.setVisible(false);
                    }
                    worker.get();
                    for (int i = 0; i < CARD_COUNT; i++) {
                        Card card = deck.deal();
                        Icon cardIcon = cardIconMap.get(card);
                        JLabel cardLbl = new JLabel(cardIcon);
                        cardLbl.setSize(cardLbl.getPreferredSize());
                        int x = (PREF_W / 2) + WIDTH_SHOWING * (CARD_COUNT - 2 * i) / 2 - 
                              cardLbl.getPreferredSize().width / 2;
                        int y = PREF_H - cardLbl.getPreferredSize().height - WIDTH_SHOWING * 2;
                        cardLbl.setLocation(x, y);
                        basePane.add(cardLbl);
                     }
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                    System.exit(-1);
                };
            }
        }
    }

    class LoadImageWorker extends SwingWorker<Void, Void> {
        @Override
        protected Void doInBackground() throws Exception {
            for (Card card : deck) {
                String imgPath = String.format("%s%s_of_%s.png", PATH, 
                        card.getRank().getText().toLowerCase(), 
                        card.getSuit().toString().toLowerCase());
                URL imgUrl = new URL(imgPath);
                // System.out.println(imgPath);
                BufferedImage img = ImageIO.read(imgUrl);
                int w = img.getWidth();
                int h = img.getHeight();
                BufferedImage img2 = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
                Graphics2D g2 = img2.createGraphics();
                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
                g2.setColor(Color.WHITE);
                g2.fillRoundRect(1, 1, w - 2, h - 2, 10, 10);
                g2.setColor(Color.BLACK);
                g2.drawRoundRect(1, 1, w - 2, h - 2, 10, 10);
                g2.drawImage(img, 0, 0, null);
                g2.dispose();
                Icon icon = new ImageIcon(img2);
                cardIconMap.put(card, icon);
            }
            String backPath = PATH + BACK;
            URL imgUrl = new URL(backPath);
            // System.out.println(imgPath);
            BufferedImage img = ImageIO.read(imgUrl);
            setBackIcon(new ImageIcon(img));
            return null;
        }
    }

    private static void createAndShowGui() {
        CardFun mainPanel = null;
        try {
            mainPanel = new CardFun();
        } catch (IOException e) {
            e.printStackTrace();
            System.exit( -1);
        }

        JFrame frame = new JFrame("CardFun");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(mainPanel);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> createAndShowGui());
    }
}

class CardMouseAdapter extends MouseAdapter {
    private JLabel selectedCard = null;
    private JLayeredPane cardGameTable = null;
    private JPanel basePane = null;
    private int deltaX = 0;
    private int deltaY = 0;

    public CardMouseAdapter(JLayeredPane gameTable, JPanel basePane) {
       this.cardGameTable = gameTable;
       this.basePane = basePane;
    }

    @Override
    public void mousePressed(MouseEvent mEvt) {
       Component comp = basePane.getComponentAt(mEvt.getPoint());
       if (comp != null && comp instanceof JLabel) {
          selectedCard = (JLabel) comp;
          basePane.remove(selectedCard);
          basePane.revalidate();
          basePane.repaint();

          cardGameTable.add(selectedCard, JLayeredPane.DRAG_LAYER);
          cardGameTable.revalidate();
          cardGameTable.repaint();
          deltaX = mEvt.getX() - selectedCard.getX();
          deltaY = mEvt.getY() - selectedCard.getY();
       }
    }

    @Override
    public void mouseReleased(MouseEvent mEvt) {
       if (selectedCard != null) {
          cardGameTable.remove(selectedCard);
          cardGameTable.revalidate();
          cardGameTable.repaint();

          basePane.add(selectedCard, 0);
          basePane.revalidate();
          basePane.repaint();
          selectedCard = null;
       }
    }

    @Override
    public void mouseDragged(MouseEvent mEvt) {
       if (selectedCard != null) {
          int x = mEvt.getX() - deltaX;
          int y = mEvt.getY() - deltaY;
          selectedCard.setLocation(x, y);
          cardGameTable.revalidate();
          cardGameTable.repaint();
       }
    }
 }

enum Suit {
    CLUBS, DIAMONDS, HEARTS, SPADES;
}

enum Rank {
    ACE("Ace"), TWO("2"), THREE("3"), FOUR("4"), FIVE("5"), SIX("6"), SEVEN("7"), EIGHT("8"), NINE(
            "9"), TEN("10"), JACK("Jack"), QUEEN("Queen"), KING("King");

    private String text;

    private Rank(String text) {
        this.text = text;
    }

    public String getText() {
        return text;
    }

}

class Card {
    private Suit suit;
    private Rank rank;

    public Card(Suit suit, Rank rank) {
        this.suit = suit;
        this.rank = rank;
    }

    public Suit getSuit() {
        return suit;
    }

    public Rank getRank() {
        return rank;
    }

    @Override
    public String toString() {
        return "Card [suit=" + suit + ", rank=" + rank + "]";
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        result = prime * result + ((rank == null) ? 0 : rank.hashCode());
        result = prime * result + ((suit == null) ? 0 : suit.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        Card other = (Card) obj;
        if (rank != other.rank)
            return false;
        if (suit != other.suit)
            return false;
        return true;
    }
}

class Deck implements Iterable<Card> {
    private List<Card> cards;

    public Deck() {
        reset();
    }

    public final void reset() {
        cards = new ArrayList<>();
        for (Rank rank : Rank.values()) {
            for (Suit suit : Suit.values()) {
                cards.add(new Card(suit, rank));
            }
        }
        Collections.shuffle(cards);
    }

    public Card deal() {
        return cards.remove(0);
    }

    @Override
    public Iterator<Card> iterator() {
        return cards.iterator();
    }
}
Community
  • 1
  • 1
Hovercraft Full Of Eels
  • 283,665
  • 25
  • 256
  • 373
  • I see. Should I not use images and switch to real GUI? – XHyper May 06 '17 at 02:31
  • Forgot to mention, thank you so much for your reply! – XHyper May 06 '17 at 02:32
  • @XHyper: please translate or re-phrase your statement: `"Should I not use images and switch to real GUI?"`, because I'm having a hard time understanding this. I thought that you were already trying to create a "real" GUI. – Hovercraft Full Of Eels May 06 '17 at 02:33
  • This is still lagging. I fixed the BufferedImage part so it is loading it from the Card Object only once. – XHyper May 06 '17 at 12:37
  • 1
    @XHyper: if you need more precise help, then consider changing your question so that it is a valid [mcve] -- a small new program that we can compile and run, that uses images from public internet URL's not from your disk. – Hovercraft Full Of Eels May 06 '17 at 12:40
  • 1
    One way to get image(s) for an example is to hot link to images seen in [this Q&A](http://stackoverflow.com/q/19209650/418556). – Andrew Thompson May 06 '17 at 12:51
  • Okay, I will do that now. Also, would be better to used LayeredPane rather than Graphics? Much less calculation involved. – XHyper May 06 '17 at 13:24
  • @XHyper: please run my code above to see an example of card game graphics that are not too sluggish. – Hovercraft Full Of Eels May 06 '17 at 13:37
  • @HovercraftFullOfEels It runs really smooth! Thanks for your answer, I think I will use the approach of draggable cards now. – XHyper May 06 '17 at 14:03