0

I would like to display an opaque overlay with a loading description or spinner over the top of my JFrame when a button is clicked and disappear again when the action has been performed. I have read up on Glass Pane but I cannot understand the right way to do this underneath an action performed function with a button. Is there a way to do this using Java and Swing? By the way here is my JFrame at the moment...

public class Frame {

private JButton btnH;

/** Main Panel */
private static final Dimension PANEL_SIZE = new Dimension(500, 500);
private static JPanel panel = new JPanel();

public Frame() {
   init();
   panel.setLayout(null);
   panel.setPreferredSize(PANEL_SIZE);
}

public void init() {
   btnH = new JButton("HELP");
   btnH.setBounds(50, 50, 100, 25);

   panel.add(btnH);

   // Action listener to listen to button click and display pop-up when received.
   btnH.addActionListener(new ActionListener() {
       public void actionPerformed(ActionEvent event) {
            // Message box to display.
            JOptionPane.showMessageDialog(null, "Helpful info...", JOptionPane.INFORMATION_MESSAGE);
       }
   });
} 

public JComponent getComponent() {
    return panel;
}

private static void createAndDisplay() {
    JFrame frame = new JFrame("Frame");
    frame.getContentPane().add(new Frame().getComponent());
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.pack();
    frame.setLocationRelativeTo(null);
    frame.setVisible(true);
} 

public static void main(String[] args) {
    java.awt.EventQueue.invokeLater(new Runnable() {
        public void run() {
            createAndDisplay();
        }
    });
}
JS91
  • 5
  • 3
  • The definition is not clear to me. Do you want to display `JOptionPane` over the `JFrame` and remove it when a certain action is completed ? – c0der Jan 17 '20 at 13:34
  • *I would like to display an opaque overlay with a loading description or spinner* - then just use a JProgressBar as suggestion in an answer below. The Swing tutorial has a working example. If you want more control and you want a semi-opaque overlay then you can use a `Glass Pane`. See: https://stackoverflow.com/questions/27822671/change-display-cursor-with-text/27822784#27822784 for an implementation with a simple API. – camickr Jan 17 '20 at 15:53

2 Answers2

1

If I understood correctly, you need to display some sort of ongoing progress until the button finishes its work.

If so, you can use a CardLayout with an indeterminate JProgressBar.

CardLayout is a LayoutManager (such as BorderLayout, FlowLayout, etc...) which lets you define cards. Each card is a Component (such as a Container with other Components, such as a JPanel for example). Only one card can be visible at any time. Each card is associated with a String to identify it and being able to select it as visible over the others. You can read more about CardLayout in the corresponding Java tutorial.

A JProgressBar is a progress bar, ie a JComponent which shows you the progress of an ongoing task. There are two modes: determinate and indeterminate. In determinate mode you specify the size of the problem and you advance the progress bar yourself via code. In indeterminate mode, there is constantly an indicator knob spinning (which lets the user know that there is an ongoing task in progress and that the program does not know how long will it take). JProgressBar can be used as just a simple visual indicator for the user. You can read more about JProgressBar in the corresponding Java tutorial.

So, in your case, you can use a CardLayout with two cards, where one card contains the "Help" button, and the other an indeterminate JProgressBar. When the user clicks on "Help", you show the progress bar card, and when the progress is done you switch back to the "Help" button card.

Now if you do the progress inside the ActionListener of the button "Help", that will run on the Event Dispatch Thread (or EDT for short). So your cards will not be able to be switched because the switching is done also inside the EDT. In this case we are going to create a separate SwingWorker to handle the progress. So the only thing the ActionListener will do, is to create and start such a SwingWorker. So that will let the ActionListener end before the end of the progress, so the cards will be switched.

Consider the following sample code:

import java.awt.CardLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.FlowLayout;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.SwingWorker;

public class MyPanel extends JPanel {

    private static final Dimension PANEL_SIZE = new Dimension(500, 500);

