4

I am putting together a slideshow program that will measure a user's time spent on each slide. The slideshow goes through several different magic tricks. Each trick is shown twice. Interim images are shown between the repetition. Transition images are shown between each trick.

On the first repetition of a trick the JPanel color flashes on the screen after a click before the next image is shown. This doesn't happen during the second repetition of the same trick. It's possible that the image is taking too long to load.

Is there an easy way to pre-load the images so that there isn't a delay the first time through?

NOTE: Original code deleted.

EDIT 1/10/2013: This code now works on slower machines. trashgod's second addendum helped the most. The mouseClick control structure periodically asks SwingWorker classes to load 40 images or less of the current trick while also setting the used images to null. I have simplified my code down for this to just two Image[]s and added a main method so it stands alone. Images are still required to run though. This is now pretty bare bones code, and if you're trying to make a slideshow with a lot of images I think it would be a good place to start.

NOTE: I think I figured out how to properly implement SwingWorker while still using multiple Image[]s. trashgod and kleopatra is this implementation in-line with what you were suggesting? I didn't end up using publish and process since I couldn't figure out how to get that to work appropriately with an indexed array, but because the StringWorker doesn't load all images in the array (only 40), and the code calls StringWorker every 20 images, there should be a pretty good buffer.

EDIT 1/10/2013 Changed out MouseListener by instead extending MouseAdapter on my Mouse class. Also fixed my paintComponent method to include a call to super.paintComponent(g). Added publish/process methods to my SwingWorker class ImageWorker. Added a wrapper class, ArrayWrapper to allow passing imageArray[i] and its corresponding index int i with publish to process.

package slideshow3;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseAdapter;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import java.util.List;

public class SlideShow3 extends JFrame
{
    //screenImage will be replaced with each new slide
    private Image screenImage;
    private int width;
    private int height;

    //Create panel for displaying images using paintComponent()
    private SlideShow3.PaintPanel mainImagePanel;

    //Used for keybinding
    private Action escapeAction;

    //Image array variables for each trick
    private Image[] handCuffs; //h
    private Image[] cups; //c

    //Used to step through the trick arrays one image at a time
    private int h = 0;
    private int c = 0;

    //Used by timeStamp() for documenting time per slide
    private long time0 = 0;
    private long time1;

    public SlideShow3()
    {
        super();

        //Create instance of each Image array
        handCuffs = new Image[50];
        cups = new Image[176];

        //start(handCuffsString);
        start("handCuffs");

        try
        {
            screenImage = ImageIO.read(new File("images/begin1.jpg"));
        }
        catch (IOException nm) 
        {
            System.out.println("begin");
            System.out.println(nm.getMessage());
            System.exit(0);
        }

        /****************************************** 
         * Removes window framing. The next line sets fullscreen mode.
         * Once fullscreen is set width and height are determined for the window
         ******************************************/

        this.setUndecorated(true);
        GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().setFullScreenWindow(this);
        width = this.getWidth();
        height = this.getHeight();

        //Mouse click binding to slide advance control structure
        addMouseListener(new Mouse());

        //Create panel so that I can use key binding which requires JComponent
        mainImagePanel = new PaintPanel();      
        add(mainImagePanel);

        /****************************************** 
         * Key Binding
         * ESC will exit the slideshow
         ******************************************/

        // Key bound AbstractAction items 
        escapeAction = new EscapeAction();

        // Gets the mainImagePanel InputMap and pairs the key to the action
        mainImagePanel.getInputMap().put(KeyStroke.getKeyStroke("ESCAPE"), "doEscapeAction");

        // This line pairs the AbstractAction enterAction to the action "doEnterAction"
        mainImagePanel.getActionMap().put("doEscapeAction", escapeAction);

        /******************************************
         * End Key Binding
         ******************************************/
    }

