Это правильный способ использования Java 2D Graphics API?

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

Мне нужны только геометрические примитивы для рисования симуляции JBox2D. Этот API выглядел как самый простой выбор, но его дизайн немного сбивает с толку.

В настоящее время у меня есть один класс под названием Window простирающийся JFrame, который содержит в качестве члена другой класс с именем Renderer, Window класс только инициализирует себя и обеспечивает updateDisplay() метод (который вызывается основным циклом), который вызывает updateDisplay(objects) метод на Renderer, Я сделал эти два метода сам, и их единственная цель состоит в том, чтобы вызвать repaint() на Renderer,

Это JPanel должен быть использован таким образом? Или я должен использовать какой-то более сложный метод для анимации (такой, который включает события и / или временные интервалы в некотором внутреннем потоке)?

3 ответа

Решение

Если вы хотите запланировать обновления с заданным интервалом, javax.swing.Timer предоставляет интегрированный сервис Swing для этого. Timer периодически выполняет свою задачу на EDT, без явного цикла. (Явный цикл блокировал бы EDT от обработки событий, что могло бы заморозить интерфейс. Я объяснил это более подробно здесь.)

В конечном итоге, занимаясь рисованием в Swing, вы все равно будете делать две вещи:

  1. Переопределение paintComponent сделать свой рисунок.
  2. призвание repaint при необходимости, чтобы сделать ваш рисунок видимым. (Обычно Swing перерисовывается только тогда, когда это необходимо, например, когда окно какой-либо другой программы проходит поверх компонента Swing.)

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

Рисование в AWT и Swing охватывает некоторые вещи "за кадром", если вы не добавили их в закладки.

Вы можете заглянуть в JavaFX. Лично я не очень много знаю об этом, но он должен быть более ориентирован на анимацию.

В качестве некоторой оптимизации можно сделать одну вещь - нарисовать отдельное изображение, а затем нарисовать изображение на панели в paintComponent, Это особенно полезно, если картина длинная: перерисовка может быть запланирована системой, поэтому она сохраняется, когда это происходит под большим контролем.

Если вы не рисуете изображение, вам нужно построить модель с объектами и каждый раз рисовать их внутри. paintComponent,


Вот пример рисования изображения:

import javax.swing.*;
import java.awt.*;
import java.awt.image.*;
import java.awt.event.*;

/**
 * Holding left-click draws, and
 * right-clicking cycles the color.
 */
