2010-12-13 19 views
17

EDICION DE DOSJava: ¿cómo hacer doble buffer en Swing?

Para evitar comentarios sarcásticos y una línea responde no viene al caso: IFF es tan simple como llamar setDoubleBuffered (verdadero), entonces, ¿cómo puedo obtener acceso a la memoria intermedia de línea actual para que pueda comenzar a jugar con el buffer de datos de píxeles subyacente de BufferedImage?

Me tomé el tiempo para escribir un fragmento de código (lo que también me pareció divertido) así que realmente agradecería las respuestas (¡qué sorpresa!) Y explicando qué y cómo funciona en lugar de uno -liners y comentarios snarky;)

Aquí hay un código de trabajo que rebota un cuadrado en un JFrame. Me gustaría saber sobre las diversas formas en que se puede utilizar para transformar este fragmento de código para que use doble almacenamiento en búfer.

Tenga en cuenta que la forma en que borro la pantalla y vuelvo a dibujar el cuadrado no es la más eficiente, pero de esto no se trata en realidad (en cierto modo, es mejor por el ejemplo que es algo lento)

Básicamente, tengo que modificar constantemente muchos píxeles en una Imagen Buffered (para tener algún tipo de animación) y no quiero ver los artefactos visuales debido a la función de almacenamiento en memoria única en la pantalla.

Tengo un JLabel cuyo Icono es un ImageIcon que envuelve una Imagen Buffered. Quiero modificar esa Imagen Buffered.

¿Qué se debe hacer para que esto se convierta en un doble buffer?

entiendo que de alguna manera "Imagen 1" se mostrará mientras yo vaya a dibujar en "imagen 2". Pero una vez que termine de dibujar en "imagen 2", ¿cómo puedo "reemplazar" rápidamente "imagen 1" por "imagen 2"?

¿Esto es algo que debería hacer manualmente, como, por ejemplo, intercambiando el ImageIcon de JLabel por mi cuenta?

¿Debo estar siempre dibujando en la misma Imagen Buffered y luego hacer una rápida 'blit' de los píxeles de esa Imagen Buffered en la Imagen BufferedImage de JLabel? (Supongo que no y no veo cómo podría "sincronizar" esto con la "línea en blanco vertical" del monitor [o equivalente en pantalla plana: me refiero a "sincronizar" sin interferir con el momento en que el monitor refresca su píxeles, como para evitar el cizallamiento]).

¿Qué pasa con los pedidos de "repintado"? ¿Debo desencadenar esto yo mismo? ¿Cuál/cuándo exactamente debería llamar repintado() o algo más?

El requisito más importante es que debería modificar los píxeles directamente en el buffer de datos de píxeles de las imágenes.

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; 
    } 

} 

EDITAR

Ésta es no para una aplicación Java de pantalla completa, sino una aplicación regular de Java, se ejecuta en su propia ventana (un poco pequeña).

+2

Swing tiene doble buffer por defecto. Esto no es AWT. – camickr

+0

Todavía estamos esperando que alguien publique el código real que podemos probar para ver si el doble buffer vale la pena el esfuerzo. – camickr

+1

Supongo que has entendido mal el swing. No puede acceder al búfer fuera de línea (sin modificar). Swing pasará el búfer a su padre, y su padre lo modificará. ... Para hacer lo que quiera, tiene que * desactivar * el doble buffer oscilante y hacerlo con, digamos, BufferedImage. Si quiere hacer un "bitblt" real, debe usar el componente "heavyweight" en AWT. –

Respuesta

3

Generalmente utilizamos la clase Canvas, que es adecuada para la animación en Java. Anyhoo, que sigue es cómo lograr el doble buffer:

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); 

    } 
} 

Ahora puedes actualizar los x_pos y y_pos de un hilo, seguido por el 'pintar' llaman en el objeto de la lona. La misma técnica debería funcionar en un JPanel también.

+0

@Usman Saleem: Hola, Saleem, es genial ver a un recién llegado ayudando aquí :) Cuando trabajo con un Canvas, ¿también puedo acceder al buffer de datos de los píxeles de la misma manera que hago con un BufferedImage? – SyntaxT3rr0r

+0

¿No debería emitirse la llamada de repintado en el EDT? – Riduidel

+1

Pero no es necesario para esto. Swing también permite la animación. Eso no es doble buffer. O si se trata del mismo código que se puede usar en un Swing JPanel, ya que todo lo que hace es dibujar una imagen. – camickr

11

---- Editado para abordar por ajuste de píxeles ----

El golpe artículo aborda el doble buffer, pero también hay un problema sobre cómo conseguir píxeles en una BufferedImage.

Si llama

WriteableRaster raster = bi.getRaster() 

en el BufferedImage devolverá un WriteableRaster. Desde allí se puede utilizar

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

Tenga en cuenta que es probable que desee para optimizar el código en realidad no crear una nueva matriz para cada prestación. Además, probablemente desee optimizar el código de borrado de la matriz para no usar un bucle for.

Arrays.fill(pixels, 0xFFFFFFFF); 

probablemente supere su bucle estableciendo el fondo en blanco.

---- ---- Editado después de la respuesta

La clave está en su configuración original del JFrame y dentro del bucle de ejecución de representación.

Primero debes decirle a SWING que deje de rastrillar cuando quiera; porque lo dirá cuando termine de dibujar en la imagen almacenada en el búfer que desea intercambiar en su totalidad. Haga esto con JFrame's

setIgnoreRepaint(true); 

