Это правильный способ использования 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, вы все равно будете делать две вещи:
- Переопределение
paintComponent
сделать свой рисунок. - призвание
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);
}
}
Если процедура длится долго и перерисовки могут происходить одновременно, двойная буферизация также может быть использована. Рисование выполняется на изображении, отдельном от показанного. Затем, когда процедура рисования завершена, ссылки на изображения меняются местами, поэтому обновление происходит без проблем.
Например, вы должны использовать двойную буферизацию для игры. Двойная буферизация предотвращает частичное отображение изображения. Это может произойти, если, например, вы используете фоновый поток для игрового цикла (вместо 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();
}
}
}
Процедура рисования предназначена только для рисования мусора, который занимает много времени:
Для тесно связанной симуляции, 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