javaswingawt2djbox2d

Is this the correct way of using Java 2D Graphics API?


I'm creating a graphical front-end for a JBox2D simulation. The simulation runs incrementally, and in between the updates, the contents of the simulation are supposed to be drawn. Similar to a game except without input.

I only need geometric primitives to draw a JBox2D simulation. This API seemed like the simplest choice, but its design is a bit confusing.

Currently I have one class called Window extending JFrame, that contains as a member another class called Renderer. The Window class only initializes itself and provides an updateDisplay() method (that is called by the main loop), that calls updateDisplay(objects) method on the Renderer. I made these two methods myself and their only purpose is to call repaint() on the Renderer.

Is the JPanel supposed to be used that way? Or am I supposed to use some more sophisticated method for animation (such that involves events and/or time intervals in some back-end thread)?


Solution

  • If you are wanting to schedule the updates at a set interval, javax.swing.Timer provides a Swing-integrated service for it. Timer runs its task on the EDT periodically, without having an explicit loop. (An explicit loop would block the EDT from processing events, which would freeze the UI. I explained this more in-depth here.)

    Ultimately doing any kind of painting in Swing you'll still be doing two things:

    1. Overriding paintComponent to do your drawing.
    2. Calling repaint as-needed to request that your drawing be made visible. (Swing normally only repaints when it's needed, for example when some other program's window passes over top of a Swing component.)

    If you're doing those two things you're probably doing it right. Swing doesn't really have a high-level API for animation. It's designed primarily with drawing GUI components in mind. It can certainly do some good stuff, but you will have to write a component mostly from scratch, like you're doing.

    Painting in AWT and Swing covers some of the 'behind the scenes' stuff if you do not have it bookmarked.

    You might look in to JavaFX. I don't know that much about it personally, but it's supposed to be more geared towards animation.

    As somewhat of an optimization, one thing that can be done is to paint on a separate image and then paint the image on to the panel in paintComponent. This is especially useful if the painting is long: repaints can be scheduled by the system so this keeps when it happens more under control.

    If you aren't drawing to an image, then you'd need to build a model with objects, and paint all of them every time inside paintComponent.


    Here's an example of drawing to an image:

    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 screenshot


    If the routine is long-running and repaints could happen concurrently, double buffering can also be used. Drawing is done to an image which is separate from the one being shown. Then, when the drawing routine is done, the image references are swapped so the update is seamless.

    You should typically use double buffering for a game, for example. Double buffering prevents the image from being shown in a partial state. This could happen if, for example, you were using a background thread for the game loop (instead of a Timer) and a repaint happened the game was doing the painting. Without double buffering, this kind of situation would result in flickering or tearing.

    Swing components are double buffered by default, so if all of your drawing is happening on the EDT you don't need to write double buffering logic yourself. Swing already does it.

    Here is a somewhat more complicated example which shows a long-running task and a buffer swap:

    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();
            }
        }
    }
    

    The painting routine is just intended draw garbage which takes a long time:

    DoubleBuffer screenshot