    public static void main(String[] args) 
    {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() 
            {
                SlideShow3 show = new SlideShow3();
                show.setVisible(true);
            }
        });
    }

    //This method executes a specific SwingWorker class to preload images
    public void start(String e) 
    {
        if(e.equals("handCuffs"))
        {
            new ImageWorker(handCuffs.length, h, e).execute();
        }
        else if(e.equals("cups"))
        {
            new ImageWorker(cups.length, c, e).execute();
        }
    }

    //Stretches and displays images in fullscreen window
    private class PaintPanel extends JPanel
    {
        @Override
        public void paintComponent(Graphics g) 
        { 
            if(screenImage != null)
            {
                super.paintComponent(g);
                g.drawImage(screenImage, 0, 0, width, height, this);
            }  
        }
    }

    /******************************************
     * The following SwingWorker class Pre-loads all necessary images.
     ******************************************/

    private class ArrayWrapper
    {
        private int i;
        private Image image;

        public ArrayWrapper(Image image, int i)
        {
            this.i = i;
            this.image = image;
        }

        public int getIndex()
        {
            return i;
        }

        public Image getImage()
        {
            return image;
        }
    }

    private class ImageWorker extends SwingWorker<Image[], ArrayWrapper>
    {
        private int currentPosition;
        private int arraySize;
        private String trickName;
        private Image[] imageArray;

        public ImageWorker(int arraySize, int currentPosition, String trick)
        {
            super();
            this.currentPosition = currentPosition;
            this.arraySize = arraySize;
            this.trickName = trick;
        }

        @Override
        public Image[] doInBackground()
        {
            imageArray = new Image[arraySize];
            for(int i = currentPosition; i < currentPosition+40 && i < arraySize; i++)
            {
                try 
                {
                    imageArray[i] = ImageIO.read(new File("images/" + trickName + (i+1) + ".jpg"));
                    ArrayWrapper wrapArray = new ArrayWrapper(imageArray[i], i);
                    publish(wrapArray);
                } 
                catch (IOException e) 
                {
                    System.out.println(trickName);
                    System.out.println(e.getMessage());
                    System.exit(0);
                }
            }
            return imageArray;
        }

        @Override
        public void process(List<ArrayWrapper> chunks)
        {
            for(ArrayWrapper element: chunks)
            {
                if(trickName.equals("handCuffs"))
                {
                    handCuffs[element.getIndex()] = element.getImage();
                }
                else if(trickName.equals("cups"))
                {
                    cups[element.getIndex()] = element.getImage();
                }
            }
        }

        @Override
        public void done()
        {
            try
            {
                if(trickName.equals("handCuffs"))
                {
                    handCuffs = get();
                }
                else if(trickName.equals("cups"))
                {
                    cups = get();
                }
            }
            catch(InterruptedException ignore){}
            catch(java.util.concurrent.ExecutionException e)
            {
                String why = null;
                Throwable cause = e.getCause();
                if(cause != null)
                {
                    why = cause.getMessage();
                }
                else
                {
                    why = e.getMessage();
                }
                System.err.println("Error retrieving file: " + why);
            }
        }
    }

     /******************************************
     * End SwingWorker Pre-Loading Classes
     ******************************************/

    //Prints out time spent on each slide
    public void timeStamp()
    {
        time1 = System.currentTimeMillis();
        if(time0 != 0)
        {
            System.out.println(time1 - time0);
        }
        time0 = System.currentTimeMillis();
    } 

    /******************************************
     * User Input Classes for Key Binding Actions and Mouse Click Actions
     ******************************************/

    private class EscapeAction extends AbstractAction
    {
        @Override
        public void actionPerformed(ActionEvent e)
        {
            System.exit(0);
        }
    }

    public class Mouse extends MouseAdapter
    {
        @Override
        public void mouseClicked(MouseEvent e) 
        {
            if(!(h<handCuffs.length) && !(c<cups.length))
            {
                timeStamp();
                System.exit(0);
            }
            else if(h<handCuffs.length)
            {
                timeStamp();
                screenImage = handCuffs[h];
                repaint();
                System.out.print("handCuffs[" + (h+1) + "]\t");
                h++;
                //purge used slides and refresh slide buffer
                if(h == 20 || h == 40)
                {
                    for(int i = 0; i < h; i++)
                    {
                        handCuffs[i] = null;
                    }
                    start("handCuffs");
                }
                if(h == 45)
                {
                    start("cups");
                }
            }
            else if(c<cups.length)
            {
                timeStamp();
                screenImage = cups[c];
                repaint();
                System.out.print("cups[" + (c+1) + "]\t");
                c++;
                //purge used slides and refresh slide buffer
                if(c == 20 || c == 40 || c == 60 || c == 80 || c == 100 || c == 120 || c == 140 || c == 160)
                {
                    for(int i = 0; i < c; i++)
                    {
                        cups[i] = null;
                    }
                    start("cups");
                }
            }
        }
    }

    /******************************************
     * End User Input Classes for Key Binding Actions and Mouse Click Actions
     ******************************************/ 
}
sage88
  • 4,104
  • 4
  • 31
  • 41
  • 1
    beware: your SwingWorker implementations are _incorrect_ - doInBackground _must not_ touch fields/properties that are used in the view realm (as @trashgod already noted in one of his comments - repeating for emphasis :-), complete separation of EDT and background thread is the whole point of a SwingWorker – kleopatra Jan 08 '13 at 11:52
  • @kleopatra I am still trying to learn this concept. I'm reading through concurrency and the SwingWorker documentation. I haven't finished yet, but if there's any other material I should cover or a good tutorial I would really appreciate it. I think I understand what you're saying, but just for the sake of clarity, which parts of say HandCuffWorker are being implemented incorrectly? – sage88 Jan 10 '13 at 00:01
  • @kleopatra Hey I think I fixed the SwingWorker separation, do you mind giving feedback on the new code shown above? – sage88 Jan 10 '13 at 08:06
  • 1
    @sage88: Progress, but without `publish()` / `process()` and depending on latency, your `MouseListener` (`MouseAdapter`?) can access the image arrays before `done()`. This may be a new question; in your [sscce](http://sscce.org/), use a single image and mark it with `createGraphics()` and `drawString(String.valueOf(i), 5, 20)`. Kudos for using `Action`. – trashgod Jan 10 '13 at 09:34
  • @trashgod Thanks for referring me to MouseAdapter. The text that I've read talked about WindowListener and its associated WindowAdapter, so I had wondered if something similar might exist for MouseListener. I think with createGraphics() and drawString() you are trying to get me to notice that my paintComponent method was missing super.paintComponent(g)? I cannot find an example of code using publish/process on array/list based images. All I can find are simple examples that use publish on strings or ints. Do you know a good example of a SwingWorker using publish/process with arrays? – sage88 Jan 11 '13 at 02:53
  • @sage88: The `SwingWorker` API has a good example, and there's one [here](http://stackoverflow.com/a/4637725/230513). – trashgod Jan 11 '13 at 03:29
  • @trashgod You have been tremendously helpful! You have also dramatically increased the scope of my ignorance ;), but I feel like I learned a huge amount. I updated my code above to include publish/process methods. I had to use a wrapper class to make them work, which is what was tripping me up. – sage88 Jan 11 '13 at 06:33

