4

Problem: graphics aren't repainted until after methods are run.

When button clicked two methods are called. Inside each method is code that is supposed to change the color of the graphic associated with this method (in the UI); when the method starts the graphic is changed from black to green; when the method finishes the color is changed from green to red. Then the next method is called and its graphic should turn green (method is running) and when the method finishes its graphic should be filled with red (method finished).

I created a simple status circle graphic (a 30 px circle with fill color) with 3 color states: black for ready; green for running; red for finished.

I believe the problem has to do with repaint() being on a separate thread and scheduled to run when able? I tried putting the code that updates the graphic inside its own thread-runnable and then using thread.join() to make sure the code had finished running but that didn't work.

EDIT

Edit: removing the code I had used for demonstration and replacing with a single, runnable code sample as per comments. What you'll see if you run the code is after you click the button the graphics don't update when each method is started and stopped, it waits until both methods have run and then repaints the graphics.

    package graphicsUpdateDemo;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Ellipse2D;
import java.beans.Transient;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

/**
 * Application entry
 */
public class App{
    public static void main(String[] args) {        
        SwingUtilities.invokeLater(new Runnable() {         
            @Override
            public void run() {
                new MainFrame();                
            }
        });
    }
}

/**
 * Main frame
 */
class MainFrame extends JFrame implements SomeListener{
    private AddedPanel addedPanel;

    // Constructor
    public MainFrame(){
        // Set frame properties
        setSize(500, 500);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setVisible(true);
        setLayout(new FlowLayout(FlowLayout.CENTER, 5, 20));

        // Create AddedPanel.
        addedPanel = new AddedPanel();
        add(addedPanel);

        // Set AddedPanel listener to this JFrame.
        addedPanel.setSomeListener(this);       
    }

    // AddedPanel listener method
    @Override
    public void doStuff() {
        // run simulated sort methods
        sort1();
        sort2();        
    }

    // Simulated sort 
    // .......graphic should turn green as soon as method starts
    // .......graphic should turn red as soon as method finishes.
    private void sort1() {
        // repaint graphic to show method is starting
        addedPanel.statusOne.setStatus(SortStatus.running);

        // EDIT: Make panel repaint itself.
        addedPanel.paintImmediately(0, 0, getWidth(), getHeight());

        // Simulate work being done.
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }       

        // repaint graphic to show methid is finished
        addedPanel.statusOne.setStatus(SortStatus.finished);
        // EDIT: Make panel repaint itself.
        addedPanel.paintImmediately(0, 0, getWidth(), getHeight());
    }

    // Simulated sort
    // .......graphic should turn green as soon as method starts
    // .......graphic should turn red as soon as method finishes.
    private void sort2() {
        // repaint graphic to show method is starting (green)
        addedPanel.statusTwo.setStatus(SortStatus.running);
        // EDIT: Make panel repaint itself.
        addedPanel.paintImmediately(0, 0, getWidth(), getHeight());

        // Simulate work being done.
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }   

        // repaint graphic to show method is finished.
        addedPanel.statusTwo.setStatus(SortStatus.finished);
        // EDIT: Make panel repaint itself.
        addedPanel.paintImmediately(0, 0, getWidth(), getHeight());
    }
}

/**
 * Panel to add to MainFrame
 */
class AddedPanel extends JPanel{
    // Button listener
    SomeListener listener;
    // Button
    private JButton aButton = new JButton("Click Me");

    // Create Status Circles for showing method state.
    public StatusCircles statusOne = new StatusCircles();
    public StatusCircles statusTwo = new StatusCircles();

    // Constructor.
    public AddedPanel(){
        setLayout(new BorderLayout(0, 15));

        // Add button to panel.
        add(aButton, BorderLayout.NORTH);

        // Make panel for holding graphics and labels.
        JPanel resultsPanel = new JPanel(new GridBagLayout());
        GridBagConstraints c = new GridBagConstraints();
        c.insets = new Insets(5, 5, 5, 5);
        resultsPanel.add(statusOne, c);
        c.gridx = 1;
        resultsPanel.add(new JLabel("Method A"), c);
        c.gridx = 0; c.gridy = 1;
        resultsPanel.add(statusTwo, c);
        c.gridx = 1;
        resultsPanel.add(new JLabel("Method B"), c);

        add(resultsPanel, BorderLayout.CENTER);

        aButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent arg0) {
                if(listener != null){
                    listener.doStuff();
                }               
            }
        });
    }

    public void setSomeListener(SomeListener listener){
        this.listener = listener;
    }
}

