19

编辑两个

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

我花时间编写了一段正在运行的代码(看起来也很有趣),所以我非常感谢实际回答(多么令人震惊;)我的问题并解释这是什么/如何工作而不是单行和刻薄的注释 ;)

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

请注意,我清除屏幕并重新绘制正方形的方式不是最有效的,但这实际上不是这个问题的目的(在某种程度上,为了这个例子,它有点慢更好)。

基本上,我需要不断地修改 BufferedImage 中的很多像素(因为有某种动画),并且我不想看到由于屏幕上的单缓冲而导致的视觉伪影。

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

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

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

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

我是否应该总是在同一个 BufferedImage 中绘制,然后在 JLabel 的 ImageIcon 的 BufferedImage 中对 BufferedImage 的像素进行快速“blit”?(我想不,我不知道如何将其与显示器的“垂直空白行”“同步”[或平面屏幕中的等效项:我的意思是,在不干扰显示器本身刷新它的时刻的情况下进行“同步”像素,以防止剪切])。

“重绘”订单呢?我应该自己触发这些吗?我应该在哪个/什么时候调用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

4 回答 4

14

---- 编辑为每个像素设置的地址 ----

item blow 解决了双缓冲问题,但也有一个关于如何将像素放入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循环方式中的“下一个”屏幕外对象。

图像和 Graphics 上下文在包含场景中不相关,并且您告诉 Swing 您将自己绘制,因此您必须手动绘制图像。这并不总是一件坏事,因为您可以在图像完全绘制时(而不是之前)指定缓冲区翻转。

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

虽然上述代码中的某个地方可能存在失误,但这应该可以让您完成 90% 的工作。祝你好运!

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

将这样的问题提交给 javase 教程似乎很愚蠢,但是您是否研究过BufferStrategyandBufferCapatbilites

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

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

于 2010-12-13T16:22:23.303 回答
3

这是一个变体,其中所有绘图都发生在事件调度线程上。

附录:

基本上,我需要在一个...中不断修改很多像素BufferedImage。</p>

这个动力学模型说明了像素动画的几种方法。

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;
    }
}
于 2010-12-13T16:22:57.387 回答
3

一般我们使用适用于Java动画的Canvas类。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,然后在画布对象上调用“重绘”。同样的技术也应该适用于 JPanel。

于 2010-12-13T15:41:40.123 回答
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 方法中的所有操作。

于 2010-12-13T19:42:42.430 回答