这是使用Java 2D Graphics API的正确方法吗?

我正在为JBox2D模拟创建一个图形前端。 模拟以递增方式运行,并且在更新之间,应该绘制模拟的内容。 类似于游戏,除了没有输入。

我只需要几何图元来绘制JBox2D模拟。 这个API似乎是最简单的选择,但它的设计有点令人困惑。

目前我有一个名为Window JFrame类,它包含另一个名为Renderer类作为成员。 Window类仅初始化自身并提供updateDisplay()方法(由主循环调用),该方法在Renderer上调用updateDisplay(objects)方法。 我自己制作了这两种方法,它们的唯一目的是在Renderer上调用repaint()

JPanel应该以这种方式使用吗? 或者我应该使用一些更复杂的动画方法(这涉及一些后端线程中的事件和/或时间间隔)?

如果您希望按设定的时间间隔安排更新, javax.swing.Timer为其提供Swing集成服务。 Timer定期在EDT上运行其任务,没有显式循环。 (显式循环会阻止EDT处理事件,这会冻结UI。我在这里更深入地解释了这一点 。)

最终在Swing中做任何一种绘画你仍然会做两件事:

  1. 覆盖paintComponent来完成绘图。
  2. 根据需要调用repaint以请求使您的绘图可见。 (Swing通常只在需要时重新绘制,例如当某个其他程序的窗口经过Swing组件的顶部时。)

如果你正在做这两件事你可能做得对。 Swing实际上没有动画的高级API。 它主要是为绘制GUI组件而设计的。 它当然可以做一些好东西,但你必须从头开始编写一个组件,就像你正在做的那样。

如果你没有书签, 在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上发生,则您不需要自己编写双缓冲逻辑。 Swing已经做到了。

这是一个更复杂的示例,它显示了一个长时间运行的任务和一个缓冲区交换:

 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 { 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