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.