Luego querrá crear una estrategia de almacenamiento intermedio. Básicamente se especifica el número de búferes que desea utilizar

createBufferStrategy(2); 

Ahora que ha intentado crear la estrategia de memoria intermedia, es necesario agarrar el objeto BufferStrategy, ya que lo necesitará más adelante para cambiar de búfer.

final BufferStrategy bufferStrategy = getBufferStrategy(); 

Dentro de su Thread modificar el bucle run() contener:

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

Los gráficos agarraron de la bufferStrategy será objeto fuera de la pantalla Graphics, al crear el triple buffering, será la "próxima "fuera de la pantalla Graphics objeto de forma circular.

La imagen y el contexto de Gráficos no están relacionados en un escenario de contención, y le dijo a Swing que haría el dibujo usted mismo, por lo que debe dibujar la imagen manualmente. Esto no siempre es malo, ya que puede especificar el volteo del búfer cuando la imagen está completamente dibujada (y no antes).

La eliminación de los objetos gráficos es una buena idea, ya que ayuda en la recolección de basura.Al mostrar el bufferStrategy se volcarán los búferes.

Si bien puede haber habido un error en algún lugar del código anterior, esto debería hacerte llegar el 90% del camino hasta allí. ¡Buena suerte!

---- ---- Post original sigue

Puede parecer tonto para referirse a una pregunta tan JavaSE un tutorial, pero ¿ha mirado en BufferStrategy and BufferCapatbilites?

El problema principal que creo que está encontrando es que se deja engañar por el nombre de la imagen. Un BufferedImage no tiene nada que ver con el doble almacenamiento en búfer, tiene que ver con "almacenar en búfer los datos (generalmente del disco) en la memoria". Como tal, necesitarás dos imágenes Buffered si deseas tener una "imagen de doble buffer"; ya que no es prudente alterar los píxeles en la imagen que se muestra (podría causar problemas de repintado).

En su código de representación, toma el objeto gráfico. Si configura el doble almacenamiento en memoria intermedia de acuerdo con el tutorial anterior, esto significa que tomará (por defecto) el objeto fuera de pantalla Graphics, y que todo el dibujo estará fuera de la pantalla. Luego dibuja tu imagen (la correcta, por supuesto) al objeto fuera de la pantalla. Finalmente, le dice a la estrategia show() el búfer, y hará el reemplazo del contexto de Gráficos por usted.

+0

@Edwin Buck: +1 por la ayuda pero no uso BufferedImage porque el nombre me estaría engañando: los estoy usando porque la capacidad de manipular píxeles directamente en la imagen es un requisito y la única forma que conozco obtener acceso al buffer de datos de píxeles subyacente de una imagen es usar una imagen Buffered. – SyntaxT3rr0r

+0

@SpoonBender Bueno, estaba un poco equivocado al pensar que necesitarías dos imágenes Buffered, en realidad puedes salir adelante con solo una porque necesitas dibujarla en cada buffer por separado. Actualicé mi publicación dando detalles sobre cómo posiblemente integrar el soporte de doble buffer de Swing en tu ejemplo. –

+0

Esta es quizás una mejor estrategia que la que publiqué. Esta misma técnica también es utilizada por la clase Canvas para varios buffers también (y creo que esta es la técnica que utilicé hace mucho tiempo en un proyecto de animación para mis alumnos en Java). –

3

Aquí hay una variación en la que todo el dibujo tiene lugar en el event dispatch thread.

Adición:

Básicamente, tengo que modificar constantemente un bajo número de píxeles en una BufferedImage ...

Este kinetic model ilustra varios enfoques para la animación de píxeles.

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

Lo que quiere es básicamente imposible en el modo ventana con Swing. No hay soporte para la sincronización de ráster para repintados de ventanas, esto solo está disponible en modo de pantalla completa (e incluso entonces puede que no sea compatible con todas las plataformas).

Los componentes de balanceo son de doble buffer por defecto, es decir, harán todo el renderizado en un búfer intermedio y ese búfer finalmente se copiará a la pantalla, evitando el parpadeo del fondo y pintando sobre él. Y esa es la única estrategia que es razonablemente compatible en todas las plataformas subyacentes. Evita el parpadeo de la pintura, pero no el desgarro visual de los elementos gráficos en movimiento.

Una forma razonablemente simple de tener acceso a los píxeles brutos de un área completamente bajo su control sería extender un componente personalizado desde JComponent y sobrescribir su método paintComponent() para pintar el área desde una imagen almacenada (desde la memoria) :

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); 
    } 

} 

A continuación, puede manipular la imagen en el búfer de la manera que desee. Para que sus cambios sean visibles en la pantalla, simplemente llame a repaint() en él. Si realiza la manipulación de píxeles a partir de un hilo que no sea el EDT, necesita DOS imágenes almacenadas para hacer frente a las condiciones de carrera entre el repintado real y su hilo de manipulación.

Tenga en cuenta que este esqueleto no pintará toda el área del componente cuando se utiliza con un administrador de diseño que extiende el componente más allá de su tamaño preferido.

Tenga en cuenta también que el enfoque de imagen en búfer solo tiene sentido si realiza una manipulación real de bajo nivel mediante setRGB (...) en la imagen o si accede directamente al DataBuffer subyacente directamente. Si puede hacer todas las manipulaciones usando los métodos de Graphics2D, podría hacer todo el trabajo en el método paintComponent utilizando los gráficos proporcionados (que en realidad es un Graphics2D y puede simplemente enviarse).