Swing Splash screen with progress bar
Asked Answered
D

3

7

This is my splash screen code,

public class SplashScreen extends JWindow {

private static final long serialVersionUID = 1L;
private BorderLayout borderLayout = new BorderLayout();
private JLabel imageLabel = new JLabel();
private JProgressBar progressBar = new JProgressBar(0, 100);

public SplashScreen(ImageIcon imageIcon) {
    imageLabel.setIcon(imageIcon);
    setLayout(borderLayout);
    add(imageLabel, BorderLayout.CENTER);
    add(progressBar, BorderLayout.SOUTH);
    pack();
    setLocationRelativeTo(null);
}

public void showScreen() {
    SwingUtilities.invokeLater(new Runnable() {
        public void run() {
            setVisible(true);
        }
    });
}

public void close() {
    SwingUtilities.invokeLater(new Runnable() {
        public void run() {
            setVisible(false);
            dispose();
        }
    });
}

public void setProgress(final String message, final int progress) {
    SwingUtilities.invokeLater(new Runnable() {
        public void run() {
            progressBar.setValue(progress);
            if (message == null) {
                progressBar.setStringPainted(false);
            } else {
                progressBar.setStringPainted(true);
            }
            progressBar.setString("Loading " + message + "...");
        }
    });
}
}

From the main method I am invoking like this,

public static void main(String[] args) {

    SwingUtilities.invokeLater(new Runnable() {
        public void run() {
            try {
                UIManager.setLookAndFeel(UIManager
                        .getSystemLookAndFeelClassName());

                SplashScreen splashScreen = new SplashScreen(new ImageIcon("images/splash.jpg"));
                splashScreen.showScreen();

                AppFrame frame = new AppFrame(splashScreen);

            } catch (Exception e) {
                appLogger.error(e.getMessage(), e); 
            }
        }
    });
}

In the constructor of AppFrame I call, splashScreen.setProgress(msg, val) method to update the progress bar. But the splash is not showing. It is showing only at the end when the frame is displayed only for a fraction of second even though the load takes much long time. But if I put these three lines

SplashScreen splashScreen = new SplashScreen(new ImageIcon("images/splash.jpg"));
splashScreen.showScreen();
AppFrame frame = new AppFrame(splashScreen);

outside the invokeLater() the splash screen is shown and the progress bar updates nicely. I believe GUI updates should be in invokeLater. What could be the problem?

Btw, AppFrame loads various panels of my application.

Edit: A mock of my AppFrame is shown below.

public class AppFrame extends JFrame {

public AppFrame(SplashScreen splashScreen) {
    JPanel test = new JPanel();
    test.setLayout(new GridLayout(0, 10));

    splashScreen.setProgress("jlabel", 10);
    for(int i = 0; i < 10000; i++) {
        test.add(new JButton("Hi..." + i));
        splashScreen.setProgress("jbutton", (int)(i * 0.1));
    }
    add(new JScrollPane(test));
    setPreferredSize(new Dimension(800, 600));
    pack();
    setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
    setLocationRelativeTo(null);
    splashScreen.setProgress("complete", 100);
    setVisible(true);
}
}
Damal answered 13/8, 2012 at 11:7 Comment(9)
" I believe GUI updates should be in invokeLater." Or invokeAndWait. But do some logging, I expect you will find the times between completion of splashScreen.showScreen(); and AppFrame frame = new AppFrame(splashScreen); is much less than you expect. Also note that the best splashes are strictly AWT (no Swing).Bomarc
That means I should use Window or Frame instead of JWindow? Thanks.Damal
Yes, use Window or Frame. Also look into MediaTracker/Canvas & EventQueue - there must be no Swing imports or classes used, or the entire Swing package is loaded 1st.Bomarc
Also consider a java-web-start splash screen.Hangbird
hmm ... is it really the ui creation that takes so long? If not, extract the real time-hogs (!ui = !EDT) and prepare them in the doInBackground of the the SwingWorker, publishing intermediate progress to the splash and close splash/show app in doneCharteris
@Charteris Should EDT be used for all swing related works including add(comp), new JPanel() etc or only for setVisible(), repaint() etcDamal
see docs.oracle.com/javase/tutorial/uiswing/concurrency/index.html - but I doubt that the creation of the ui really is the bottleneckCharteris
@Charteris Thanks. But if you look at the mock AppFrame code which I provided, it does take some time to load (some 10000 objects) and the progress bar not updating. What do you think could be the problem? Meanwhile, I will also find the exec times in my real code as well.Damal
why do you need tens of thousands of components? That's not the usual real-world scenario ...Charteris
S
2

An invokedLater is invoked from another invokeLater. The runnable will be executed only when the execution of the first one is finished.

You should modify the Splashscreen code like this :

...
private void runInEdt(final Runnable runnable) {
    if (SwingUtilities.isEventDispatchThread())
        runnable.run();
    else
        SwingUtilities.invokeLater(runnable);
}

public void showScreen() {
    runInEdt(new Runnable() {
        public void run() {
            setVisible(true);
        }
    });
}

public void close() {
    runInEdt(new Runnable() {
        public void run() {
            setVisible(false);
            dispose();
        }
    });
}

