Java:如何在 Swing 中进行双缓冲?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/4430356/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Java: how to do double-buffering in Swing?
提问by SyntaxT3rr0r
EDIT TWO
编辑两个
To prevent snarky comments and one-line answers missing the point: IFFit is as simple as calling setDoubleBuffered(true), then how do I get access to the current offline buffer so that I can start messing with the BufferedImage's underlying pixel databuffer?
为了防止尖刻的评论和单行的答案忽略了这一点:IFF就像调用setDoubleBuffered(true)一样简单,那么我如何访问当前的离线缓冲区,以便我可以开始弄乱 BufferedImage 的底层像素数据缓冲区?
I took the time to write a running piece of code (which looks kinda fun too) so I'd really appreciate answers actually answering (what a shock ;) my question and explaining what/how this is working instead of one-liners and snarky comments ;)
我花时间编写了一段正在运行的代码(这看起来也很有趣)所以我真的很感谢实际回答我的问题的答案(多么令人震惊;)并解释这是什么/如何工作而不是单行和尖刻注释 ;)
Here's a working piece of code that bounces a square across a JFrame. I'd like to know about the various ways that can be used to transform this piece of code so that it uses double-buffering.
这是一段在 JFrame 上弹跳正方形的工作代码。我想知道可用于转换这段代码以使其使用双缓冲的各种方法。
Note that the way I clear the screen and redraw the square ain't the most efficient but this is really not what this question is about (in a way, it's better for the sake of this example that it is somewhat slow).
请注意,我清除屏幕和重绘正方形的方式并不是最有效的,但这实际上不是这个问题的内容(在某种程度上,对于这个例子来说,它有点慢更好)。
Basically, I need to constantly modify a lot pixels in a BufferedImage (as to have some kind of animation) and I don't want to see the visual artifacts due to single-buffering on screen.
基本上,我需要不断修改 BufferedImage 中的很多像素(为了有某种动画),我不想看到由于屏幕上的单缓冲而导致的视觉伪影。
I've got a JLabel whose Icon is an ImageIcon wrapping a BufferedImage. I want to modify that BufferedImage.
我有一个 JLabel,它的图标是一个包装 BufferedImage 的 ImageIcon。我想修改那个 BufferedImage。
What has to be done so that this becomes double-buffered?
必须做什么才能使其成为双缓冲?
I understand that somehow "image 1"will be shown while I'll be drawing on "image 2". But then once I'm done drawing on "image 2", how do I "quickly" replace "image 1"by "image 2"?
我知道在我将在"image 2"上绘图时会以某种方式显示"image 1 "。但是,一旦我完成了“图像 2”的绘制,我如何“快速”将“图像 1”替换为“图像 2”?
Is this something I should be doing manually, like, say, by swapping the JLabel's ImageIcon myself?
这是我应该手动做的事情吗,比如说,我自己交换 JLabel 的 ImageIcon?
Should I be always drawing in the same BufferedImage then do a fast 'blit' of that BufferedImage's pixels in the JLabel's ImageIcon's BufferedImage? (I guess no and I don't see how I could "synch" this with the monitor's "vertical blank line" [or equivalent in flat-screen: I mean, to 'synch' without interfering with the moment the monitor itselfs refreshes its pixels, as to prevent shearing]).
我是否应该总是在同一个 BufferedImage 中绘制,然后在 JLabel 的 ImageIcon 的 BufferedImage 中对该 BufferedImage 的像素进行快速“blit”?(我想不,我不明白如何将它与显示器的“垂直空白行”“同步”[或平板屏幕中的等价物:我的意思是,“同步”而不干扰显示器本身刷新它的那一刻像素,以防止剪切])。
What about the "repaint" orders? Am I suppose to trigger these myself? Which/when exactly should I call repaint()or something else?
“重绘”命令呢?我应该自己触发这些吗?我应该在哪个/什么时候调用repaint()或其他什么?
The most important requirement is that I should be modifying pixels directly in the images's pixel 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;
}
}
EDIT
编辑
This is notfor a full-screen Java application, but a regular Java application, running in its own (somewhat small) window.
这不是针对全屏 Java 应用程序,而是针对在其自己的(有点小)窗口中运行的常规 Java 应用程序。
回答by Usman Saleem
Generally we use Canvas class which is suitable for animation in Java. Anyhoo, following is how you achieve double buffering:
通常我们使用 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);
}
}
Now you can update the x_pos and y_pos from a thread, followed by the 'repaint' call on the canvas object. The same technique should work on a JPanel as well.
现在,您可以从线程更新 x_pos 和 y_pos,然后在画布对象上调用“重新绘制”。同样的技术也适用于 JPanel。
回答by Edwin Buck
---- Edited to address per pixel setting ----
---- 编辑地址每像素设置----
The item blow addresses double buffering, but there's also an issue on how to get pixels into a BufferedImage
.
项目打击解决了双缓冲,但还有一个问题是如何将像素放入BufferedImage
.
If you call
如果你打电话
WriteableRaster raster = bi.getRaster()
on the BufferedImage
it will return a WriteableRaster
. From there you can use
在BufferedImage
它会返回一个WriteableRaster
. 从那里你可以使用
int[] pixels = new int[WIDTH*HEIGHT];
// code to set array elements here
raster.setPixel(0, 0, pixels);
Note that you would probably want to optimize the code to not actually create a new array for each rendering. In addition, you would probably want to optimized the array clearing code to not use a for loop.
请注意,您可能希望优化代码,而不是为每个渲染实际创建一个新数组。此外,您可能希望优化数组清除代码以不使用 for 循环。
Arrays.fill(pixels, 0xFFFFFFFF);
would probably outperform your loop setting the background to white.
可能会胜过将背景设置为白色的循环。
---- Edited after response ----
---- 回复后编辑----
The key is in your original setup of the JFrame and inside the run rendering loop.
关键在于 JFrame 的原始设置和运行渲染循环内。
First you need to tell SWING to stop Rasterizing whenever it wants to; because, you'll be telling it when you're done drawing to the buffered image you want to swap out in full. Do this with JFrame's
首先,您需要告诉 SWING 在需要时停止光栅化;因为,当您完成绘制到要完全换出的缓冲图像时,您会告诉它。用 JFrame 做这个
setIgnoreRepaint(true);
Then you'll want to create a buffer strategy. Basically it specifies how many buffers you want to use
然后您需要创建一个缓冲策略。基本上它指定了您要使用的缓冲区数量
createBufferStrategy(2);
Now that you tried to create the buffer strategy, you need to grab the BufferStrategy
object as you will need it later to switch buffers.
现在您已尝试创建缓冲区策略,您需要抓取BufferStrategy
对象,因为稍后您将需要它来切换缓冲区。
final BufferStrategy bufferStrategy = getBufferStrategy();
Inside your Thread
modify the run()
loop to contain:
在您Thread
修改run()
循环以包含:
...
move();
drawSqure(bi1);
Graphics g = bufferStrategy.getDrawGraphics();
g.drawImage(bi1, 0, 0, null);
g.dispose();
bufferStrategy.show();
...
The graphics grabbed from the bufferStrategy will be the off-screen Graphics
object, when creating triple buffering, it will be the "next" off-screen Graphics
object in a round-robin fashion.
从 bufferStrategy 抓取的图形将是离屏Graphics
对象,当创建三重缓冲时,它将以Graphics
循环方式成为“下一个”离屏对象。
The image and the Graphics context are not related in a containment scenario, and you told Swing you'd do the drawing yourself, so you have to draw the image manually. This is not always a bad thing, as you can specify the buffer flipping when the image is fully drawn (and not before).
图像和 Graphics 上下文在包含场景中不相关,并且您告诉 Swing 您将自己绘制,因此您必须手动绘制图像。这并不总是一件坏事,因为您可以在图像完全绘制时(而不是之前)指定缓冲区翻转。
Disposing of the graphics object is just a good idea as it helps in garbage collection. Showing the bufferStrategy
will flip buffers.
处理图形对象只是一个好主意,因为它有助于垃圾收集。显示bufferStrategy
将翻转缓冲区。
While there might have been a misstep somewhere in the above code, this should get you 90% of the way there. Good luck!
虽然上面代码中的某个地方可能有错误,但这应该可以帮助您完成 90% 的工作。祝你好运!
---- Original post follows ----
---- 原帖如下----
It might seem silly to refer such a question to a javase tutorial, but have you looked into BufferStrategy
and BufferCapatbilites
?
将这样的问题参考 javase 教程似乎很愚蠢,但是您是否研究过BufferStrategy
和BufferCapatbilites
?
The main issue I think you are encountering is that you are fooled by the name of the Image. A BufferedImage
has nothing to do with double buffering, it has to do with "buffering the data (typically from disk) in memory." As such, you will need two BufferedImages if you wish to have a "double buffered image"; as it is unwise to alter pixels in image which is being shown (it might cause repainting issues).
我认为您遇到的主要问题是您被图像的名称所迷惑。ABufferedImage
与双缓冲无关,它与“在内存中缓冲数据(通常来自磁盘)”有关。因此,如果您希望拥有“双缓冲图像”,则需要两个 BufferedImages;因为更改正在显示的图像中的像素是不明智的(这可能会导致重新绘制问题)。
In your rendering code, you grab the graphics object. If you set up double buffering according to the tutorial above, this means you will grab (by default) the off-screen Graphics
object, and all drawing will be off-screen. Then you draw your image (the right one of course) to the off-screen object. Finally, you tell the strategy to show()
the buffer, and it will do the replacement of the Graphics context for you.
在您的渲染代码中,您获取图形对象。如果你根据上面的教程设置了双缓冲,这意味着你将抓取(默认)离屏Graphics
对象,并且所有绘图都将离屏。然后将图像(当然是正确的图像)绘制到屏幕外对象上。最后,您将策略告知show()
缓冲区,它会为您替换 Graphics 上下文。
回答by trashgod
Here's a variation in which all drawing takes place on the event dispatch thread.
这是一个变体,其中所有绘制都发生在事件调度线程上。
Addendum:
附录:
Basically, I need to constantly modify a lot pixels in a
BufferedImage
…
基本上,我需要在一个
BufferedImage
…
This kinetic modelillustrates several approaches to pixel 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;
}
}
回答by Durandal
What you want is basically impossible in windowed mode with Swing. There is no support for raster synchronization for window repaints, this is only available in fullscreen mode (and even then may not be supported by all platforms).
在使用 Swing 的窗口模式下,您想要的基本上是不可能的。不支持窗口重绘的光栅同步,这仅在全屏模式下可用(即使这样也可能不被所有平台支持)。
Swing components are double-buffered by default, that is they will do all the rendering to an intermediate buffer and that buffer is then finally copied to the screen, avoiding flicker from background clearing and then painting on top of it. And thats the only strategy that is reasonable well supported on all underlying platforms. It avoids only repaint flickering, but not visual tearing from moving graphic elements.
默认情况下,Swing 组件是双缓冲的,也就是说,它们将所有渲染工作到一个中间缓冲区,然后该缓冲区最终被复制到屏幕上,避免背景清除引起的闪烁,然后在其上绘画。这是唯一在所有底层平台上都得到合理支持的策略。它仅避免了重新绘制闪烁,而避免了移动图形元素的视觉撕裂。
A reasonably simple way of having access to the raw pixels of an area fully under you control would be to extend a custom component from JComponent and overwrite its paintComponent()-method to paint the area from a BufferedImage (from memory):
访问完全由您控制的区域的原始像素的一种相当简单的方法是从 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);
}
}
You can then manipulate you buffered image whichever way you desire. To get your changes made visible on screen, simply call repaint() on it. If you do the pixel manipulation from a thread other than the EDT, you need TWO buffered images to cope with race conditions between the actual repaint and your manipulation thread.
然后,您可以以任何您想要的方式操作缓冲图像。要使您的更改在屏幕上可见,只需在其上调用 repaint() 即可。如果您从 EDT 以外的线程执行像素操作,则需要两个缓冲图像来应对实际重绘和操作线程之间的竞争条件。
Note that this skeleton will not paint the entire area of the component when used with a layout manager that stretches the component beyond its preferred size.
请注意,当与将组件拉伸到超出其首选大小的布局管理器一起使用时,此骨架不会绘制组件的整个区域。
Note also, the buffered image approach mostly only makes sense if you do real low level pixel manipulation via setRGB(...) on the image or if you directly access the underlying DataBuffer directly. If you can do all the manipulations using Graphics2D's methods, you could do all the stuff in the paintComponent method using the provided graphics (which is actually a Graphics2D and can be simply casted).
另请注意,缓冲图像方法通常仅在您通过图像上的 setRGB(...) 进行真正的低级像素操作或直接访问底层 DataBuffer 时才有意义。如果您可以使用 Graphics2D 的方法进行所有操作,则可以使用提供的图形(实际上是 Graphics2D 并且可以简单地进行转换)来完成paintComponent 方法中的所有操作。