/**
 * Graphic for showing user state of method:
 *      black for ready
 *      green for running
 *      red for finished
 */
class StatusCircles extends JPanel{
    private SortStatus sortStatus;
    private Ellipse2D.Double statusCircle = new Ellipse2D.Double(2, 2, 25, 25);

    // Constructor
    public StatusCircles(){
        sortStatus = SortStatus.ready;
    }

    @Override
    protected void paintComponent(Graphics g) {     
        // Cast Graphics to Graphics2D
        Graphics2D g2 = (Graphics2D)g;

        // Turn on anti aliasing
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        // Set background
        g2.setColor(Color.BLACK);
        g2.fillRect(0, 0, getWidth(), getHeight());

        // Fill status circle with color based on status field
        switch(sortStatus){
        case ready:
            g2.setColor(Color.BLACK);
            g2.fill(statusCircle);
            break;
        case running:
            g2.setColor(Color.GREEN);
            g2.fill(statusCircle);
            break;          
        case finished:
            g2.setColor(Color.RED);
            g2.fill(statusCircle);
            break;
        }
    }       

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

    // Set state method is in.
    public void setStatus(SortStatus status) {
        this.sortStatus = status;       
        repaint();      
    }   
}

/**
 * Interface
 */
interface SomeListener{
    public void doStuff();
}

/**
 * Enum for depicting status of graphic.
 */
enum SortStatus {
    ready,
    running,
    finished
}

EDIT

"The repaint method lodges a request to update the viewing area and returns immediately. Its effect is asynchronous, meaning that it is up to the JVM to execute the paintComponent method on a separate thread." - Introduction to Java programming by Liang.

I think the problem is either A) in my ignorance my program design is doing something no sane programmer would do, and/or B) I don't know how to make the program change graphics colors then after that happens, then continue doing work on whatever thread the work is being done on (EDT, main thread?).

I did run into an answer that suggested never to slow down the "main thread" to wait for things to be drawn; and to instead make icons for each status circle and then swap icons - which I guess would force an immediate redraw of whatever is holding the icon? Wouldn't this, though, suggest there is a way to force an immediate repaint?

Thought experiment: you have a loop that runs 100 times, each iteration takes a second. You want to show the user each iteration by changing the color of a circle to one of a hundred different colors. Would you have to make 100 different icons for this? Or, and what I want to do, is change the fill color of the circle each iteration. ...but how to force the repainting of the circle with each iteration?

EDIT

Don't know if it is the "right" solution but the program now functions as I want it to. I placed these addedPanel.paintImmediately(0, 0, getWidth(), getHeight()); directly after the method calls asking for the graphic color to change. I updated the working example code above, the edits are depicted by "//EDIT: Make panel repaint itself".

EDIT

Now I have more confidence that I am on the right track. I believe I have implemented the things recommended to me. Understanding SwingWorker came really fast once I understood it was basically like Android's asynTask() (that's where I learned it first, that's why I say it like that). And the simulated work via sleeping is occurring in its own thread, off the EDT, so's okay now (?) ((not that I need my program to take a nap)) Here, now, is the full working code:

package graphicsUpdateDemo;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Ellipse2D;
import java.beans.Transient;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;

/**
 * Application entry
 */
public class App{
    public static void main(String[] args) {        
        SwingUtilities.invokeLater(new Runnable() {         
            @Override
            public void run() {
                new MainFrame();                
            }
        });
    }
}

/**
 * Main frame
 */
class MainFrame extends JFrame implements SomeListener{
    private AddedPanel addedPanel;

    // Constructor
    public MainFrame(){
        // Set frame properties
        setSize(500, 500);
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        setLayout(new FlowLayout(FlowLayout.CENTER, 5, 20));

        // Create AddedPanel.
        addedPanel = new AddedPanel();
        add(addedPanel);

        // Set AddedPanel listener to this JFrame.
        addedPanel.setSomeListener(this);

        // Call setVisible last
        setVisible(true);
    }

    // AddedPanel listener method
    @Override
    public void doStuff() {
        // Call sort1(), when that finishes have it call sort2().
        sort1();

    }