class PaintAnyTime {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new PaintAnyTime();
            }
        });
    }

    Color[]    colors = {Color.red, Color.blue, Color.black};
    int  currentColor = 0;
    BufferedImage img = new BufferedImage(256, 256, BufferedImage.TYPE_INT_ARGB);
    Graphics2D  imgG2 = img.createGraphics();

    JFrame frame = new JFrame("Paint Any Time");
    JPanel panel = new JPanel() {
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            // Creating a copy of the Graphics
            // so any reconfiguration we do on
            // it doesn't interfere with what
            // Swing is doing.
            Graphics2D g2 = (Graphics2D) g.create();
            // Drawing the image.
            int w = img.getWidth();
            int h = img.getHeight();
            g2.drawImage(img, 0, 0, w, h, null);
            // Drawing a swatch.
            Color color = colors[currentColor];
            g2.setColor(color);
            g2.fillRect(0, 0, 16, 16);
            g2.setColor(Color.black);
            g2.drawRect(-1, -1, 17, 17);
            // At the end, we dispose the
            // Graphics copy we've created
            g2.dispose();
        }
        @Override
        public Dimension getPreferredSize() {
            return new Dimension(img.getWidth(), img.getHeight());
        }
    };

    MouseAdapter drawer = new MouseAdapter() {
        boolean rButtonDown;
        Point prev;

        @Override
        public void mousePressed(MouseEvent e) {
            if (SwingUtilities.isLeftMouseButton(e)) {
                prev = e.getPoint();
            }
            if (SwingUtilities.isRightMouseButton(e) && !rButtonDown) {
                // (This just behaves a little better
                // than using the mouseClicked event.)
                rButtonDown  = true;
                currentColor = (currentColor + 1) % colors.length;
                panel.repaint();
            }
        }

        @Override
        public void mouseDragged(MouseEvent e) {
            if (prev != null) {
                Point  next = e.getPoint();
                Color color = colors[currentColor];
                // We can safely paint to the
                // image any time we want to.
                imgG2.setColor(color);
                imgG2.drawLine(prev.x, prev.y, next.x, next.y);
                // We just need to repaint the
                // panel to make sure the
                // changes are visible
                // immediately.
                panel.repaint();
                prev = next;
            }
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            if (SwingUtilities.isLeftMouseButton(e)) {
                prev = null;
            }
            if (SwingUtilities.isRightMouseButton(e)) {
                rButtonDown = false;
            }
        }
    };

    PaintAnyTime() {
        // RenderingHints let you specify
        // options such as antialiasing.
        imgG2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                            RenderingHints.VALUE_ANTIALIAS_ON);
        imgG2.setStroke(new BasicStroke(3));
        //
        panel.setBackground(Color.white);
        panel.addMouseListener(drawer);
        panel.addMouseMotionListener(drawer);
        Cursor cursor =
            Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR);
        panel.setCursor(cursor);
        frame.setContentPane(panel);
        frame.pack();
        frame.setResizable(false);
        frame.setLocationRelativeTo(null);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }
}

PaintAnyTime скриншот


Если процедура длится долго и перерисовки могут происходить одновременно, двойная буферизация также может быть использована. Рисование выполняется на изображении, отдельном от показанного. Затем, когда процедура рисования завершена, ссылки на изображения меняются местами, поэтому обновление происходит без проблем.

Например, вы должны использовать двойную буферизацию для игры. Двойная буферизация предотвращает частичное отображение изображения. Это может произойти, если, например, вы используете фоновый поток для игрового цикла (вместо Timer) и случился перекрас, игра рисовала. Без двойной буферизации такая ситуация может привести к мерцанию или разрыву.

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

Вот несколько более сложный пример, который показывает долгосрочную задачу и замену буфера:

import java.awt.*;
import javax.swing.*;
import java.awt.image.*;
import java.awt.event.*;
import java.util.*;

/**
 * Left-click to spawn a new background
 * painting task.
 */
