Search code examples
javaswingdrawingjpanelpaintcomponent

How to keep drawn shapes from disappearing in Java?


I'm working on a project to create a screen saver that draws random shapes. I have a few questions, but my main concern right now is how to get the shapes to stay on the screen and not just disappear after they are created. Here is my code. I cannot use any loops and I am not looking to change any of the functionality of what I have yet (other than possibly shapesDrawn).

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

public class ScreenSaver2 extends JPanel implements ActionListener {
    private JFrame frame = new JFrame("FullSize");
    private Rectangle rectangle;
    boolean full;


    protected void paintComponent(Graphics g) {
        int r = (int)(Math.random() * 255);
        int gr = (int)(Math.random() * 255);
        int b = (int)(Math.random() * 255);
        Color color = new Color(r, gr, b);
        int width = 10 + (int)(Math.random() * 40);
        int height = 10 + (int)(Math.random() * 40);
        int x = (int)(Math.random() * (getWidth() - width));
        int y = (int)(Math.random() * (getHeight() - height));
        int whichShape = (int)(Math.random() * 3);
        int shapesDrawn = 0;

        super.paintComponent(g);
        if (shapesDrawn >= 30) {
            shapesDrawn = 0;
        }

        switch (whichShape) {
        case 0:
            g.setColor(color);
            g.drawLine(x, y, x + width, y + height);
            shapesDrawn++;
            break;
        case 1:
            g.setColor(color);
            g.drawRect(x, y, width, height);
            shapesDrawn++;
            break;
        case 2:
            g.setColor(color);
            g.drawRoundRect(x, y, width, height, 25, 25);
            shapesDrawn++;
            break;
        case 3:
            g.setColor(color);
            g.drawOval(x, y, width, height);
            shapesDrawn++;
            break;
        }

    }


    ScreenSaver2() {
        // Remove the title bar, min, max, close stuff
        frame.setUndecorated(true);
        // Add a Key Listener to the frame
        frame.addKeyListener(new KeyHandler());
        // Add this panel object to the frame
        frame.add(this);
        // Get the dimensions of the screen
        rectangle = GraphicsEnvironment.getLocalGraphicsEnvironment()
        .getDefaultScreenDevice().getDefaultConfiguration().getBounds();
        // Set the size of the frame to the size of the screen
        frame.setSize(rectangle.width, rectangle.height);
        frame.setVisible(true);
        // Remember that we are currently at full size
        full = true;
        // set and initialize timer
        Timer t = new Timer(500, this);
        t.setDelay(500);
        t.start();

    }

    // This method will run when any key is pressed in the window
    class KeyHandler extends KeyAdapter {
        public void keyPressed(KeyEvent e) {
            // Terminate the program.
            if (e.getKeyChar() == 'x') {
                System.out.println("Exiting");
                System.exit(0);
            }
            // Change background color
            else if (e.getKeyChar() == 'r') {
                System.out.println("Change background color");
                setBackground(new Color((int)(Math.random() * 256), (int)(Math.random() * 256), (int)(Math.random() * 256)));
                repaint();
            }
            // Resize to half-screen
            else if (e.getKeyChar() == 'z') {
                System.out.println("Resizing");
                frame.setSize((int)rectangle.getWidth() / 2, (int)rectangle.getHeight());
            }
        }
    }

    public void actionPerformed(ActionEvent e) {
        repaint();
    }

    public static void main(String[] args) {
        ScreenSaver2 obj = new ScreenSaver2();
    }
}

Solution

  • Rather than painting each new shape within the paintComponent directly to the Graphics context, use a backing buffer to render the shapes to first and paint this to the Graphics context.

    This way, you would simply need to maintain a simple counter each time a new shape is rendered.

    This is also troublesome, because paintComponent may be called for any number of reasons, which, you don't control many, meaning that it would be possible for your program to paint more shapes before any timer ticks have actually occurred...

    Updated with example

    The only choice you have left is to create a backing buffer, onto which you can paint each change as required. This will "store" each change between paint cycles. You could then simply paint this to the screen...

    private BufferedImage img;
    
    //...
    
    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        Graphics2D g2d = (Graphics2D) g.create();
        // Create the backing buffer
        // This is a little cheat, creating a new image when the number of shapes
        // exceeds the requirements, but it saves messing about with clearing
        // a alpha image ;)
        if (img == null || shapesDrawn >= 30) {
            img = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
            shapesDrawn = 0;
        } else if (img.getWidth() != getWidth() || img.getHeight() != img.getHeight()) {
            // Update the backing buffer to meet the requirements of the changed screen size...
            BufferedImage buffer = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
            Graphics2D gbuffer = buffer.createGraphics();
            gbuffer.drawImage(img, 0, 0, this);
            gbuffer.dispose();
            img = buffer;
        }
    
        // Get a reference to the backing buffers graphics context...
        Graphics2D gbuffer = img.createGraphics();
    
        // Paint the shapes to the backing buffer...
        int r = (int) (Math.random() * 255);
        int gr = (int) (Math.random() * 255);
        int b = (int) (Math.random() * 255);
        Color color = new Color(r, gr, b);
        int width = 10 + (int) (Math.random() * 40);
        int height = 10 + (int) (Math.random() * 40);
        int x = (int) (Math.random() * (getWidth() - width));
        int y = (int) (Math.random() * (getHeight() - height));
        int whichShape = (int) (Math.random() * 3);
        int shapesDrawn = 0;
    
        switch (whichShape) {
            case 0:
                gbuffer.setColor(color);
                gbuffer.drawLine(x, y, x + width, y + height);
                shapesDrawn++;
                break;
            case 1:
                gbuffer.setColor(color);
                gbuffer.drawRect(x, y, width, height);
                shapesDrawn++;
                break;
            case 2:
                gbuffer.setColor(color);
                gbuffer.drawRoundRect(x, y, width, height, 25, 25);
                shapesDrawn++;
                break;
            case 3:
                gbuffer.setColor(color);
                gbuffer.drawOval(x, y, width, height);
                shapesDrawn++;
                break;
        }
        // Dispose of the buffers graphics context, this frees up memory for us
        gbuffer.dispose();
        // Paint the image to the screen...
        g2d.drawImage(img, 0, 0, this);
        g2d.dispose();
    }
    

    Possibly the worst advice I've ever had to give

    Change...

    super.paintComponent(g);
    if (shapesDrawn >= 30) {
        shapesDrawn = 0;
    }
    

    To...

    if (shapesDrawn >= 30) {
        super.paintComponent(g);
        shapesDrawn = 0;
    }