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
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
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();
}
});
}
}