Search code examples
javaswinggraphicsgraphics2d

Incremental graphics in Swing


I'm trying to get the hang of doing graphics stuff (drawing lines, etc.) in Swing. So far, all the tutorials I've seen declare a class that overrides paintComponent, and all the paintComponent methods do some set, specific thing, like drawing a red square (although maybe they draw it at a different location every time). Or maybe they draw a number of lines and shapes, but the paintComponent method does everything all at once.

I'm trying to figure out: suppose I want to draw one thing in a component, and later on draw something else on top of it without erasing what I drew before. My first thought was to have my paintComponent override call a callback.

import java.awt.*;
import javax.swing.*;
public class DrawTest {

    private interface GraphicsAction {
        public void action (Graphics g);
    }

    private static class TestPanel extends JPanel {

        private GraphicsAction paintAction;

        public void draw (GraphicsAction action) {
            paintAction = action;
            repaint();
        }

        @Override
        public void paintComponent (Graphics g) {
            super.paintComponent (g);
            if (paintAction != null)
                paintAction.action(g);
        }
    }

    private static void createAndShowGui() {
        JFrame frame = new JFrame ("DrawTest");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setPreferredSize(new Dimension(500,500));

        TestPanel p = new TestPanel ();
        frame.getContentPane().add(p);
        frame.pack();
        frame.setVisible(true);
        p.repaint();

        p.draw (new GraphicsAction () {
            public void action (Graphics g) {
                g.setColor(Color.RED);
                g.drawLine(5, 30, 100, 50);
            }
        });

        // in real life, we would do some other stuff and then
        // later something would want to add a blue line to the
        // diagram 

        p.draw (new GraphicsAction () {
            public void action (Graphics g) {
                g.setColor(Color.BLUE);
                g.drawLine(5, 30, 150, 40);
            }
        });

    }

    public static void main (String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                createAndShowGui();                 
            }
        });
    }
}

This doesn't work. A blue line shows up, but no red line. I'm guessing this is because the repaint() in draw causes everything to start over when I draw the blue line, but I'm not sure; anyway, I don't know how else to get paintComponent to be called.

Also, if I put a Thread.sleep(1000) between the two p.draw calls, I don't even see the red line for a second. So I'm not at all clear on how to get my graphics to show up when I want them to.

I've done some searching on "incremental graphics" in Swing, but nothing that helps find a solution. I found an Oracle article "Painting in AWT and Swing" that discusses overriding an update() method to accomplish incremental graphics, but I haven't found any actual examples of this being done.

So how would I get this to do what I want? It seems like a common enough task that there should be a simple way to do it, but I haven't found one. I'm assuming it should be doable without calling getGraphics, which, based on other StackOverflow responses would be, at best, kind of gauche.