1 Answers1

5

This example uses a List<ImageIcon> as a cache of images returned by getImage(). Using getResource(), the delay is imperceptible. The next and previous buttons are bound to the Space key by default.

Addendum: You can control navigation by conditioning a button's setEnabled() state using an instance of javax.swing.Timer, for example.

Addendum: Your second example waits until the mouse is clicked to begin reading an image, an indeterminate process that may return a copy immediately or may not complete until after repaint(). Instead, begin reading the images in the background using ImageIO.read(), as shown here. You can process() your List<Image> and show progress, as seen here. The SwingWorker can be launched from the initial thread, running while you subsequently build your GUI on the EDT. You can display the first image as soon as it is processed.

Community
  • 1
  • 1
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • 1
    I would suggest also looking into javax.imageio.ImageIO.read(File) as an alternative to reading images... – Sinkingpoint Jan 05 '13 at 21:05
  • @Quirliom: Good point, although resources may ease deployment; see also Q&As [here](http://stackoverflow.com/q/7045214/230513) and [here](http://stackoverflow.com/q/12041474/230513). – trashgod Jan 05 '13 at 21:16
  • So you both are pretty sure this is an image loading delay problem then? I thought so but it's good to hear it confirmed. I'll take a look and see if one of those options works well for me. – sage88 Jan 05 '13 at 21:27
  • No, your example is incomplete and can't be tested; you may be blocking the event thread elsewhere; `javax.swing.Timer` does its waiting on another thread. – trashgod Jan 05 '13 at 21:32
  • @trashgod I'm not sure I follow you. The code above does run, I'm not sure what you mean by incomplete or untestable? I mean you don't have the necessary image files, but it could be run with any image named handcuffs1.jpg through handcuffs50.jpg. The main method is in another class, but functionally all it's doing right now is: SlideShow show = new SlideShow(); show.setVisible(true); – sage88 Jan 05 '13 at 22:10
  • 2
    Please edit your question to include an [sscce](http://sscce.org/) that focuses on the problem. [`FauxImage`](http://stackoverflow.com/a/8090328/230513) may be a convenient way to isolate where any remaining latency arises. – trashgod Jan 05 '13 at 22:32
  • @trashgod I added self contained code that uses some URL based images to demonstrate the problem. I found each image had to be different or the error didn't show. – sage88 Jan 05 '13 at 23:09
  • @trashgod Thanks for the help, I have marked this as the correct answer and posted my final code. I ended up using your suggestion in addendum 2. Seems to work quite well and didn't require too much tinkering once I figured it out. Just had to teach myself threading first >.<. This is my first independent program in Java, so again thanks a lot for your help! – sage88 Jan 06 '13 at 23:42
  • 1
    @sage88: It's a journey. Note that you should not update instance variables directly in the background thread; instead, `publish()` each image as it arrives, and add it to the relevant instance variable in `process()`. – trashgod Jan 07 '13 at 01:53
  • Arg I think I'm running into what you warned me about. My code runs fine on my desktop, but when I move it over a laptop it loads the first few images of each array and then the rest are just never loaded. – sage88 Jan 07 '13 at 13:00
  • That's a lot of classes to fix. It may be worth re-factoring to have just one `SwingWorker` that you can instantiate for each kind of image. – trashgod Jan 07 '13 at 14:57
  • @trashgod Hey I put together another SSCCE of the code and figured out how to re-factored all my SwingWorkers into a single class. Can you let me know if you think this is a correct implementation at this point? – sage88 Jan 10 '13 at 08:08