Java: как сделать двойную буферизацию в Swing?

РЕДАКТИРОВАТЬ ВТОРОЙ

Чтобы не допустить вкрадчивых комментариев и однострочных ответов, пропускающих точку: если это так просто, как вызвать setDoubleBuffered (true), то как мне получить доступ к текущему автономному буферу, чтобы я мог начать работу с базовым пиксельным буфером данных BufferedImage?

Я потратил время, чтобы написать работающий кусок кода (который тоже выглядит довольно забавно), поэтому я был бы очень признателен за ответы, которые на самом деле отвечают (что шокирует;) на мой вопрос и объясняют, что / как это работает вместо однострочников и ворчливости Комментарии;)

Вот рабочий фрагмент кода, который пересекает квадрат через JFrame. Я хотел бы знать о различных способах, которые можно использовать для преобразования этого фрагмента кода, чтобы он использовал двойную буферизацию.

Обратите внимание, что способ очистки экрана и перерисовки квадрата не самый эффективный, но на самом деле это не тот вопрос, о котором идет речь (в некотором смысле, в этом примере лучше, если он несколько медленный).

По сути, мне нужно постоянно изменять много пикселей в BufferedImage (чтобы иметь некоторую анимацию), и я не хочу видеть визуальные артефакты из-за одиночной буферизации на экране.

У меня есть JLabel, Icon которого является ImageIcon, оборачивающим BufferedImage. Я хочу изменить это BufferedImage.

Что нужно сделать, чтобы это стало двойной буферизацией?

Я понимаю, что каким-то образом "изображение 1" будет показано, пока я буду рисовать "изображение 2". Но как только я закончу рисовать "изображение 2", как мне "быстро" заменить "изображение 1" на "изображение 2"?

Это то, что я должен делать вручную, например, меняя сам ImageIcon JLabel?

Должен ли я всегда рисовать в одном и том же BufferedImage, а затем делать быстрое "блицание" пикселей этого BufferedImage в BufferedImage JIabel's ImageIcon? (Наверное, нет, и я не вижу, как я мог бы "синхронизировать" это с "вертикальной пустой линией" монитора [или эквивалентом на плоском экране: я имею в виду, чтобы "синхронизировать", не вмешиваясь в момент обновления самого монитора). пикселей, чтобы предотвратить сдвиг]).

А как насчет "перекрасить" заказы? Я полагаю, чтобы вызвать их сам? Который / когда именно я должен вызвать 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 ответа

---- Отредактировано по адресу на пиксель настройки ----

Удар по объекту обращается к двойной буферизации, но есть также проблема, как получить пиксели в 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's

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 а такжеBufferCapatbilites?

Я думаю, что главная проблема, с которой вы сталкиваетесь, заключается в том, что вас одурачивает имя Изображения. BufferedImage не имеет ничего общего с двойной буферизацией, это связано с "буферизацией данных (обычно с диска) в памяти". Таким образом, вам понадобятся два BufferedImages, если вы хотите иметь "изображение с двойной буферизацией"; так как неразумно изменять пиксели в отображаемом изображении (это может вызвать проблемы с перекрашиванием).

В вашем коде рендеринга вы захватываете графический объект. Если вы настроили двойную буферизацию в соответствии с приведенным выше руководством, это означает, что вы по умолчанию захватите экран Graphics объект, и весь рисунок будет за кадром. Затем вы рисуете свое изображение (конечно, правильное) на объекте вне экрана. Наконец, вы говорите стратегии show() буфер, и он сделает замену графического контекста для вас.

Вот вариант, в котором все рисование происходит в потоке отправки событий.

Приложение:

По сути, мне нужно постоянно изменять много пикселей в BufferedImage...

Эта кинетическая модель иллюстрирует несколько подходов к пиксельной анимации.

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://stackru.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;
    }
}

Обычно мы используем класс 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);

    }
}

Теперь вы можете обновить x_pos и y_pos из потока с последующим вызовом repaint для объекта canvas. Та же самая техника должна работать и на JPanel.

То, что вы хотите, в принципе невозможно в оконном режиме с 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, вы можете выполнять все действия в методе paintComponent, используя предоставленную графику (которая на самом деле является Graphics2D и может быть просто приведена).

Другие вопросы по тегам