Java:如何在Swing中进行双缓冲?

时间:2010-12-13 15:22:33

标签: java swing doublebuffered

编辑两个

为了防止讽刺评论和单行答案错过了重点: IFF 它就像调用 setDoubleBuffered(true)一样简单,那么我该如何访问当前的离线缓冲区,以便我可以开始搞乱BufferedImage的底层像素数据缓冲区?

我花时间写了一段正在运行的代码(看起来也很有趣)所以我真的很感激答案实际回答(多么令人震惊;)我的问题并解释这是什么/如何工作而不是一个 - 衬里和讽刺的评论;)

这是一段可以在JFrame上反弹正方形的工作代码。我想知道可以用来转换这段代码的各种方法,以便它使用双缓冲。

请注意,我清除屏幕并重新绘制方块的方式并不是最有效的,但这不是这个问题的关键(在某种程度上,为了这个例子,它有点慢)

基本上,我需要不断修改BufferedImage中的大量像素(因为有某种动画),我不希望看到由于屏幕上的单缓冲而产生的视觉伪像。

我有一个JLabel,其Icon是一个包装BufferedImage的ImageIcon。我想修改那个BufferedImage。

必须做什么才能使其成为双重缓冲?

据我所知,当我在“image 2”上画画时,会以某种方式显示“image 1”。但是,一旦我完成了“image 2”的绘制,我如何通过“image 2”“快速”替换“图像1”

这是我应该手动做的事情,比如说,自己交换JLabel的ImageIcon吗?

我是否应该总是使用相同的BufferedImage进行绘制,然后在JLabel的ImageIcon的BufferedImage中快速'blit'显示BufferedImage的像素? (我猜不是,我不知道我怎么能用显示器的“垂直空白线”[或平板屏幕中的等效物]“同步”这个:我的意思是'同步'而不会干扰显示器本身刷新它的时刻像素,以防止剪切])。

“重画”订单怎么样?我想我自己触发这些吗?哪个/什么时候应该调用 repaint()或其他什么?

最重要的要求是我应该直接在图像的像素数据缓冲区中修改像素。

import javax.swing.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;

public class DemosDoubleBuffering extends JFrame {

    private static final int WIDTH  = 600;
    private static final int HEIGHT = 400;

    int xs = 3;
    int ys = xs;

    int x = 0;
    int y = 0;

    final int r = 80;

    final BufferedImage bi1;

    public static void main( final String[] args ) {
        final DemosDoubleBuffering frame = new DemosDoubleBuffering();
        frame.addWindowListener(new WindowAdapter() {
            public void windowClosing( WindowEvent e) {
                System.exit(0);
            }
        });
        frame.setSize( WIDTH, HEIGHT );
        frame.pack();
        frame.setVisible( true );
    }

    public DemosDoubleBuffering() {
        super( "Trying to do double buffering" );
        final JLabel jl = new JLabel();
        bi1 = new BufferedImage( WIDTH, HEIGHT, BufferedImage.TYPE_INT_ARGB );
        final Thread t = new Thread( new Runnable() {
            public void run() {
                while ( true ) {
                    move();
                    drawSquare( bi1 );
                    jl.repaint();
                    try {Thread.sleep(10);} catch (InterruptedException e) {}
                }
            }
        });
        t.start();
        jl.setIcon( new ImageIcon( bi1 ) );
        getContentPane().add( jl );
    }

    private void drawSquare( final BufferedImage bi ) {
        final int[] buf = ((DataBufferInt) bi.getRaster().getDataBuffer()).getData();
        for (int i = 0; i < buf.length; i++) {
            buf[i] = 0xFFFFFFFF;    // clearing all white
        }
        for (int xx = 0; xx < r; xx++) {
            for (int yy = 0; yy < r; yy++) {
                buf[WIDTH*(yy+y)+xx+x] = 0xFF000000;
            }
        }
    }

    private void move() {
        if ( !(x + xs >= 0 && x + xs + r < bi1.getWidth()) ) {
            xs = -xs;
        }
        if ( !(y + ys >= 0 && y + ys + r < bi1.getHeight()) ) {
            ys = -ys;
        }
        x += xs;
        y += ys;
    }

}

