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

3

1

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)?

Seismoscope answered 21/1, 2014 at 17:52 Comment(0)
P
3

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

Pinkiepinkish answered 21/1, 2014 at 18:27 Comment(5)
I wont look into JavaFX right now since I'm running short on time but thanks. I am doing those two things. Also, the window should be repainted each time the simulation is updated, kind of, so for now I'm OK with not using a Timer.Seismoscope
This is awfully slow unless you do double buffering by hand, FYIBravado
@DanielMurphy Of course there are optimizations that can be made but that isn't the question. Note that all Swing components are by default double buffered so a more elegant optimization is to do custom image drawing to an Image and paint the Image on the panel in repaint. There's no need to double buffer.Pinkiepinkish
That's exactly what I meant actually. The image is the buffer :)Bravado
I updated my answer with some code examples showing what @DanielMurphy and I briefly discussed. The second is more optimized and it's true that this could be considered tedious compared to using an existing library.Pinkiepinkish
S
2

For a tightly coupled simulation, javax.swing.Timer is a good choice. Let the timer's listener invoke your implementation of paintComponent(), as shown here and in the example cited here.

For a loosely coupled simulation, let the model evolve in the background thread of a SwingWorker, as shown here. Invoke publish() when apropos to you simulation.

The choice is dictated in part by the nature of the simulation and the duty cycle of the model.

Savoirvivre answered 21/1, 2014 at 18:30 Comment(0)
B
1

Why not just use stuff from the testbed? It already does everything. Just take the JPanel, controller, and debug draw. It uses Java 2D drawing.

See here for the JPanel that does the buffered rendering: https://github.com/dmurph/jbox2d/blob/master/jbox2d-testbed/src/main/java/org/jbox2d/testbed/framework/j2d/TestPanelJ2D.java

and here for the debug draw: https://github.com/dmurph/jbox2d/blob/master/jbox2d-testbed/src/main/java/org/jbox2d/testbed/framework/j2d/DebugDrawJ2D.java

See the TestbedMain.java file to see how the normal testbed is launched, and rip out what you don't need :)

Edits: Disclaimer: I maintain jbox2d

Here is the package for the testbed framework: https://github.com/dmurph/jbox2d/tree/master/jbox2d-testbed/src/main/java/org/jbox2d/testbed/framework

TestbedMain.java is in the j2d folder, here: https://github.com/dmurph/jbox2d/tree/master/jbox2d-testbed/src/main/java/org/jbox2d/testbed/framework/j2d

Bravado answered 22/1, 2014 at 18:46 Comment(3)
Usually when you are promoting something you are affiliated with it's good etiquette to point out your affiliation.Pinkiepinkish
Wow that would be stellar if it actually works, thanks! Could you provide some more information on how I'm supposed to use this code? Maybe a simple example? I can't find TestbedMain.java.Seismoscope
Here is the package for the testbed framework: github.com/dmurph/jbox2d/tree/master/jbox2d-testbed/src/main/… TestbedMain.java is in the j2d folder, here: github.com/dmurph/jbox2d/tree/master/jbox2d-testbed/src/main/…Bravado

© 2022 - 2024 — McMap. All rights reserved.