class DoubleBuffer {
    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new DoubleBuffer();
            }
        });
    }

    final int  width = 640;
    final int height = 480;

    BufferedImage createCompatibleImage() {
        GraphicsConfiguration gc =
            GraphicsEnvironment
                .getLocalGraphicsEnvironment()
                .getDefaultScreenDevice()
                .getDefaultConfiguration();
        // createCompatibleImage creates an image that is
        // optimized for the display device.
        // See http://docs.oracle.com/javase/8/docs/api/java/awt/GraphicsConfiguration.html#createCompatibleImage-int-int-int-
        return gc.createCompatibleImage(width, height, Transparency.TRANSLUCENT);
    }

    // The front image is the one which is
    // displayed in the panel.
    BufferedImage front = createCompatibleImage();
    // The back image is the one that gets
    // painted to.
    BufferedImage  back = createCompatibleImage();
    boolean  isPainting = false;

    final JFrame frame = new JFrame("Double Buffer");
    final JPanel panel = new JPanel() {
        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            // Scaling the image to fit the panel.
            Dimension actualSize = getSize();
            int w = actualSize.width;
            int h = actualSize.height;
            g.drawImage(front, 0, 0, w, h, null);
        }
    };

    final MouseAdapter onClick = new MouseAdapter() {
        @Override
        public void mousePressed(MouseEvent e) {
            if (!isPainting) {
                isPainting = true;
                new PaintTask(e.getPoint()).execute();
            }
        }
    };

    DoubleBuffer() {
        panel.setPreferredSize(new Dimension(width, height));
        panel.setBackground(Color.WHITE);
        panel.addMouseListener(onClick);
        frame.setContentPane(panel);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }

    void swap() {
        BufferedImage temp = front;
        front = back;
        back = temp;
    }

    class PaintTask extends SwingWorker<Void, Void> {
        final Point pt;

        PaintTask(Point pt) {
            this.pt = pt;
        }

        @Override
        public Void doInBackground() {
            Random rand = new Random();

            synchronized(DoubleBuffer.this) {
                Graphics2D g2 = back.createGraphics();
                g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                                    RenderingHints.VALUE_ANTIALIAS_ON);
                g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
                                    RenderingHints.VALUE_STROKE_PURE);
                g2.setBackground(new Color(0, true));
                g2.clearRect(0, 0, width, height);
                // (This computes pow(2, rand.nextInt(3) + 7).)
                int  depth = 1 << ( rand.nextInt(3) + 7 );
                float  hue = rand.nextInt(depth);
                int radius = 1;
                int c;
                // This loop just draws concentric circles,
                // starting from the inside and extending
                // outwards until it hits the outside of
                // the image.
                do {
                    int rgb = Color.HSBtoRGB(hue / depth, 1, 1);
                    g2.setColor(new Color(rgb));

                    int x = pt.x - radius;
                    int y = pt.y - radius;
                    int d = radius * 2;

                    g2.drawOval(x, y, d, d);

                    ++radius;
                    ++hue;
                    c = (int) (radius * Math.cos(Math.PI / 4));
                } while (
                       (0 <= pt.x - c) || (pt.x + c < width)
                    || (0 <= pt.y - c) || (pt.y + c < height)
                );

                g2.dispose();
                back.flush();

                return (Void) null;
            }
        }

        @Override
        public void done() {
            // done() is completed on the EDT,
            // so for this small program, this
            // is the only place where synchronization
            // is necessary.
            // paintComponent will see the swap
            // happen the next time it is called.
            synchronized(DoubleBuffer.this) {
                swap();
            }

            isPainting = false;
            panel.repaint();
        }
    }
}

Процедура рисования предназначена только для рисования мусора, который занимает много времени:

Скриншот DoubleBuffer

Для тесно связанной симуляции, javax.swing.Timer хороший выбор Позвольте слушателю таймера вызвать вашу реализацию paintComponent(), как показано здесь и в приведенном здесь примере.

Для слабосвязанной симуляции позвольте модели развиваться в фоновом потоке SwingWorker, как показано здесь. взывать publish() когда у вас симуляция.

Выбор продиктован частично характером моделирования и рабочим циклом модели.

Почему бы просто не использовать материал с испытательного стенда? Это уже все делает. Просто возьмите JPanel, контроллер и отладочный розыгрыш. Это использует Java 2D рисунок.

Смотрите здесь для JPanel, который выполняет буферизованный рендеринг: https://github.com/dmurph/jbox2d/blob/master/jbox2d-testbed/src/main/java/org/jbox2d/testbed/framework/j2d/TestPanelJ2D.java

и здесь для отладки: https://github.com/dmurph/jbox2d/blob/master/jbox2d-testbed/src/main/java/org/jbox2d/testbed/framework/j2d/DebugDrawJ2D.java

Посмотрите файл TestbedMain.java, чтобы увидеть, как запускается обычный тестовый стенд, и извлеките то, что вам не нужно:)

Редактирует: Отказ от ответственности: я поддерживаю jbox2d

Вот пакет для среды тестирования: https://github.com/dmurph/jbox2d/tree/master/jbox2d-testbed/src/main/java/org/jbox2d/testbed/framework

TestbedMain.java находится в папке j2d, здесь: https://github.com/dmurph/jbox2d/tree/master/jbox2d-testbed/src/main/java/org/jbox2d/testbed/framework/j2d

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