修改

对于全屏Java应用程序来说,这是,而是在自己的(有点小的)窗口中运行的常规Java应用程序。

4 个答案:

答案 0 :(得分:13)

----编辑以解决每像素设置----

项目打击解决了双重缓冲问题,但是如何将像素转换为BufferedImage也存在问题。

如果你打电话

WriteableRaster raster = bi.getRaster()
BufferedImage上的

将返回WriteableRaster。从那里你可以使用

int[] pixels = new int[WIDTH*HEIGHT];
// code to set array elements here
raster.setPixel(0, 0, pixels);

请注意,您可能希望优化代码,以便不为每个渲染实际创建新数组。此外,您可能希望优化阵列清除代码以不使用for循环。

Arrays.fill(pixels, 0xFFFFFFFF);

可能会超越你的循环,将背景设置为白色。

----回复后编辑----

关键在于JFrame的原始设置和运行渲染循环内部。

首先,您需要告诉SWING在任何时候停止栅格化;因为,当你完成绘制到想要完全换出的缓冲图像时,你会告诉它。使用JFrame的

执行此操作
setIgnoreRepaint(true);

然后你会想要创建一个缓冲策略。基本上它指定了你想要使用多少个缓冲区

createBufferStrategy(2);

现在您尝试创建缓冲区策略,您需要获取BufferStrategy对象,因为稍后需要它来切换缓冲区。

final BufferStrategy bufferStrategy = getBufferStrategy();

Thread内修改run()循环以包含:

...
  move();
  drawSqure(bi1);
  Graphics g = bufferStrategy.getDrawGraphics();
  g.drawImage(bi1, 0, 0, null);
  g.dispose();
  bufferStrategy.show();
...

从bufferStrategy抓取的图形将是屏幕外Graphics对象,在创建三重缓冲时,它将以循环方式成为“下一个”屏幕外Graphics对象。

图像和图形上下文在包含场景中没有关联,你告诉Swing你自己做绘图,所以你必须手动绘制图像。这并不总是坏事,因为您可以在完全绘制图像时指定缓冲区翻转(而不是之前)。

处理图形对象只是一个好主意,因为它有助于垃圾收集。显示bufferStrategy将翻转缓冲区。

虽然在上面的代码中某处可能存在失误,但这应该可以让你获得90%的失误。祝你好运!

----原帖如下----

将这样的问题引用到javase教程可能看起来很愚蠢,但你有没有看过BufferStrategy and BufferCapatbilites

我认为你遇到的主要问题是你被图像的名字所欺骗。 BufferedImage与双缓冲无关,它与“缓冲内存中的数据(通常来自磁盘)有关。”因此,如果您希望拥有“双缓冲图像”,则需要两个BufferedImages;因为改变正在显示的图像中的像素是不明智的(这可能会导致重新绘制问题)。

在渲染代码中,您可以抓取图形对象。如果您根据上面的教程设置了双缓冲,这意味着您将(默认情况下)抓取屏幕外Graphics对象,并且所有绘图都将在屏幕外。然后,您将图像(当然是正确的)绘制到屏幕外对象。最后,您告诉策略show()缓冲区,它将为您替换Graphics上下文。

答案 1 :(得分:3)

通常我们使用Canvas类,它适用于Java中的动画。 Anyhoo,以下是你如何实现双缓冲:

class CustomCanvas extends Canvas {
  private Image dbImage;
  private Graphics dbg; 
  int x_pos, y_pos;

  public CustomCanvas () {

  }

  public void update (Graphics g) {
    // initialize buffer
    if (dbImage == null) {

      dbImage = createImage (this.getSize().width, this.getSize().height);
      dbg = dbImage.getGraphics ();

    }

    // clear screen in background
    dbg.setColor (getBackground ());
    dbg.fillRect (0, 0, this.getSize().width, this.getSize().height);

    // draw elements in background
    dbg.setColor (getForeground());
    paint (dbg);

    // draw image on the screen
    g.drawImage (dbImage, 0, 0, this); 
  }

