Hajduk
Hajduk

Reputation: 1

How to make a Java GUI paint on demand only?

My application is meant to draw a tree using L-Systems. I start with an axiom, provide a rule for what should replace what in the next generation. I use a combination of JFrame/JPanel for one button (maybe more in the future)/JComponent for the drawing area. I coded a little turtle graphics method (go forward, turn right, turn left, push current transform, pop a transform). Each time I click the "Generate" button, I call repaint() which in turn calls turtleDraw().

package com.flak;

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Stack;

@SuppressWarnings("serial")
public class LSystemTree extends JFrame{
    JButton generateBut;
    int currentAction = 1;

    public static void main(String[] args) {
        new LSystemTree();
    }

    public LSystemTree() {
        this.setSize(600, 600);
        this.setTitle("L-System Tree");
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        this.setResizable(false);
        JPanel buttonPanel = new JPanel();
        Box box = Box.createHorizontalBox();
        generateBut = makeButton("Generate", 1);
        box.add(generateBut);
        buttonPanel.add(box);
        Map<String, String> rules = new HashMap<>();
        rules.put("F", "FF+[+F-F-F]-[-F+F+F]");
        this.add(buttonPanel,  BorderLayout.NORTH);
        this.add(new TreeDrawing("F", 22.5, rules), BorderLayout.CENTER);
        this.setVisible(true);
    }

    public JButton makeButton(String text, final int actionNum) {
        JButton theBut = new JButton();
        theBut.setText(text);
        theBut.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                currentAction = actionNum;
                System.out.println("actionNum: " + actionNum);
                repaint();
            }
        });
        return theBut; 
    }

    private class TreeDrawing extends JComponent{
        private String axiom;
        private String sentence;
        private double angle;
        private Map<String, String> rules;
        private int len;
        private Stack<AffineTransform> transformStack;
        public TreeDrawing(String axiom, double angle, Map<String, String> rules) {
            this.axiom = axiom;
            this.sentence = axiom;
            this.angle = Math.toRadians(angle);
            this.rules = rules;
            len = 100;
            transformStack = new Stack<>();
        }

        public void generate() {
            len /= 2;
            String nextSentence = "";
            for(int i = 0; i < sentence.length(); i++) {
                char current = sentence.charAt(i);
                boolean found = false;
                if(rules.containsKey(String.valueOf(current))) {
                    found = true;
                    nextSentence += rules.get(String.valueOf(current));
                }
                if(!found) {
                    nextSentence += current;
                }
            }
            sentence = nextSentence;
        }

        private void turtleDraw(Graphics2D g2d) {
            g2d.translate(getWidth() / 2, getHeight());
            for(int i = 0; i < sentence.length(); i++) {
                char current = sentence.charAt(i);
                if(current == 'F') {
                    g2d.drawLine(0, 0, 0, -len);
                    g2d.translate(0, -len);
                } else if(current == '+') {
                    g2d.rotate(angle);
                } else if(current == '-') {
                    g2d.rotate(-angle);
                } else if(current == '[') {
                    transformStack.push(g2d.getTransform());
                } else if(current == ']') {
                    g2d.setTransform(transformStack.pop());
                }
            }
            generate();
            System.out.println(sentence);
        }

        @Override
        public void paintComponent(Graphics g) {
            Graphics2D g2d = (Graphics2D) g;
            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            turtleDraw(g2d);
            g2d.dispose();
        }
    }
}

The first problem I encountered is that paint() which in turn calls paintComponent() (I think) is already called twice when I run the application. That's already annoying. I set setResizable() to false because that would repaint the window again. Is there a way to go around this, as to draw the lines only when I click "Generate"? From what I found, there's no actual way to stop paint() from being called when the application "needs to do it", so maybe there's another way to tweak the code.

Upvotes: 0

Views: 402

Answers (2)

MadProgrammer
MadProgrammer

Reputation: 347334

The first problem I encountered is that paint() which in turn calls paintComponent() (I think) is already called twice when I run the application. That's already annoying.

Painting can occur at any time for any reason, it's the responsibility of the paint/Component methods to redraw the current state of the component in response. This is controlled by the painting sub system.

I set setResizable() to false because that would repaint the window again. Is there a way to go around this, as to draw the lines only when I click "Generate"? From what I found, there's no actual way to stop paint() from being called when the application "needs to do it",

In general - no. Swing makes use of passive rendering algorithm, this means that painting occurs at irregular intervals and only when the system thinks that something needs to be updated

Your paintComponent is slightly wrong. You shouldn't be calling dispose on a Graphics context you didn't create, this could have adverse effects on other components which need to be painted after yours

@Override
public void paintComponent(Graphics g) {
    Graphics2D g2d = (Graphics2D) g;
    g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
    turtleDraw(g2d);
    //g2d.dispose();
}

so maybe there's another way to tweak the code.

Well, the first question is, why is this a problem? The reason for asking, as any other solution may introduce more overhead and complexity beyond their implied advantages.

You could use a double buffering approach, painting the updates of your algorithm to a image and then painting the image via paintComponent. This could reduce the re-draw complexity/time, assuming you're painting tens of thousands of iterations on each paint pass.

You could use java.awt.Canvas and it's BufferStrategy which would give you complete control over the painting process. This is complex solution and may not be required for an otherwise simple problem

Upvotes: 0

Xirema
Xirema

Reputation: 20396

You need to decouple the drawing of the lines from the API's refresh behavior.

A simple hack in the meantime, though, is to just manually manage the redraw behavior by caching an image of the fully drawn image, and then setting the image to null to force a redraw.

private Image turtleImage = null;
private void turtleDraw(Graphics2D g2d) {
    if(turtleImage == null) { //Anywhere else, set turtleImage to null to force a redraw
        turtleImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_ARGB);
        Graphics2D graphics = turtleImage.createGraphics();
        graphics.translate(getWidth() / 2, getHeight());
        for(int i = 0; i < sentence.length(); i++) {
            char current = sentence.charAt(i);
            if(current == 'F') {
                graphics.drawLine(0, 0, 0, -len);
                graphics.translate(0, -len);
            } else if(current == '+') {
                graphics.rotate(angle);
            } else if(current == '-') {
                graphics.rotate(-angle);
            } else if(current == '[') {
                transformStack.push(graphics.getTransform());
            } else if(current == ']') {
                graphics.setTransform(transformStack.pop());
            }
        }
        generate();
        System.out.println(sentence);
    }
    g2d.drawImage(turtleImage, 0, 0, null, null);
}

With this code, any other method in your class can set turtleImage to null, and the screen will be redrawn when the OS does its normal redraw behavior.

Upvotes: 0

Related Questions