Java:如何在Swing中实现双缓冲?
编辑两个
为了防止snarky注释和单行答案错过了这一点: IFF就像调用setDoubleBuffered(true)一样简单,那么如何访问当前的离线缓冲区,以便我可以开始搞乱BufferedImage的底层像素databuffer?
我花时间写了一段代码(看起来还挺有趣的),所以我真的很感激答案,实际回答(这是多么令人震惊),我的问题和解释什么/这是如何工作,而不是单线和snarky注释 ;)
这里有一段代码在JFrame上反弹一个正方形。 我想知道可以用来转换这段代码的各种方式,以便它使用双缓冲。
请注意,我清除屏幕并重新绘制正方形的方式并不是最有效的方法,但实际上并不是这个问题的关键所在(从某种意义上说,这个例子有点慢)。
基本上,我需要不断地修改BufferedImage中的很多像素(因为有某种animation),而且我不想看到由于在屏幕上单缓冲而造成的视觉伪影。
我有一个JLabel,它的Icon是一个包装了BufferedImage的ImageIcon。 我想修改那个BufferedImage。
必须做些什么才能变成双缓冲?
我明白,不知何故“图像1”将显示,而我将在“图像2”上绘制。 但是一旦我完成了“图像2”的绘制,我该如何“快速”将“图像1”replace为“图像2” ?
这是我应该手动做的,就像通过自己交换JLabel的ImageIcon一样?
我应该总是在同一个BufferedImage中绘图,然后在JLabel的ImageIcon的BufferedImage中做一个BufferedImage像素的快速“blit”? (我猜不是,我不知道怎样才能使它与显示器的“垂直空白线”同步[或者与平板显示器相当:我的意思是,在不干扰显示器本身刷新的情况下“同步”像素,以防止剪切])。
那么“重绘”命令呢? 我想自己触发这些吗? 哪个/什么时候我应该打电话repaint()或别的什么?
最重要的要求是我应该直接在图像的像素databuffer中修改像素。
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应用程序,运行在它自己的(有点小的)窗口中。
—-编辑处理每个像素设置—-
该项目打击地址双缓冲,但也有一个关于如何获得像素到一个BufferedImage
。
如果你打电话
WriteableRaster raster = bi.getRaster()
在BufferedImage
上它将返回一个WriteableRaster
。 从那里你可以使用
int[] pixels = new int[WIDTH*HEIGHT]; // code to set array elements here raster.setPixel(0, 0, pixels);
请注意,您可能需要优化代码,而不是为每个渲染实际创build一个新的数组。 另外,你可能想优化数组清除代码,不使用for循环。
Arrays.fill(pixels, 0xFFFFFFFF);
可能会超越你的循环设置背景为白色。
—-响应后编辑—-
关键在于JFrame的原始设置和运行渲染循环中。
首先,你需要告诉SWING在任何时候停止Rasterizing; 因为当你完成绘制到你想要换出的缓冲图像时,你会告诉它。 用JFrame做到这一点
setIgnoreRepaint(true);
那么你会想创build一个缓冲策略。 基本上它指定了你想要使用多less个缓冲区
createBufferStrategy(2);
现在您尝试创build缓冲区策略,您需要获取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
对象,创build三重缓冲时,它将以循环方式成为“下一个”离屏Graphics
对象。
图像和Graphics上下文在遏制场景中不相关,并且您告诉Swing您将自己执行绘图,因此您必须手动绘制图像。 这并不总是一件坏事,因为当图像被完全绘制时(而不是在之前),您可以指定缓冲区翻转。
处理graphics对象只是一个好主意,因为它有助于垃圾收集。 显示bufferStrategy
将翻转缓冲区。
在上面的代码中可能会有一个错误的地方,这应该会让你有90%的select。 祝你好运!
—-原文如下—-
将这样的问题提交给javase教程可能看起来很愚蠢,但是你是否看过BufferStrategy
和BufferCapatbilites
?
我认为你遇到的主要问题是你被图像的名称所愚弄。 BufferedImage
与双缓冲没有任何关系,它与缓冲内存中的数据(通常是磁盘)有关。 因此,如果你想有一个“双缓冲图像”,你将需要两个BufferedImages; 因为改变正在显示的图像中的像素是不明智的(这可能导致重新绘制问题)。
在您的渲染代码中,您可以抓取graphics对象。 如果按照上面的教程设置了双缓冲,这意味着您将抓取(默认情况下)离屏Graphics
对象,并且所有graphics都将在屏幕外。 然后,您将您的图像(当然是正确的)绘制到屏幕外的对象上。 最后,你告诉show()
缓冲区的策略,它将为你replaceGraphics上下文。
一般我们使用适合Javaanimation的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,然后在canvas对象上进行“repaint”调用。 同样的技术也应该在JPanel上工作。
Swing在窗口模式下基本上不可能实现。 不支持窗口重绘的光栅同步,这只能在全屏模式下使用(甚至可能不被所有平台支持)。
Swing组件默认是双缓冲的,也就是说它们会把所有的渲染都渲染到一个中间缓冲区,然后这个缓冲区最终被复制到屏幕上,从而避免闪烁从背景清除,然后在其上绘画。 这就是在所有底层平台上合理支持的唯一策略。 它避免了只重画闪烁,而不是从移动graphics元素的视觉撕裂。
在您控制下完全访问区域的原始像素的相当简单的方法是从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以外的线程进行像素操作,则需要两个缓冲图像来处理实际重绘和操作线程之间的竞争条件。
请注意,当与布局pipe理器一起使用时,此构架不会绘制组件的整个区域,而该布局pipe理器会将该组件拉伸超出其首选大小。
另外请注意,caching图像的方法大多只有在图像上通过setRGB(…)进行真正的低级像素操作时才有意义,或者直接直接访问底层的DataBuffer。 如果你可以使用Graphics2D的方法来完成所有的操作,你可以使用提供的graphics(它实际上是一个Graphics2D,可以简单地被转换)在paintComponent方法中完成所有的操作。
以下是所有绘图都在事件派发线程上进行的变体。
附录:
基本上,我需要不断修改
BufferedImage
的很多像素…
这个动力学模型说明了像素animation的几种方法。
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; } }