My problem: Efficient way for the program to delay in my scenario.
My Scenario:
I am making a quiz game for a short project, long story short. A question is answered when the user presses one of four Jbuttons having an answer on them. Then my quizengine class calls the paintbutton method showed here. After that it proceeds to call some other methods responsible for making the next question appear. Now what I want is to make the button change between two colors with decreasing time intervals.
What have I tried so far:
First I placed a JoptionPane between the paintbutton method and the method that changes the interface to the next question just to see if the button would change color. It did successfully. Of course that wasn't my intention, I wanted just a time interval.
Then I tried using Thread.Sleep. Although the program would wait before changing to the next question, the color change was not visible.
Finally I tried an implementation of the Timer thingy (probably not correctly) which although it changed the color the program went ahead and proceeded to the next question.
My want to be code
/* Paints the button pressed red or green
* Green if the anwser is correct red if
* its false
*
* @param int bnr the number of the button
* @param boolean corr true if correct false if false :D
*/
public static void paintbutton(int bnr,boolean corr) {
for (int i=10;i>1;i--){
b[bnr-1].setBackground(null);
//wait for i*100 milliseconds
b[bnr-1].setBackground(corr?Color.green:Color.red);
}
}
Let's start with some basics. Swing is single threaded, meaning you must never block or execute long running code within the context of the Event Dispatching Thread, this will make the UI freeze and the user upset
Swing is NOT thread safe, this means that you should never create or update the UI from outside of the context of the EDT.
This leads you into a problem. You want to, after a some small delay, update the UI.
Lucky for you, there are at least two possibilities, a Swing Timer
or a SwingWorker
.
A Swing Timer
is relatively simple, but doesn't really provide a means to generate a variable delay between updates. A SwingWorker
is more complex, but gives you the control to basically do what you want.
Both of these (can) "wait" outside of the EDT, but both provide means by which you can push updates to the EDT safely.
For example...
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingWorker;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
public class BlinkyTheButton {
public static void main(String[] args) {
new BlinkyTheButton();
}
public BlinkyTheButton() {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
ex.printStackTrace();
}
JFrame frame = new JFrame("Testing");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new TestPane());
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
public class TestPane extends JPanel {
private JButton blinky;
public TestPane() {
blinky = new JButton("Blinky");
blinky.setOpaque(true);
blinky.addActionListener(new ActionListener() {
private BlinkWorker worker;
@Override
public void actionPerformed(ActionEvent e) {
if (worker == null || worker.getState() == SwingWorker.StateValue.DONE) {
worker = new BlinkWorker(blinky, Color.RED);
worker.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
SwingWorker worker = (SwingWorker) evt.getSource();
if ("state".equals(evt.getPropertyName())) {
if (worker.getState() == SwingWorker.StateValue.DONE) {
// this is where you would then call the method to
// update the state for a new question
}
}
}
});
worker.execute();
}
}
});
setLayout(new GridBagLayout());
add(blinky);
}
@Override
public Dimension getPreferredSize() {
return new Dimension(200, 200);
}
public class BlinkWorker extends SwingWorker<Void, Color> {
private JButton btn;
private Color color;
private Color normal;
public BlinkWorker(JButton btn, Color color) {
this.btn = btn;
normal = btn.getBackground();
this.color = color;
}
@Override
protected Void doInBackground() throws Exception {
for (int index = 10; index > 1; index--) {
publish(color);
Thread.sleep(index * 100);
publish(normal);
Thread.sleep(index * 100);
}
return null;
}
@Override
protected void process(List<Color> chunks) {
Color color = chunks.get(chunks.size() - 1);
btn.setBackground(color);
}
}
}
}
Have a look at Concurrency in Swing, How to use Swing Timers and Worker Threads and SwingWorker for more details
Found a workaround!
Ok I don't know on how many levels my solution is wrong but it seems to do the trick. I noticed that if a JOptionPane.showMessageDialog is used the button is being painted normally. Now the next problem was the user not having to press the OK button on the dialog ... So I used a robot! Here is the code:
public static void paintbutton(int bnr,boolean corr) {
long time;
try {
Robot robot = new Robot();
for (int i=5;i>1;i--){
b[bnr-1].setBackground(null);
// Simulate a key press
robot.keyPress(KeyEvent.VK_SPACE);
robot.keyRelease(KeyEvent.VK_SPACE);
JOptionPane.showMessageDialog(null,"hi");
time = System.currentTimeMillis();
do {
}while(time+i*100>System.currentTimeMillis());
b[bnr-1].setBackground(i==1?(corr?Color.green:Color.red):Color.yellow);
// Simulate a key press
robot.keyPress(KeyEvent.VK_SPACE);
robot.keyRelease(KeyEvent.VK_SPACE);
JOptionPane.showMessageDialog(null,"hi");
time = System.currentTimeMillis();
do {
}while(time+i*100>System.currentTimeMillis());
}
} catch (AWTException e) {
System.err.println("error");
}
}
Ok its messy, the message dialog appears briefly but due to the location of the game frame its hidden. If you move main frame it appears briefly etc. etc. But it seems I am on to something here. Something that happens when the JOptionPane is created makes the buttons repaint. If anyone got an idea on how to recreate what happens when the JOptionPane is created without having to create it , it would be great!