Reputation: 1
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
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
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