Skip to main content

Reducing flicker when painting rubber band rectangle over image

2 replies [Last post]
ljnelson
Offline
Joined: 2003-08-04

I'm new to Java2D (but not to Swing) so be gentle. :-)

Mostly for my own education I'm building an image panel component.

One of its features is that you can drag a semi-transparent over the rendered image (which might be scaled). Eventually I plan to hook this up to some cropping functionality, but right now I'm just working with the drawing calls.

I have this working so that drawing rectangles does indeed do exactly what I want. The problem is that the rectangle flickers.

The approach I've taken is, within my mouse listeners, to erase the prior rectangle before drawing the new one by calling paintImmediately with the rectangle's coordinates. Indeed, this does exactly what I want--that portion of the image that was dirtied by the prior rectangle repaints. But in my paintComponent() method, it looks like I have to draw the entire image, where I'd prefer just to redraw the dirty part. I have to do this because in order to determine what rectangle in the image is actually dirty, I have to scale it again.

I've attached the code for my (functional, working) image panel class below. Could someone kindly investigate the paintComponent() override--particularly the graphics.drawImage() call? Can you think of a more efficient way to do this to reduce the flickering?

Thanks,
Laird

<br />
package bricks.swing.imagepanel;</p>
<p>import java.awt.*; // imports starred to reduce space<br />
import java.awt.event.*;<br />
import javax.swing.*;</p>
<p>public class ImagePanel extends JPanel {</p>
<p>  private Image suppliedImage;<br />
  private Image imageToPaint;<br />
  private final MouseAdapter mouseListener;<br />
  private boolean erasing;<br />
  private int lastX;<br />
  private int lastY;</p>
<p>  public ImagePanel() {<br />
    super();<br />
    this.lastX = -1;<br />
    this.lastY = -1;<br />
    this.mouseListener = new MouseAdapter() {<br />
        private int startX;<br />
        private int startY;<br />
        private boolean dragging;<br />
        private Graphics g;</p>
<p>        @Override<br />
        public final void mousePressed(final MouseEvent event) {<br />
          assert event != null;<br />
          if (!this.dragging) {<br />
            this.dragging = true;<br />
            this.g = ImagePanel.this.getGraphics();<br />
            assert this.g != null;<br />
            this.g.setColor(new Color(0, 0, 255, 30));<br />
            if (ImagePanel.this.lastX >= 0) {<br />
              assert ImagePanel.this.lastY >= 0;<br />
              // That means that someone drew a band on here before, so wipe it<br />
              // out.<br />
              this.drawRubberBand(true); // true == erase<br />
            }<br />
            this.startX = event.getX();<br />
            this.startY = event.getY();<br />
            ImagePanel.this.lastX = this.startX;<br />
            ImagePanel.this.lastY = this.startY;<br />
            this.drawRubberBand(false); // false == draw, not erase<br />
          }<br />
        }</p>
<p>        @Override<br />
        public final void mouseReleased(final MouseEvent event) {<br />
          assert event != null;<br />
          if (this.dragging) {<br />
            this.dragging = false;<br />
            this.drawRubberBand(true); // true == erase<br />
            this.drawRubberBand(false); // false == draw<br />
            assert this.g != null;<br />
            this.g.dispose();<br />
          }<br />
        }</p>
<p>        @Override<br />
        public final void mouseDragged(final MouseEvent event) {<br />
          assert event != null;<br />
          if (this.dragging) {<br />
            this.drawRubberBand(true);<br />
            ImagePanel.this.lastX = event.getX();<br />
            ImagePanel.this.lastY = event.getY();<br />
            this.drawRubberBand(false);<br />
          }<br />
        }</p>
<p>        private final void drawRubberBand(final boolean erase) {<br />
          assert this.g != null;</p>
<p>          final int x;<br />
          final int width;<br />
          if (ImagePanel.this.lastX > this.startX) {<br />
            x = this.startX;<br />
            width = ImagePanel.this.lastX - this.startX;<br />
          } else {<br />
            x = ImagePanel.this.lastX;<br />
            width = this.startX - ImagePanel.this.lastX;<br />
          }</p>
<p>          final int y;<br />
          final int height;<br />
          if (ImagePanel.this.lastY > this.startY) {<br />
            y = this.startY;<br />
            height = ImagePanel.this.lastY - this.startY;<br />
          } else {<br />
            y = ImagePanel.this.lastY;<br />
            height = this.startY - ImagePanel.this.lastY;<br />
          }</p>
<p>          if (erase) {<br />
            ImagePanel.this.erasing = true;<br />
            ImagePanel.this.paintImmediately(x, y, width, height);<br />
            ImagePanel.this.erasing = false;<br />
          } else {<br />
            this.g.fillRect(x, y, width, height);<br />
          }</p>
<p>        }<br />
      };<br />
    this.addMouseListener(this.mouseListener);<br />
    this.addMouseMotionListener(this.mouseListener);<br />
  }</p>
<p>  public Image getImage() {<br />
    return this.suppliedImage;<br />
  }</p>
<p>  public void setImage(final Image image) {<br />
    final Image old = this.getImage();<br />
    this.suppliedImage = image;<br />
    this.imageToPaint = image;<br />
    if (image != null) {<br />
      final int height = image.getHeight(this);<br />
      final int width = image.getWidth(this);<br />
      if (height >= 0 && width >= 0) {<br />
        this.setPreferredSize(new Dimension(height, width));<br />
      }<br />
    }<br />
    this.firePropertyChange("image", old, this.getImage());<br />
  }</p>
<p>  @Override<br />
  protected void paintComponent(final Graphics graphics) {<br />
    super.paintComponent(graphics);<br />
    if (!this.erasing) {<br />
      this.lastX = -1;<br />
      this.lastY = -1;<br />
    }<br />
    if (graphics != null) {<br />
      // Could this be an issue?  The graphics clip is probably set to a tiny fraction of<br />
      // the overall image size.  I'd really like to only drawImage() on that part.  But<br />
      // I could be scaling the image, so I don't really know where that portion of the<br />
      // *scaled* image is.  So I have to tell the graphics to draw the whole image,<br />
      // it, and then apply the clip (which is basically what paintImmediately(Rectangle)<br />
      // is doing when it calls through to here).  Is there a better way?<br />
      graphics.drawImage(this.imageToPaint, 0, 0, this.getWidth(), this.getHeight(), this);<br />
    }<br />
  }</p>
<p>}<br />