public void setProgress(final String message, final int progress) {
    runInEdt(new Runnable() {
        public void run() {
            progressBar.setValue(progress);
            if (message == null)
                progressBar.setStringPainted(false);
            else
                progressBar.setStringPainted(true);
            progressBar.setString("Loading " + message + "...");
        }
    });
}
Subsoil answered 13/8, 2012 at 13:44 Comment(5)
I tried your solution. Since it is already in EDT, else part in 'runInEdt' will not execute. Anyways I still have the same problem. The progress bar is not getting updated in EDT. I used this as an example and just added invokeLater in main(). Without invokeLater it works fine.Damal
The 'runInEDT' method ensures that your Splashscreen is thread safe. It can be invoked from any thread. I don't understand why my solution doesn't works for you. May be you could provide more details on the content of the AppFrame constructor.Subsoil
Pls see the edit which has a mock of my AppFrame. Basically I am loading my entire app components in that constructor. So I want to update the progress bar while the components load. But it is not showing even though it takes quite a time to load.Damal
Are you sure this mock behaviour is equivalent with the real one ? I mean, generally the loading time is not due to the gui initialization.Subsoil
Thanks it was due to one team member's dao code running in EDT.Damal
S
5

Hmm take a look at this working sample I put together which uses the SplashScreen class (only uses a simple Timer and ActionListener to increase the ProgressBar's value until 100 and a frame is shown):

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Font;
import java.awt.event.ActionListener;
import javax.swing.*;

public class SplashScreen extends JWindow {

    private static JProgressBar progressBar = new JProgressBar();
    private static SplashScreen splashScreen;
    private static int count = 1, TIMER_PAUSE = 25,PROGBAR_MAX=100;
    private static Timer progressBarTimer;
    ActionListener al = new ActionListener() {

        @Override
        public void actionPerformed(java.awt.event.ActionEvent evt) {
            progressBar.setValue(count);
            System.out.println(count);
            if (PROGBAR_MAX == count) {
                splashScreen.dispose();//dispose of splashscreen
                progressBarTimer.stop();//stop the timer
                createAndShowFrame();
            }
            count++;//increase counter

        }
    };

    public SplashScreen() {
        createSplash();
    }

    private void createSplash() {
        Container container = getContentPane();

        JPanel panel = new JPanel();
        panel.setBorder(new javax.swing.border.EtchedBorder());
        container.add(panel, BorderLayout.CENTER);

        JLabel label = new JLabel("Hello World!");
        label.setFont(new Font("Verdana", Font.BOLD, 14));
        panel.add(label);

        progressBar.setMaximum(PROGBAR_MAX);
        container.add(progressBar, BorderLayout.SOUTH);


        pack();
        setLocationRelativeTo(null);
        setVisible(true);

        startProgressBar();
    }

    private void startProgressBar() {
        progressBarTimer = new Timer(TIMER_PAUSE, al);
        progressBarTimer.start();
    }

    private void createAndShowFrame() {
        JFrame frame = new JFrame();
        frame.setSize(500, 500);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                splashScreen = new SplashScreen();
            }
        });
    }
}
Salinas answered 13/8, 2012 at 13:17 Comment(0)
S
2

An invokedLater is invoked from another invokeLater. The runnable will be executed only when the execution of the first one is finished.

You should modify the Splashscreen code like this :

...
private void runInEdt(final Runnable runnable) {
    if (SwingUtilities.isEventDispatchThread())
        runnable.run();
    else
        SwingUtilities.invokeLater(runnable);
}

public void showScreen() {
    runInEdt(new Runnable() {
        public void run() {
            setVisible(true);
        }
    });
}

public void close() {
    runInEdt(new Runnable() {
        public void run() {
            setVisible(false);
            dispose();
        }
    });
}

public void setProgress(final String message, final int progress) {
    runInEdt(new Runnable() {
        public void run() {
            progressBar.setValue(progress);
            if (message == null)
                progressBar.setStringPainted(false);
            else
                progressBar.setStringPainted(true);
            progressBar.setString("Loading " + message + "...");
        }
    });
}
Subsoil answered 13/8, 2012 at 13:44 Comment(5)
I tried your solution. Since it is already in EDT, else part in 'runInEdt' will not execute. Anyways I still have the same problem. The progress bar is not getting updated in EDT. I used this as an example and just added invokeLater in main(). Without invokeLater it works fine.Damal
The 'runInEDT' method ensures that your Splashscreen is thread safe. It can be invoked from any thread. I don't understand why my solution doesn't works for you. May be you could provide more details on the content of the AppFrame constructor.Subsoil
Pls see the edit which has a mock of my AppFrame. Basically I am loading my entire app components in that constructor. So I want to update the progress bar while the components load. But it is not showing even though it takes quite a time to load.Damal
Are you sure this mock behaviour is equivalent with the real one ? I mean, generally the loading time is not due to the gui initialization.Subsoil
Thanks it was due to one team member's dao code running in EDT.Damal
A
0
    Thread l_splash_thread = new Thread(
            new SplashScreen ());
    l_splash_thread.start();

    try {
        l_splash_thread.join();
    } catch (InterruptedException e1) {
        e1.printStackTrace();
    }

I think this way you can hold your splash scrren.

Anabasis answered 14/8, 2012 at 6:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.