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
}
Using the approach shown here, let each sort update its display from a separate
SwingWorker
, while aSupervisor
worker monitors aCountDownLatch
to determine when all sorts are done.Addendum: I have never seen an
Applet
before, nor aSwingWorker
…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 aColorIcon
, seen here, in your label. Each time yousetProgress()
in the worker, you'll see a correspondingPropertyChangeEvent
.