    // Simulated sort 
    // .......graphic should turn green as soon as method starts
    // .......graphic should turn red as soon as method finishes.
    private void sort1() {
        // repaint graphic to show method is starting
        addedPanel.statusOne.setStatus(SortStatus.running);

        // Run sort in its own thread.
        SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>(){

            @Override
            protected Void doInBackground() throws Exception {
                // Simulate work being done.
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return null;
            }

            @Override
            protected void done() {             
                // repaint graphic to show methid is finished
                addedPanel.statusOne.setStatus(SortStatus.finished);

                // Call sort2
                sort2();
            }

        };      
        worker.execute();       
    }

    // Simulated sort
    // .......graphic should turn green as soon as method starts
    // .......graphic should turn red as soon as method finishes.
    private void sort2() {
        // repaint graphic to show method is starting (green)
        addedPanel.statusTwo.setStatus(SortStatus.running);

        // Run sort in its own thread
        SwingWorker<Void, Void> worker = new SwingWorker<Void, Void>(){

            @Override
            protected Void doInBackground() throws Exception {
                // Simulate work being done.
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return null;
            }

            @Override
            protected void done() {             
                // repaint graphic to show method is finished.
                addedPanel.statusTwo.setStatus(SortStatus.finished);
            }

        };      
        worker.execute();
    }
}

/**
 * Panel to add to MainFrame
 */
class AddedPanel extends JPanel{
    // Button listener
    SomeListener listener;
    // Button
    private JButton aButton = new JButton("Click Me");

    // Create Status Circles for showing method state.
    public StatusCircles statusOne = new StatusCircles();
    public StatusCircles statusTwo = new StatusCircles();

    // Constructor.
    public AddedPanel(){
        setLayout(new BorderLayout(0, 15));

        // Add button to panel.
        add(aButton, BorderLayout.NORTH);

        // Make panel for holding graphics and labels.
        JPanel resultsPanel = new JPanel(new GridBagLayout());
        GridBagConstraints c = new GridBagConstraints();
        c.insets = new Insets(5, 5, 5, 5);
        resultsPanel.add(statusOne, c);
        c.gridx = 1;
        resultsPanel.add(new JLabel("Method A"), c);
        c.gridx = 0; c.gridy = 1;
        resultsPanel.add(statusTwo, c);
        c.gridx = 1;
        resultsPanel.add(new JLabel("Method B"), c);

        add(resultsPanel, BorderLayout.CENTER);

        aButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent arg0) {
                if(listener != null){
                    listener.doStuff();
                }               
            }
        });
    }

    public void setSomeListener(SomeListener listener){
        this.listener = listener;
    }
}

/**
 * Graphic for showing user state of method:
 *      black for ready
 *      green for running
 *      red for finished
 */
class StatusCircles extends JPanel{
    private SortStatus sortStatus;
    private Ellipse2D.Double statusCircle = new Ellipse2D.Double(2, 2, 25, 25);

    // Constructor
    public StatusCircles(){
        sortStatus = SortStatus.ready;
    }

    @Override
    protected void paintComponent(Graphics g) {     
        // Cast Graphics to Graphics2D
        Graphics2D g2 = (Graphics2D)g;

        // Turn on anti aliasing
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        // Set background
        g2.setColor(Color.BLACK);
        g2.fillRect(0, 0, getWidth(), getHeight());

        // Fill status circle with color based on status field
        switch(sortStatus){
        case ready:
            g2.setColor(Color.BLACK);
            g2.fill(statusCircle);
            break;
        case running:
            g2.setColor(Color.GREEN);
            g2.fill(statusCircle);
            break;          
        case finished:
            g2.setColor(Color.RED);
            g2.fill(statusCircle);
            break;
        }
    }       

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

    // Set state method is in.
    public void setStatus(SortStatus status) {
        this.sortStatus = status;       
        repaint();      
    }   
}

/**
 * Interface
 */
interface SomeListener{
    public void doStuff();
}

/**
 * Enum for depicting status of graphic.
 */
