2007-10-26

Swing Tip: Heavy Paint Job...

Have you ever needed to make some heavy 2D rendering operation in Swing and you didn't want to block the Swing Thread? Well, I had to, several times and today I made a hack for a colleague that I would like to built upon here.

My first attempt to ensure that the Swing Thread wasn't blocked was to conduct all rendering on a background thread. This obviously violated all Swing and Java2D principles. If you don't know, just Google for it, but the fact is that all rendering should be done by the Event Dispatch Thread. So, all Swing activities should also be done in that same thread. What happens if the scene you are rendering is rather complex and you don't want to hog the Even Dispatch Thread?

A simple technique can be applied. The idea is to hold the context when the rendering is started. If the rendering has been going for quite some time, place an event in the Event Queue to resume it later. This allows the Event Dispatch Thread to process other events. When the rendering is resumed we can check if the context has changed. If it has we probably have to start the painting process all over. If it hasn't we can resume were we left of.

Here is a sample code for this. The rendering is completely stupid, but the idea is to emphasize that you can put any rendering code in it.

  1: import java.awt.BorderLayout;
2: import java.awt.Color;

3: import java.awt.Dimension;
4: import java.awt.Graphics;

6: import javax.swing.JComponent;
7: import javax.swing.JFrame;

8: import javax.swing.SwingUtilities;

10: /**
11: * Class that demonstrates out to avoid hogging the Event Dispatch Thread
12: * when a Heavy Paint Job needs to be performed.

13: *
14: * @author Nuno Sousa
15: * @version v0.1a(C) 2007 Nuno Sousa
16: */

17: public class HeavyPaintJob {

19: public static void main(String[] args) {
20: final JFrame frame = new JFrame("Heavy Paint Job");
21: frame.setSize(800, 600);

22: frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

24: frame.setLayout(new BorderLayout());
25: frame.add(new HeavyPaintComponent(), BorderLayout.CENTER);

27: SwingUtilities.invokeLater(new Runnable() {
28: @Override

29: public void run() {
30: frame.setVisible(true);
31: }
32: });

33: }
34: }

36: class HeavyPaintComponent extends JComponent {

38: private Dimension fSize = null;
39: private int fCurrentRenderingStep = 0;


41: public HeavyPaintComponent() {
42: setOpaque(true);
43: setDoubleBuffered(true);
44: }


46: @Override
47: protected void paintComponent(Graphics g) {

49: // As context I'm using the components Size.
50: if ((fSize == null) || (fSize.width != getWidth()) || (fSize.height != getHeight())) {

51: fSize = getSize();
52: fCurrentRenderingStep = 0;

54: // Rendering initialisation goes here (for instance background filling)
55: super.paintComponent(g);

56: }

58: // Boolean that controls if rendering has ended or not
59: boolean renderingCompleted = false;

61: // Don't hold the Event Dispatch Thread for more than 250ms

62: long time = System.currentTimeMillis();
63: while ((System.currentTimeMillis() - time) <>= getWidth()) {
64: renderingCompleted = true;
65: break;

66: }

68: // Make sure that rendering is resumed at a later time
69: if (!renderingCompleted) {
70: repaint();

71: } else {
72: fSize = null;
73: }
74: }

75: }


The next goal is to make this a component and put it somewhere people can get it and reuse. But at least you get the general idea today and not when I have the time to make a component out of this.

2 comments:

  1. Anonymous12:56

    My greetings from the Netherlands.
    It seems to me you are an expert on Swing. A question:

    I have pages and pages of code where many times JLabel, JTextField, JTextArea, JProgressBar get updated. Unfortunately, it seems that most of it is not running on the EventDispatchThread.

    The question i have is:

    What is the best way to deal with such issues? how can I make sure that GUI-code is running on EDT?

    I have seen fragments of code like this:

    static public void updateSearchString(String _msg)
    {
    if(!SwingUtilities.isEventDispatchThread())
    {
    SwingUtilities.invokeLater(new Runnable(){
    public void run(){
    textfield.setText(_msg);
    }
    });
    }
    else
    {
    textfield.setText(_msg);
    }
    }


    but I cannot imagine a long program to be calling these fragments for every single object that needs to be refreshed. Isnt there a more elegant way to do such dispersed GUI-updates??

    If you find the time to answer, please do so using my email address:

    xanthopoulos@yahoo.com


    cheers
    ioannis

    ReplyDelete
  2. Hello Ioannis.

    As I see it you have two ways to make this more beautiful/better...

    You can create a Runnable and use it directly or in the EDT, with a Utility class. Something like:

    Runnable doInEDT = new Runnable() {
    public void run() {
    // Swing Code
    }
    }
    MySwingUtilities.runInEDT(doInEDT);

    The method of the MySwingUtilities would be something like:


    public void runInEDT(Runnable work) {
    if(!SwingUtilities.isEventDispatchThread()) {
    SwingUtilities.invokeLater(work);
    } else {
    work.run();
    }
    }


    Of course that we will have to ask yourself what is better invokeLater, or invokeAndWait. Maybe even two utility methods.

    The other way is to use the new SwingWorker class. It is available since Java 5 I believe, but you can find it for Java 1.4 on the web.
    The SwingWorker class accepts tasks that have a part that is executed outside the EDT and a part that runs on the EDT. You can even have interim results being pushed to the UI. It is a powerful API, if you use it right because the SwingWorker can use an ExecutorService of Java, from the concurrency utilities. This way you get a single Thread Pool for background tasks.

    I hope this helps guide you in the right path ;-)

    Cheers,
    Nuno

    ReplyDelete