I order to practice JavaFX, I built a simple app that draws Sierpinski Triangles.
import javafx.application.Application;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
public class SierpinskiTriangles extends Application {
private final int PADDING = 5;
private static int numberOfLevels;
public static void launch(String... args){
numberOfLevels = 8;
if((args != null) && (args.length > 0)) {
int num = -1;
try {
num = Integer.parseInt(args[0]);
} catch (NumberFormatException ex) {
ex.printStackTrace();
return;
}
numberOfLevels = (num > 0) ? num : numberOfLevels;
}
Application.launch(args);
}
@Override
public void start(Stage stage) {
stage.setOnCloseRequest((ae) -> {
Platform.exit();
System.exit(0);
});
stage.setTitle("Sierpinski Triangles (fx)");
BorderPane mainPane = new BorderPane();
mainPane.setPadding(new Insets(PADDING));
Pane triPanel = new Triangles();
BorderPane.setAlignment(triPanel, Pos.CENTER);
mainPane.setCenter(triPanel);
Scene scene = new Scene(mainPane);
stage.setScene(scene);
stage.centerOnScreen();
stage.setResizable(false);
stage.show();
}
class Triangles extends AnchorPane{
private static final int PANEL_WIDTH =600, PANEL_HEIGHT = 600;
private static final int TRI_WIDTH= 500, TRI_HEIGHT= 500;
private static final int SIDE_GAP = (PANEL_WIDTH - TRI_WIDTH)/2;
private static final int TOP_GAP = (PANEL_HEIGHT - TRI_HEIGHT)/2;
private int countTriangles;
private long startTime;
private Point2D top, left, right;
private Canvas canvas;
private GraphicsContext gc;
Triangles(){
setPrefSize(PANEL_WIDTH, PANEL_HEIGHT);
canvas = getCanvas();
gc = canvas.getGraphicsContext2D();
getChildren().add(canvas);
draw(numberOfLevels);
}
void draw(int numberLevels) {
Platform.runLater(new Runnable() {
@Override
public void run() {
clearCanvas();
setStartPoints();
startTime = System.currentTimeMillis();
countTriangles = 0;
RunTask task = new RunTask(numberLevels, top, left, right);
Thread thread = new Thread(task);
thread.setDaemon(true);
thread.start();
}
});
}
private void drawTriangle( int levels, Point2D top, Point2D left, Point2D right) {
if(levels < 0) {//add stop criteria
return ;
}
gc.strokePolygon( //implementing with strokeLine did not make much difference
new double[]{
top.getX(),left.getX(),right.getX()
},
new double[]{
top.getY(),left.getY(), right.getY()
},3
);
countTriangles++;
//Get the midpoint on each edge in the triangle
Point2D p12 = midpoint(top, left);
Point2D p23 = midpoint(left, right);
Point2D p31 = midpoint(right, top);
// recurse on 3 triangular areas
drawTriangle(levels - 1, top, p12, p31);
drawTriangle(levels - 1, p12, left, p23);
drawTriangle(levels - 1, p31, p23, right);
}
private void setStartPoints() {
top = new Point2D(getPrefWidth()/2, TOP_GAP);
left = new Point2D(SIDE_GAP, TOP_GAP + TRI_HEIGHT);
right = new Point2D(SIDE_GAP + TRI_WIDTH, TOP_GAP + TRI_WIDTH);
}
private Point2D midpoint(Point2D p1, Point2D p2) {
return new Point2D((p1.getX() + p2.getX()) /
2, (p1.getY() + p2.getY()) / 2);
}
private void updateGraphics(boolean success){
if(success) {
gc.fillText("Number of triangles: "+ countTriangles,5,15);
gc.fillText("Time : "+ (System.currentTimeMillis()- startTime)+ " mili seconds", 5,35);
gc.fillText("Levels: "+ numberOfLevels,5,55);
}
System.out.println("Completed after: "+
(System.currentTimeMillis()- startTime)+ " mili seconds"
+" Triangles: " + countTriangles +" Failed: "+ !success );
}
private Canvas getCanvas() {
Canvas canvas = new Canvas();
canvas.widthProperty().bind(widthProperty());
canvas.heightProperty().bind(heightProperty());
canvas.getGraphicsContext2D().setStroke(Color.RED);
canvas.getGraphicsContext2D().setLineWidth(0.3f);
return canvas;
}
private void clearCanvas() {
gc.clearRect(0, 0, canvas.getWidth(), canvas.getHeight());
}
class RunTask extends Task<Void>{
private int levels;
private Point2D top, left;
private Point2D right;
RunTask(int levels, Point2D top, Point2D left, Point2D right){
this.levels = levels;
this.top = top;
this.left = left;
this.right = right;
startTime = System.currentTimeMillis();
countTriangles = 0;
}
@Override public Void call() {
drawTriangle(levels,top, left, right);
return null;
}
@Override
protected void succeeded() {
updateGraphics(true);
super.succeeded();
}
@Override
protected void failed() {
updateGraphics(false);
}
}
}
public static void main(String[] args) {
launch("13");
}
}
The problems I have:
a. The time printout at updateGraphics()
shows long before (8 seconds on my machine) the drawing of the triangles is completed, hence it doesn't measure the complete process. How do I improve it ?
b. On my machine it takes 30-35 seconds until the panel is completely drawn. A similar swing application takes on 4 seconds. It may suggest that there is something fundamentally wrong with my javafx implementation.
Your
Task
invokesdrawTriangle()
in the background to update aCanvas
. The associatedGraphicsContext
requires that "Once aCanvas
node is attached to a scene, it must be modified on the JavaFX Application Thread." Your deeply recursive call blocks the JavaFX Application Thread, preventing a timely screen update. In contrast, your platform's implementation ofSystem.out.println()
may allow it to report in a timely way. The timing disparity is seen even without aTask
at all.Happily for
Canvas
, "If it is not attached to any scene, then it can be modified by any thread, as long as it is only used from one thread at a time." One approach might be suggested in A Task Which Returns Partial Results. Create a notionalTask<Image>
that updates a detachedCanvas
in the background. Periodically, perhaps at each level of recursion, copy theCanvas
and publish a snapshot viaupdateValue()
. The enclosingPane
can listen to the task'svalue
property and update an enclosedCanvas
viadrawImage()
without blocking the JavaFX Application Thread.Sadly, snapshot "Throws
IllegalStateException
if this method is called on a thread other than the JavaFX Application Thread."In the alternative shown below,
CanvasTask
extendsTask<Canvas>
and publishes a newCanvas
on each iteration of a loop. The enclosingCanvasTaskTest
listens to thevalue
property and replaces the previousCanvas
each time a new one arrives. The example below displays a series of fractal trees of increasing depth and the time needed to compose each. Note that in aGraphicsContext
, "Each call pushes the necessary parameters onto the buffer where they will be later rendered onto the image of theCanvas
node by the rendering thread at the end of a pulse." This allows JavaFX to leverage a platform's rendering pipeline, but it may impose an additional overhead for a large number of strokes. In practice, tens of thousands of strokes slow rendering imperceptibly, while millions of overlapping strokes may be superfluous.