enum SortStatus {
    ready,
    running,
    finished
}
ross studtman
  • 936
  • 1
  • 8
  • 22
  • You could try Component.setIgnoreRepaint(true), so java knows you only want to repaint manually. – w4etwetewtwet Jul 15 '13 at 19:43
  • hmmm by default isn't there any issue to paint whatever to the JPanel, from Image, shapes to animations e.g. fireworks, I'm afraid nobody knows how this works, for better help sooner post an [SSCCE[http://sscce.org/), short, runnable, compilable – mKorbel Jul 15 '13 at 20:21
  • 1
    don't sleep the EDT ... – kleopatra Jul 16 '13 at 08:37
  • Swing event handling (dispatching) runs on its own thread, the EDT - just learned that. In the above the sleep only exists to simulate work being done, its not in my actual program; I put it in so we can appreciate in the above program that both status circles go from black to red, skipping green, at the completion of the two methods. When what should happen is the first status circle turns green for 2 seconds (sleep duration), then turns red, then the second status circle turns green for 2 seconds, then turns red. But it waits 4 seconds and both immediately jump to red. – ross studtman Jul 16 '13 at 11:41
  • 1
    _only exists to simulate work being done_ it's doing that simulation incorrectly (as you probably learned by now :-) - however chewy, @trashgod 's approach points the direction to go. As to your thought experiment: at the end of each iteration, set the color to your swing component (on the EDT!) - if implemented correctly (as your statusPanel does on changing status), it will take care of its own repaint. – kleopatra Jul 16 '13 at 13:49
  • @kleopatra, how would I re-code the example to simulate a method that takes a second or two to run? – ross studtman Jul 16 '13 at 15:48
  • easiest for simulation would be to use a (Swing!) Timer - but as you'll need a more sophisicated approach anyway, go and follow @trashgod. I'm off now, since I know you are in good hands, if you do :-) – kleopatra Jul 16 '13 at 15:59
  • +1 for an [sscce](http://sscce.org/); `paintImmediately()` works, but it tightly couples your model (sorting latency) and view (changing color). Notice how `sleep()` blocks the EDT making the GUI unresponsive. For fixed delays, use `javax.Swing.Timer`; to simulate sorting latency, go with `SwingWorker`. – trashgod Jul 16 '13 at 17:07
  • @ross studtman paintImmediately caused exception from RepaintManager in the case that EDT isn't empty, is active, your Swing GUi will be freeeze forever, forgot about .... – mKorbel Jul 16 '13 at 18:53
  • @mKorbel is correct; BTW, I had to move `setVisible()` to the end of the constructor. – trashgod Jul 16 '13 at 21:38

1 Answers1

5

Using the approach shown here, let each sort update its display from a separate SwingWorker, while a Supervisor worker monitors a CountDownLatch to determine when all sorts are done.

image

Addendum: I have never seen an Applet before, nor a SwingWorker…I don't understand why I would need to determine when all the sorts are done…I have edited the question.

  • The example is also a hybrid.

  • SwingWorker helps avoid blocking the EDT.

  • Determining done tells you when to (re-)enable the start button.

  • Try adding a PropertyChangeListener, shown here, to a ColorIcon, seen here, in your label. Each time you setProgress() in the worker, you'll see a corresponding PropertyChangeEvent.

Community
  • 1
  • 1
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • I spent an hour with your answer last night, I'll spend more time today. It's a chewy answer for me because I have never seen an Applet before, nor a SwingWorker or Supervisor; though I have been introduced to CountDownLatches. Yet, I confess, I don't understand why I would need to determine when all the sorts are done (I have edited the question, hopefully for the better, since you posted this answer; ie, I probably wasn't clear about what I wanted). – ross studtman Jul 16 '13 at 11:50
  • 1
    thank you for your help, really appreciate it. I have less than a week experience with Java Swing so your answer was a little bit of a deep-six for me but I know I need to understand what you helped with so I will spend some time with it. Thanks again. – ross studtman Jul 16 '13 at 17:51
  • @rossstudtman: Sorry for the tsunami; look for more [examples](http://sscce.org/) on these topics from top [tag:Swing] users; ping me here if you update the question. – trashgod Jul 16 '13 at 21:40
  • I think I have it. Thank you for your assistance. I didn't get to the PropertyChangedListener bit but not sure I need it; I feel like I have correctly implemented the functionality I am looking for. And, even better, it makes sense, now, why I was asked to do it this way. – ross studtman Jul 19 '13 at 00:52
  • 2
    Good foundation; going forward, you may be able use two instances of a single worker that implements a suitable interface. – trashgod Jul 19 '13 at 01:22
  • 2
    @rossstudtman (expanding on trashgod's last comment): or a single worker instance that takes a list of tasks which it processes one after the other – kleopatra Jul 19 '13 at 07:18