Search code examples
javauser-interfacejbuttonactionlistener

Make an ActionListener commit a change to a JButton before it has finished executing the entire ActionListener


In Java, I'm using an ActionListener for an array of JButtons. I would like for an earlier part of the ActionListener to set a new ImageIcon to a JButton, that change to be displayed immediately, then near the end of the ActionListener to set the JButton's ImageIcon back to null after a second long delay.

My problem is that none of the changes that happen to the JButton get displayed in the GUI window that it is set in until the ActionListener is completely finished, making the change in the JButton's ImageIcon unnoticeable. Is there any way to make an ActionListener commit a change to a JButton before it has finished executing the entire ActionListener, or should I be going about this differently?


Solution

  • The reason this is happening:

    Swing repaints the buttons on the same thread (EDT) as the ActionListener is ran on. Hence if it is executing you ActionListener it cannot repaint since the thread is busy - as simple as that. You may have noticed that while your action listener is executing you also can't properly move your frames around etc. (GUI freezes up).

    The solution:

    Move heavy processing outside the EDT. It doesn't belong there anyway. As you could have guessed - use a background thread/thread pool for that. A good guide to it is Swing tutorial for concurrency

    Notes:

    As portrayed in the guide you do not want to modify components outside the EDT. As such the easiest strategy is to make a Runnable to execute on a background thread, start it, change the picture on the button and return without waiting for the task to finish.

    public void actionPerformed(ActionEvent ae) {
        Runnable task = new Runnable() {..};
        executor.execute(task);
        button.setIcon(newIcon);
        return;
    }
    

    Note that this doesn't lock up the EDT for the task, hence allowing Swing to change the picture immediately.

    This of course means that the user has no idea if the task has finished or not (And if there were any exceptions)! It is in the background after all! Hence there is an extra state of your execution: GUI is responsive and non-frozen, button is changed, but task is still running. In most applications this may be a problem (the user will spam the button or your background tasks may interleave). In that case you may want to use a SwingWorker to have a "processing" state as well.

    public void actionPerformed(ActionEvent ae) {
        new TaskWorker().execute();
        button.setIcon(loadingIcon); //Shows loading. Maybe on button, maybe somewhere else.
        return;
    }
    
    private class TaskWorker extends SwingWorker<Void, T> {
        public Void doInBackground() {
            //Do your task in the background here
        }
        protected void done() {
            try {
                get();
                button.setIcon(doneIcon);
            catch (<relevant exceptions>) {
                button.setIcon(failedIcon);
            }
        }
    }  
    

    Here done() is called on the EDT when doInBackground() is finished.