3

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.

ajb
  • 31,309
  • 3
  • 58
  • 84

1 Answers1

6

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();
            }
        });
    }
}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • OK, I'll take another look at the "Painting" article; I already looked at part of it, but I'll try to look at it more carefully. Bummer that they botched the formatting of their code examples and have a link to an `UpdateDemo` demo that doesn't exist. Are there any links to sample code that uses a backing buffer or `BufferedImage` with its own `Graphics` context (I presume it wouldn't be shared by any other components if I don't let it); and do I need to create the `Graphics` instance myself to get this to work? – ajb Sep 17 '13 at 02:05
  • I added a simple example, based on direct drawing the action to a backing buffer... – MadProgrammer Sep 17 '13 at 02:20
  • Thank you very much for the example. I'll need to study it further. How often would invalidate be called? From the javadoc for `java.AWT.Component`, it would be called whenever layout-related info changes; but if you don't expect the window to be resized, it seems like it would be called hardly at all, and that might be good enough for my purposes. Or should I expect that it might be called more often for other reasons, even if the component containing the graphics is pretty much at a fixed location? – ajb Sep 17 '13 at 05:12
  • The component being resized is one of the most likely cause of `invaldiate`. It may also be called if Swing thinks it needs to re-laid out... – MadProgrammer Sep 17 '13 at 05:14
  • @MadProgrammer Thanks again, I've gotten this to work (also fixed the event-thread problem) and am going to have fun playing with graphics some more. – ajb Sep 17 '13 at 17:57