Java: как сделать двойную буферизацию в Swing? - PullRequest
17 голосов
/ 13 декабря 2010

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

Чтобы не допустить вкрадчивых комментариев и однострочных ответов, пропуская точку: IFF это так же просто, как вызов 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;
    }

}

EDIT

Это не для полноэкранного Java-приложения, а обычное Java-приложение, работающее в собственном (несколько маленьком) окне.

Ответы [ 4 ]

13 голосов
/ 13 декабря 2010

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

Элемент уничтожает двойную буферизацию, но есть также проблема с тем, как получить пиксели в 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 прекратить растеризацию в любое время;потому что вы будете говорить об этом, когда закончите рисовать буферизованное изображение, которое хотите полностью заменить.Сделайте это с помощью

setIgnoreRepaint(true);

в JFrame. Затем вы захотите создать буферную стратегию.По сути, он определяет, сколько буферов вы хотите использовать

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, но вы изучили * 1052?* и BufferCapatbilites?

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

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

3 голосов
/ 13 декабря 2010

То, что вы хотите, в принципе невозможно в оконном режиме с Swing.Не поддерживается растровая синхронизация для перерисовок окон, она доступна только в полноэкранном режиме (и даже в этом случае может поддерживаться не всеми платформами).

Компоненты Swing по умолчанию имеют двойную буферизацию, то есть они будутвыполнить весь рендеринг в промежуточный буфер, и этот буфер затем окончательно копируется на экран, избегая мерцания при очистке фона, а затем рисуется поверх него.И это единственная стратегия, которая разумно хорошо поддерживается на всех базовых платформах.Это позволяет избежать только мерцания при перерисовке, но не визуального отрыва от движущихся графических элементов.

Достаточно простой способ получить доступ к необработанным пикселям области, находящейся полностью под вашим контролем, состоит в расширении пользовательского компонента из JComponent и перезаписи.its 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 и может быть просто приведена).

3 голосов
/ 13 декабря 2010

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

Добавление:

По сути, мне нужно постоянно изменять много пикселей в 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://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 голосов
/ 13 декабря 2010

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

...