    public MyPanel() {
        //Always prefer a layout instead of setting it to null.
        super(new CardLayout()); //Set the layout of the main panel to CardLayout, so we can add the cards...

        //Obtain the CardLayout we just created for this panel:
        final CardLayout cardLayout = (CardLayout) super.getLayout();

        //String names of each card:
        final String nameForCardWithButton = "BUTTON",
                     nameForCardWithProgress = "PROGRESS";

        //Creating first card...
        final JPanel cardWithButton = new JPanel(new GridBagLayout());
        /*Using a GridBagLayout in a panel which contains a single component (such as the
        cardWithButton panel, containing a single JButton) will layout the component in
        the center of the panel.*/
        final JButton btnH = new JButton("HELP");
        cardWithButton.add(btnH);

        // Action listener to listen to button click and display pop-up when received.
        btnH.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent event) {

                cardLayout.show(MyPanel.this, nameForCardWithProgress); //Switch to progress bar card...

                //Create and start worker Thread:
                new SwingWorker() {
                    @Override
                    protected Object doInBackground() throws Exception {
                        /*Simulate a long ongoing process without blocking the EDT...
                        Well, actually, the JOptionPane will block the EDT I think, so I will leave
                        it here for demonstration puprposes only.*/
                        JOptionPane.showMessageDialog(null, "Helpful info...", "info", JOptionPane.INFORMATION_MESSAGE);
                        return null; //Not sure if returning null here is a good practice or not.
                    }

                    @Override
                    protected void done() {
                        cardLayout.show(MyPanel.this, nameForCardWithButton); //Switch back to button card, when the job has finished.
                    }
                }.execute();
            }
        });

        //Creating second card...
        final JPanel cardWithProgress = new JPanel(new FlowLayout());
        final JProgressBar bar = new JProgressBar();
        bar.setIndeterminate(true); //Here we initialize the progress bar to indeterminate mode...
        cardWithProgress.add(bar);

        super.add(cardWithButton, nameForCardWithButton);
        super.add(cardWithProgress, nameForCardWithProgress);
        super.setPreferredSize(PANEL_SIZE);
    }

    private static void createAndDisplay() {
        JFrame frame = new JFrame("Frame");
        frame.getContentPane().add(new MyPanel());
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    } 

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                createAndDisplay();
            }
        });
    }
}

You can see here the creation and initialization of MyPanel which is the container with CardLayout. In it, we add the two cards. One with the button and one with the progress bar.

gthanop
  • 3,035
  • 2
  • 10
  • 27
  • Just pointing out that Swing is NOT thread save and you should NOT be modifying the state of the UI from outside of the context of the EDT – MadProgrammer Jan 17 '20 at 19:37
  • @MadProgrammer ok. Any recommended way of improving my answer? For example would *SwingUtilities.invokeLater(...)* for the long ongoing task solve this sutiation? – gthanop Jan 17 '20 at 19:41
  • @MadProgrammer but then again the EDT would be blocked as long as the long ongoing task was in progress. I don't know what to do. Maybe a *SwingWorker*?. – gthanop Jan 17 '20 at 19:49
  • Well, instance of using a `Thread`, I'd use a `SwingWorker`, which is designed to solve this immediate issue. In your example, you actually don't need the thread at all, the `JOptionPane` will (safely) block the EDT until the dialog is dismissed – MadProgrammer Jan 17 '20 at 20:10
  • Personally, I'm sure `CardLayout` would be the best choice for this as it assumes a particular set of design choices (ie you're not using some other navigation mechanism, you're not trying to design a re-usable solution) – MadProgrammer Jan 17 '20 at 20:12
  • @MadProgrammer ok, I will update to SwingWorker. *you actually don't need the thread at all* wait, what? I mean, I'm simulating a long ongoing task with the JOptionPane pop up... Where can I read on how to design reusable solutions? – gthanop Jan 17 '20 at 20:20
  • 1
    Ok, first, I appreciate that this is just an "example" - but you could have done `Thread.sleep` or something. Using a `JOptionPane` in this case was probably just a bad choice - especially considering it's violating the Swing threading capabilities. The problem with the use of `CardLayout` like this means you're making an assumption about how the UI is laid out and managed, which means that a developer may need to make major changes to support this kind of work flow, which might not always be possible. – MadProgrammer Jan 17 '20 at 21:29
  • 1
    Any "solution" should first, try not to "assume" anything about the current design and second not require the developer to make major changes in order to incorporate it. Not always an easy thing. There is no where you can read up about this, it's something you need to learn. Try imagining you have a very large and complex UI, with a complex hierarchy. In this case, I might be tempted to simply provide a simple "progress pane" which would update the progress bar based on feedback (via a `ProgressListener` for example) - this decouples the code and it doesn't care about how the work is done – MadProgrammer Jan 17 '20 at 21:32
1

There hard part about this isn't the glass pane, but the interoperability between the UI and the SwingWorker.

There are lots of ways you might do this, this is just one.