        public void paint (Graphics g)
 {

        g.setColor  (Color.red);



        g.fillOval (x_pos - radius, y_pos - radius, 2 * radius, 2 * radius);

    }
}

现在你可以从一个线程更新x_pos和y_pos,然后在canvas对象上调用'repaint'。同样的技术也适用于JPanel。

答案 2 :(得分:3)

这是一种变体,其中所有绘图都在event dispatch thread上进行。

附录:

  

基本上,我需要不断修改BufferedImage ...

中的大量像素

这个kinetic model说明了几种像素动画的方法。

import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics2D;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.*;
import java.awt.image.BufferedImage;

/** @see http://stackoverflow.com/questions/4430356 */
public class DemosDoubleBuffering extends JPanel implements ActionListener {

    private static final int W = 600;
    private static final int H = 400;
    private static final int r = 80;
    private int xs = 3;
    private int ys = xs;
    private int x = 0;
    private int y = 0;
    private final BufferedImage bi;
    private final JLabel jl = new JLabel();
    private final Timer t  = new Timer(10, this);

    public static void main(final String[] args) {
        EventQueue.invokeLater(new Runnable() {

            @Override
            public void run() {
                JFrame frame = new JFrame();
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.add(new DemosDoubleBuffering());
                frame.pack();
                frame.setVisible(true);
            }
        });
    }

    public DemosDoubleBuffering() {
        super(true);
        this.setLayout(new GridLayout());
        this.setPreferredSize(new Dimension(W, H));
        bi = new BufferedImage(W, H, BufferedImage.TYPE_INT_ARGB);
        jl.setIcon(new ImageIcon(bi));
        this.add(jl);
        t.start();
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        move();
        drawSquare(bi);
        jl.repaint();
    }

    private void drawSquare(final BufferedImage bi) {
        Graphics2D g = bi.createGraphics();
        g.setColor(Color.white);
        g.fillRect(0, 0, W, H);
        g.setColor(Color.blue);
        g.fillRect(x, y, r, r);
        g.dispose();
    }

    private void move() {
        if (!(x + xs >= 0 && x + xs + r < bi.getWidth())) {
            xs = -xs;
        }
        if (!(y + ys >= 0 && y + ys + r < bi.getHeight())) {
            ys = -ys;
        }
        x += xs;
        y += ys;
    }
}

答案 3 :(得分:3)

在Swing的窗口模式下,你想要的基本上是不可能的。不支持窗口重绘的光栅同步,这仅适用于全屏模式(即使这样,所有平台也可能不支持)。

默认情况下,Swing组件是双缓冲的,即它们将对中间缓冲区进行所有渲染,然后最终将缓冲区复制到屏幕上,避免从背景清除中闪烁,然后在其上进行绘制。 这就是所有底层平台都能得到合理支持的唯一策略。它避免了重复闪烁,但不会移动图形元素的视觉撕裂。

访问完全在您控制范围内的区域的原始像素的一种相当简单的方法是从JComponent扩展自定义组件并覆盖其paintComponent()方法以从BufferedImage(从内存)绘制区域:

public class PixelBufferComponent extends JComponent {

    private BufferedImage bufferImage;

    public PixelBufferComponent(int width, int height) {
        bufferImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        setPreferredSize(new Dimension(width, height));
    }

    public void paintComponent(Graphics g) {
        g.drawImage(bufferImage, 0, 0, null);
    }

}

然后,无论您想要什么样的方式,您都可以操纵缓冲图像。要使您的更改在屏幕上可见,只需在其上调用repaint()即可。如果从EDT以外的线程进行像素处理,则需要两个缓冲图像来处理实际重绘和操作线程之间的竞争条件。

请注意,与布局管理器一起使用时,此骨架不会绘制组件的整个区域,布局管理器会将组件拉伸超出其首选大小。

另请注意,如果您通过图像上的setRGB(...)进行真正的低级像素操作,或者直接直接访问基础DataBuffer,则缓冲图像方法通常才有意义。如果你可以使用Graphics2D的方法进行所有操作,你可以使用提供的图形(实际上是Graphics2D并且可以简单地投射)在paintComponent方法中完成所有操作。