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

Posted

技术标签:

【中文标题】Java:如何在 Swing 中进行双缓冲?【英文标题】:Java: how to do double-buffering in Swing? 【发布时间】:2011-05-24 17:47:03 【问题描述】:

编辑两个

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

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

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

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

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

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

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

我知道当我在 "image 2" 上绘图时,会以某种方式显示 "image 1"。但是,一旦我完成了 "image 2" 的绘图,我如何“快速”将 "image 1" 替换为 "image 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 应用程序,在其自己的(有点小)窗口中运行。

【问题讨论】:

Swing 默认是双缓冲的。这不是 AWT。 仍在等待有人发布我们可以测试的实际代码,看看双缓冲是否值得。 我猜你误解了挥杆。您无法访问(未修改的)离线缓冲区。 Swing 会将缓冲区传递给您的父级,您的父级将对其进行修改。 ... 要做你想做的事,你必须禁用摆动双缓冲,并使用 BufferedImage 来实现。如果你想做真正的“bitblt”,就必须在 AWT 中使用“重量级”组件。 【参考方案1】:

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

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 教程可能看起来很愚蠢,但你看过 BufferStrategy and BufferCapatbilites 吗?

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

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

【讨论】:

@Edwin Buck:+1 寻求帮助,但我不使用 BufferedImage,因为名称会欺骗我:我使用它们是因为需要能够直接在图像中操作像素我知道访问图像底层像素数据缓冲区的唯一方法是使用 BufferedImage。 @SpoonBender 好吧,我认为你需要两个 BufferedImages 有点偏离,实际上你可以只用一个,因为你需要将它单独绘制到每个缓冲区。我更新了我的帖子,详细介绍了如何将 Swing 的双缓冲支持集成到您的示例中。 这可能是比我发布的更好的策略。 Canvas 类也将相同的技术用于多个缓冲区(我认为这是我很久以前在 Java 学生的动画项目中使用的技术)。 @SpoonBender 添加了一些关于设置像素的信息。关键是使用 Raster 对象(概念上包含像素)并通过批量设置来破坏 BufferedImage 的内部 Raster。 @Edwin Buck:非常感谢。关于设置ignoreRepainting的部分以及为什么非常很有趣:)【参考方案2】:

一般我们使用适用于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。

【讨论】:

@Usman Saleem:嗨 Saleem,很高兴看到一个新人在这里提供帮助 :) 使用 Canvas 时,我是否也可以像使用 BufferedImage 一样访问像素的数据缓冲区?跨度> 不应该在 EDT 中发出 repaint 调用吗? 但是没有这个必要。 Swing 也允许动画。那不是双重缓冲。或者,如果在 Swing JPanel 上可以使用相同的代码,因为您所做的只是绘制图像。 @Usman Saleem:您展示的技术是使用一个后台缓冲区进行所有绘图,然后使用 g.drawIamge(...) 进行“blit”对吗? @camickr:使用后备缓冲区,然后像 Usman Saleem 这样的 blit 发布一种双缓冲。它实际上是 Oracle/Sun 关于双缓冲的文章中解释的那个(遗憾的是,这篇文章太短了)。 “dbg”是后台缓冲区,“g”是主缓冲区。也就是两个缓冲,也就是(一种)双缓冲。【参考方案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 方法中的所有操作。

【讨论】:

【参考方案4】:

这是一个变体,其中所有绘图都在 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://***.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;
    

【讨论】:

以上是关于Java:如何在 Swing 中进行双缓冲?的主要内容,如果未能解决你的问题,请参考以下文章

暂时禁用Java图形小程序中的双缓冲

Swing中 paint()与paintComponent()的区别

applet 中的双缓冲是如何工作的?

Java 使用双内存缓冲区 实现简易日志组件

如何双缓冲面板?

Java中用双缓冲技术消除闪烁