You should start by reading through How to Use Root Panes which goes into how to use glass panes and Worker Threads and SwingWorker, because until you get your head around it, it will mess with you.

The important things to note here are:

  • Swing is single threaded, you should not block the Event Dispatching Thread, which is what the SwingWorker is for
  • Swing is NOT thread safe. Which means you should not modify or update the UI, directly or indirectly, from outside the context of the Event Dispatching Thread

Hence, the importance of the SwingWorker

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.MouseAdapter;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;

public class Test {

    public static void main(String[] args) {
        new Test();
    }

    public Test() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() {
            setLayout(new GridBagLayout());

            JButton workButton = new JButton("Do some work already");
            add(workButton);

            workButton.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    workButton.setEnabled(false);
                    ProgressPane progressPane = new ProgressPane();
                    // This is a dangrous kind of thing to do and you should
                    // check that the result is a JFrame or JDialog first
                    JFrame parent = (JFrame) SwingUtilities.windowForComponent(TestPane.this);
                    parent.setGlassPane(progressPane);
                    progressPane.setVisible(true);
                    // This is a little bit of overkill, but it allows time
                    // for the component to become realised before we try and
                    // steal focus...
                    SwingUtilities.invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            progressPane.requestFocusInWindow();
                        }
                    });
                    Worker worker = new Worker();
                    worker.addPropertyChangeListener(new PropertyChangeListener() {
                        @Override
                        public void propertyChange(PropertyChangeEvent evt) {
                            if ("state".equals(evt.getPropertyName())) {
                                if (worker.getState() == SwingWorker.StateValue.DONE) {
                                    progressPane.setVisible(false);
                                    workButton.setEnabled(true);
                                }
                            } else if ("progress".equals(evt.getPropertyName())) {
                                double value = (int) evt.getNewValue() / 100.0;
                                progressPane.progressChanged(value);
                            }
                        }
                    });
                    worker.execute();
                }
            });
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }

    }

    public class Worker extends SwingWorker<Object, Object> {

        @Override
        protected Object doInBackground() throws Exception {
            for (int value = 0; value < 100; value++) {
                Thread.sleep(100);
                value++;
                setProgress(value);
            }
            return this;
        }

    }

    public interface ProgressListener {

        public void progressChanged(double progress);
    }

    public class ProgressPane extends JPanel implements ProgressListener {

        private JProgressBar pb;
        private JLabel label;

        private MouseAdapter mouseHandler = new MouseAdapter() {
        };
        private KeyAdapter keyHandler = new KeyAdapter() {
        };
        private FocusAdapter focusHandler = new FocusAdapter() {
            @Override
            public void focusLost(FocusEvent e) {
                if (isVisible()) {
                    requestFocusInWindow();
                }
            }
        };

        public ProgressPane() {
            pb = new JProgressBar(0, 100);
            label = new JLabel("Doing important work here...");

            setLayout(new GridBagLayout());
            GridBagConstraints gbc = new GridBagConstraints();
            gbc.gridwidth = GridBagConstraints.REMAINDER;
            gbc.insets = new Insets(8, 8, 8, 8);
            add(pb, gbc);
            add(label, gbc);

            setOpaque(false);
        }

        @Override
        public void addNotify() {
            super.addNotify();

            addMouseListener(mouseHandler);
            addMouseMotionListener(mouseHandler);
            addMouseWheelListener(mouseHandler);

            addKeyListener(keyHandler);

            addFocusListener(focusHandler);
        }

        @Override
        public void removeNotify() {
            super.removeNotify();

            removeMouseListener(mouseHandler);
            removeMouseMotionListener(mouseHandler);
            removeMouseWheelListener(mouseHandler);

            removeKeyListener(keyHandler);

            removeFocusListener(focusHandler);
        }

        @Override
        public void progressChanged(double progress) {
            pb.setValue((int) (progress * 100));
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            g2d.setColor(new Color(128, 128, 128, 224));
            g2d.fillRect(0, 0, getWidth(), getHeight());
            g2d.dispose();
        }

    }

}

Just as a side note, I have verified that the PropertyChangeListener used by the SwingWorker is updated within the context of the EDT

You should also take a look at JLayer (formally known as JXLayer)

For example, example

It's like glass pane on steroids

Now, if you really want to do something fancy, you could do something like this for example

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • Thanks so much @MadProgrammer! This has really helped me and the information you provided is great! I will definitely be trying some of your ideas out. – JS91 Jan 18 '20 at 19:45