Reply viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.
ljnelson
Offline
Joined: 2003-08-04

For other graphics newbies, I hope this is helpful.

The answer is to take advantage of the Swing painting architecture, which I had every intention of doing, but still managed to burn myself with.

Instead of using paintImmediately() to erase a region, have the mouse listener update the start and end points of the crop rectangle, and call repaint(). The Swing repaint mechanism will coalesce these calls where necessary (eliminating some flicker right there) and Swing components are double-buffered by default (which I knew and promptly did not take advantage of for some reason).

The new code (for completeness) is pasted below. BSD-licensed for all you reckless copy and pasters.

Thanks,
Laird

ImagePanel.java (code condensed to save space in this forum):
[code]
package bricks.swing.imagepanel;
import java.awt.*;
import javax.swing.JPanel;
public class ImagePanel extends JPanel {

private Image suppliedImage;
protected Image imageToPaint;

public ImagePanel() {
super();
}
public Image getImage() {
return this.suppliedImage;
}
public void setImage(final Image image) {
final Image old = this.getImage();
this.suppliedImage = image;
this.imageToPaint = image;
if (image != null) {
final int height = image.getHeight(this);
final int width = image.getWidth(this);
if (height >= 0 && width >= 0) {
this.setPreferredSize(new Dimension(height, width));
}
}
this.firePropertyChange("image", old, this.getImage());
}
@Override
protected void paintComponent(final Graphics graphics) {
super.paintComponent(graphics);
this.paintImage(graphics);
}
protected void paintImage(final Graphics graphics) {
if (graphics != null && this.imageToPaint != null) {
graphics.drawImage(this.imageToPaint, 0, 0, this.getWidth(), this.getHeight(), this);
}
}
}
[/code]

ImageCropper.java (incomplete, but the drawing works):
[code]
package bricks.swing.imagepanel;
import java.awt.*;
import java.awt.event.*;

public class ImageCropper extends ImagePanel {

private static final Color RECTANGLE_COLOR = new Color(0, 0, 255, 40);
private final MouseAdapter mouseListener;
private int startX;
private int startY;
private int currentX;
private int currentY;

public ImageCropper() {
super();
this.mouseListener = new MouseAdapter() {
private boolean dragging;

@Override
public final void mousePressed(final MouseEvent event) {
if (event != null && !this.dragging) {
this.dragging = true;
ImageCropper.this.startX = event.getX();
ImageCropper.this.startY = event.getY();
ImageCropper.this.currentX = ImageCropper.this.startX;
ImageCropper.this.currentY = ImageCropper.this.startY;
ImageCropper.this.repaint();
}
}

@Override
public final void mouseDragged(final MouseEvent event) {
if (event != null && this.dragging) {
ImageCropper.this.currentX = event.getX();
ImageCropper.this.currentY = event.getY();
ImageCropper.this.repaint();
}
}

@Override
public final void mouseReleased(final MouseEvent event) {
if (event != null && this.dragging) {
this.dragging = false;
ImageCropper.this.currentX = event.getX();
ImageCropper.this.currentY = event.getY();
ImageCropper.this.repaint();
}
}
};
this.addMouseListener(this.mouseListener);
this.addMouseMotionListener(this.mouseListener);
}

@Override
protected void paintComponent(final Graphics graphics) {
super.paintComponent(graphics);
if (graphics != null &&
this.startX >= 0 &&
this.startX != this.currentX &&
this.startY >= 0 &&
this.startY != this.currentY) {
final int x;
final int width;
if (this.currentX > this.startX) {
x = this.startX;
width = this.currentX - this.startX;
} else if (this.currentX == this.startX) {
width = 0;
return;
} else {
x = this.currentX;
width = this.startX - this.currentX;
}
final int y;
final int height;
if (this.currentY > this.startY) {
y = this.startY;
height = this.currentY - this.startY;
} else {
assert this.currentY < this.startY;
y = this.currentY;
height = this.startY - this.currentY;
}
this.paintRectangle(graphics, x, y, width, height);
}
}

protected void paintRectangle(final Graphics graphics, final int x, final int y, final int width, final int height) {
if (graphics != null) {
final Color oldColor = graphics.getColor();
try {
graphics.setColor(RECTANGLE_COLOR);
graphics.fillRect(x, y, width, height);
} finally {
graphics.setColor(oldColor);
}
}
}

}
[/code]

robross
Offline
Joined: 2003-06-13

Good job on solving your own problem :)

I saw this when you first posted, and meant to respond but forgot. I was going to suggest that next time you post sample code, make sure it's a self-contained running example. I.e., with a main method. That way people like me who are busy can just copy& paste it into our IDE, run it, see what the problem is, and if we can suggest a solution, maybe respond. I don't have time to create a sample application in order to test out your custom component, but if you make it runnable with almost no effort on my part, you increase your chances that you'll get someone to test it out, and maybe solve your problem.