Solution

  • Painting in Swing is destructive. That is, whenever a new paint cycle runs, you are expected to completely rebuild the output as per the state of the object you are painting.

    Take a look at Painting in AWT and Swing

    So when you call

    p.draw (new GraphicsAction () {
        public void action (Graphics g) {
            g.setColor(Color.RED);
            g.drawLine(5, 30, 100, 50);
        }
    });
    

    Followed by

    p.draw (new GraphicsAction () {
        public void action (Graphics g) {
            g.setColor(Color.BLUE);
            g.drawLine(5, 30, 150, 40);
        }
    });
    

    You are basically throwing away the first action. Ignoring how repaints are scheduled for the moment. The first request says, "paint a red line", the second says, "paint a blue line", but before these actions are executed, the Graphics context is cleaned, preparing it for updating.

    This is very important, as the Graphics context you are provided is a shared resource. All the components painted before have used the same context, all the components painted after you will use the same context. This means, if you don't "clean" the context before painting to it, you can end up with unwanted paint artifacts.

    But how can you get around it??

    There are a few choices here.

    You could draw to a backing buffer (or BufferedImage) which has it's own Graphics context, which you can add to and would only need to "paint" in your paintComponent method.

    This would mean, each time you call p.draw(...), you would actually paint to this buffer first then call repaint.

    The problem with this, is you need to maintain the size of the buffer. Each time the component size changes, you would need to copy this buffer to a new buffer based on the new size of the component. This is a little messy, but is doable.

    The other solution would be to place each action in a List and when required, simply loop through the List and re-apply the action whenever required.

    This is probably the simplest approach, but as the number of actions grow, could reduce the effectiveness of the paint process, slowing the paint process.

    You could also use a combination of the two. Generate a buffer when it doesn't exists, loop through the List of actions and renderer them to the buffer and simply paint the buffer in the paintComponent method. Whenever the component is resized, simply null the buffer and allow the paintComponent to regenerate it...for example...

    Also, if I put a Thread.sleep(1000) between the two p.draw calls

    Swing is a single threaded framework. That means that all updates and modifications are expected to done within the context of the Event Dispatching Thread.

    Equally, anything that blocks the EDT from running will prevent it from process (amongst other things) paint requests.

    This means that when you sleep between the p.draw calls, you are stopping the EDT from running, meaning it can't process your paint requests...

    Take a look at Concurrency in Swing for more details

    Updated with example

    enter image description here

    I just want to point out that is really inefficient. Re-creating the buffer each time invalidate is called will create a large number of short lived objects and could a significant drain on performance.

    Normally, I would use a javax.swing.Timer, set to be non-repeating, that would be restarted each time invalidate was called. This would be set to a short delay (somewhere between 125-250 milliseconds). When the timer is triggered, I would simply re-construct the buffer at this time, but this is just an example ;)

    import java.awt.Color;
    import java.awt.Dimension;
    import java.awt.Graphics;
    import java.awt.Graphics2D;
    import java.awt.image.BufferedImage;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.SwingUtilities;
    
    public class DrawTest {
    
        private interface GraphicsAction {
    
            public void action(Graphics g);
        }
    
        private static class TestPanel extends JPanel {
    
            private GraphicsAction paintAction;
            private BufferedImage buffer;
    
            @Override
            public void invalidate() {
                BufferedImage img = new BufferedImage(
                        Math.max(1, getWidth()),
                        Math.max(1, getHeight()), BufferedImage.TYPE_INT_ARGB);
                Graphics2D g2d = img.createGraphics();
                g2d.setColor(getBackground());
                g2d.fillRect(0, 0, getWidth(), getHeight());
                if (buffer != null) {
                    g2d.drawImage(buffer, 0, 0, this);
                }
                g2d.dispose();
                buffer = img;
                super.invalidate();
            }
    
            protected BufferedImage getBuffer() {
                if (buffer == null) {
                    buffer = new BufferedImage(
                            Math.max(1, getWidth()),
                            Math.max(1, getHeight()), BufferedImage.TYPE_INT_ARGB);
                    Graphics2D g2d = buffer.createGraphics();
                    g2d.setColor(getBackground());
                    g2d.fillRect(0, 0, getWidth(), getHeight());
                    g2d.dispose();
                }
                return buffer;
            }
    
            public void draw(GraphicsAction action) {
                BufferedImage buffer = getBuffer();
                Graphics2D g2d = buffer.createGraphics();
                action.action(g2d);
                g2d.dispose();
                repaint();
            }
    
            @Override
            public void paintComponent(Graphics g) {
                super.paintComponent(g);
                g.drawImage(getBuffer(), 0, 0, this);
            }
        }
    
        private static void createAndShowGui() {
            JFrame frame = new JFrame("DrawTest");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setPreferredSize(new Dimension(500, 500));
    
            TestPanel p = new TestPanel();
            frame.getContentPane().add(p);
            frame.pack();
            frame.setVisible(true);
            p.repaint();
    
            p.draw(new GraphicsAction() {
                public void action(Graphics g) {
                    g.setColor(Color.RED);
                    g.drawLine(5, 30, 100, 50);
                }
            });
    
            // in real life, we would do some other stuff and then
            // later something would want to add a blue line to the
            // diagram 
            p.draw(new GraphicsAction() {
                public void action(Graphics g) {
                    g.setColor(Color.BLUE);
                    g.drawLine(5, 30, 150, 40);
                }
            });
    
        }
    
        public static void main(String[] args) {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    createAndShowGui();
                }
            });
        }
    }