javaswingconcurrencytimerevent-dispatching

Updating swing components correctly?


I'm new to swing, any help appreciated.

In this piece of code I'm turning a card over face up, if it turns out that they don't match I want them to turn back face down again.

At the moment what is happening: 1. when clicked the first card turns over 2. when a second card is clicked either of two things happen (a) if they are the same they both stay up which is what I want (b) if they are not the same I never see the 2nd card at all as it immediately re-displays the back of the card (and the back of the previous card also as defined in my method).

I thought putting in the sleep timer might keep the 2nd card displayed for a period of time before turning back over but it does not.

I attempted to use contentPane.revalidate(); & contentPane.repaint(); but it doesn't change anything.

I have put in some console outputs:

Console output:
Card: 0 set
Card: 6 set
Sleeping now
Card: 6 unset
Card: 0 unset

Above is the resulting console output when clicking two card which do not match

@Override
public void actionPerformed(ActionEvent e) 
{
    String buttonPressed = e.getActionCommand();
    int pos = Integer.valueOf(buttonPressed);
    action = Control.model.ReceiveCardsTurned(pos);

    keypadArray[pos].setIcon(myIcons[pos]);     
    System.out.println("Card: "+pos+" set");
    currentTime.setText("" + Control.model.time);
    currentScore.setText("" + Control.model.score);

    //contentPane.revalidate();
    //contentPane.repaint();        

    if(Control.model.twoCardsTurned == false)
    {
        if (action == "unturn") 
        {
            System.out.println("Sleeping now");

            try 
            {
                Thread.sleep(1000);
            }

            catch (InterruptedException e1) 
            {
                e1.printStackTrace();
            }

            keypadArray[pos].setIcon(back);
            keypadArray[Control.model.lastCard].setIcon(back);
            System.out.println("Card: "+pos+" unset");
            System.out.println("Card: "+Control.model.lastCard+" unset");
        }
    }
}

Solution

  • There are a number of important concepts you seem to be missing.

    1. Swing is an event driven environment. That means that there is no means (or at least only a very few) that you can "wait" for user input, typically, you just need to react to their interaction.
    2. Swing is driven by a single thread, known as the Event Dispatching Thread (AKA EDT). It is the responsibility of this thread to dispatch/process events coming into the application to the appropriate parts of the application so that they can take action.
    3. The repaint manager posts its update requests onto the EDT.

    ANY action you take that stops the EDT from carrying out this work will make your application look like it's hung.

    You must NEVER carry out any time consuming operations (such as I/O, loops or Thread#sleep for example) on the EDT, doing so will make your application "pause", which is never pretty.

    Have a read through Concurrency in Swing for more information.

    Now, you have a number of choices. You could use a Thread to "wait" in the background and turn the cards back or you could use a SwingWorker or a javax.swing.Timer.

    The other problem you have, is that you should NEVER update any UI components from any Thread other than the EDT. This means if you were to use a Thread, you would become responsible for re-syncing that thread with the EDT. While not difficult, it just becomes messy.

    SwingWorker and javax.swing.Timer have functionality that make this much easier.

    Threads and SwingWorker are great for performing background processing and would simply be overkill for this problem. Instead, a javax.swing.Timer would fit perfectly here.

    if (!Control.model.twoCardsTurned)
        {
            if ("unturn".equals(action)) 
            {
                new Timer(1000, new ActionListener() {
                    public void actionPerformed(ActionEvent evt) {
                        keypadArray[pos].setIcon(back);
                        keypadArray[Control.model.lastCard].setIcon(back);
                        System.out.println("Card: "+pos+" unset");
                        System.out.println("Card: "+Control.model.lastCard+" unset");
                    }
                }).start();
            }
        }
    

    This is a really simple example. You might like to put in some controls that will prevent the user from clicking anything until the timer fires, for example ;)