2017-05-18 6 views
1

I um JavaFX zu üben, baute ich eine einfache App, die Sierpinski Triangles zeichnet.Timing JavaFX Canvas-Anwendung

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"); 
    } 
} 


Der Ausgang ist wie erwartet: triangles

Die Probleme, die ich habe:

ein. Der Zeitausdruck bei updateGraphics() zeigt lang vor (8 Sekunden auf meiner Maschine) ist das Zeichnen der Dreiecke abgeschlossen, daher misst es nicht den kompletten Prozess. Wie verbessere ich es?

b. Auf meinem Gerät dauert es 30-35 Sekunden, bis das Panel vollständig gezeichnet ist. Eine ähnliche Swing-Anwendung dauert 4 Sekunden. Es könnte bedeuten, dass etwas grundsätzlich falsch mit meiner Javafx-Implementierung ist.

Antwort

3

Ihre Task ruft drawTriangle() im Hintergrund auf, um eine Canvas zu aktualisieren. Die zugehörige GraphicsContext erfordert, dass "Sobald ein Canvas Knoten an eine Szene angehängt ist, muss es auf dem JavaFX Application Thread geändert werden." Ihr tief rekursiver Aufruf blockiert den JavaFX-Anwendungs-Thread und verhindert so eine rechtzeitige Bildschirmaktualisierung. Im Gegensatz dazu System.out.println() Ihre Plattform allow es kann, um rechtzeitig zu berichten. Die zeitliche Disparität ist sogar ohne eine Task überhaupt zu sehen.

Glücklicherweise für Canvas, "Wenn es zu keiner Szene hinzugefügt wird, kann es von jedem Thread geändert werden, solange es nur von einem Thread zu einem Zeitpunkt verwendet wird." Ein Ansatz könnte in A Task Which Returns Partial Results vorgeschlagen werden. Erstellen Sie einen fiktiven Task<Image>, der eine gelöste Canvas im Hintergrund aktualisiert. Periodisch, vielleicht auf jeder Ebene der Rekursion, copy die Canvas und veröffentlichen Sie einen Snapshot über updateValue(). Der einschließende Pane kann die value-Eigenschaft der Aufgabe anhören und eine beiliegende Canvas über drawImage() aktualisieren, ohne den JavaFX-Anwendungs-Thread zu blockieren.

Leider Snapshot "löst IllegalStateException, wenn diese Methode für einen anderen Thread als JavaFX Application Thread aufgerufen wird."

In der alternativen unten gezeigt, erstreckt sich CanvasTaskTask<Canvas> und veröffentlicht eine neue Canvas bei jeder Iteration einer Schleife. Die umgebende CanvasTaskTest hört auf die value Eigenschaft und ersetzt die vorherige Canvas jedes Mal, wenn ein neues ankommt. Das folgende Beispiel zeigt eine Reihe von fraktalen Bäumen mit zunehmender Tiefe und die Zeit, die benötigt wird, um sie zu komponieren. Beachten Sie, dass in einem GraphicsContext, "jeder Aufruf schiebt die notwendigen Parameter auf den Puffer, wo sie später auf das Bild des Canvas-Knoten durch den Render-Thread am Ende eines Impulses gerendert werden." Dadurch kann JavaFX die Plattform rendering pipeline der Plattform nutzen, es kann jedoch für eine große Anzahl von Strichen einen zusätzlichen Aufwand verursachen. In der Praxis werden Zehntausende von Strichen langsam unkenntlich wiedergegeben, während Millionen sich überlappender Striche überflüssig sein können.

image

import javafx.application.Application; 
import javafx.beans.value.ObservableValue; 
import javafx.concurrent.Task; 
import javafx.scene.Scene; 
import javafx.scene.canvas.Canvas; 
import javafx.scene.canvas.GraphicsContext; 
import javafx.scene.layout.StackPane; 
import javafx.stage.Stage; 

/** 
* @see https://stackoverflow.com/a/44056730/230513 
*/ 
public class CanvasTaskTest extends Application { 

    private static final int W = 800; 
    private static final int H = 600; 

    @Override 
    public void start(Stage stage) { 
     stage.setTitle("CanvasTaskTest"); 
     StackPane root = new StackPane(); 
     Canvas canvas = new Canvas(W, H); 
     root.getChildren().add(canvas); 
     Scene scene = new Scene(root); 
     stage.setScene(scene); 
     stage.show(); 
     CanvasTask task = new CanvasTask(); 
     task.valueProperty().addListener((ObservableValue<? extends Canvas> observable, Canvas oldValue, Canvas newValue) -> { 
      root.getChildren().remove(oldValue); 
      root.getChildren().add(newValue); 
     }); 
     Thread thread = new Thread(task); 
     thread.setDaemon(true); 
     thread.start(); 
    } 

    private static class CanvasTask extends Task<Canvas> { 

     private int strokeCount; 

     @Override 
     protected Canvas call() throws Exception { 
      Canvas canvas = null; 
      for (int i = 1; i < 15; i++) { 
       canvas = new Canvas(W, H); 
       GraphicsContext gc = canvas.getGraphicsContext2D(); 
       strokeCount = 0; 
       long start = System.nanoTime(); 
       drawTree(gc, W/2, H - 50, -Math.PI/2, i); 
       double dt = (System.nanoTime() - start)/1_000d; 
       gc.fillText("Depth: " + i 
        + "; Strokes: " + strokeCount 
        + "; Time : " + String.format("%1$07.1f", dt) + " µs", 8, H - 8); 
       Thread.sleep(200); // simulate rendering latency 
       updateValue(canvas); 
      } 
      return canvas; 
     } 

     private void drawTree(GraphicsContext gc, int x1, int y1, double angle, int depth) { 
      if (depth == 0) { 
       return; 
      } 
      int x2 = x1 + (int) (Math.cos(angle) * depth * 5); 
      int y2 = y1 + (int) (Math.sin(angle) * depth * 5); 
      gc.strokeLine(x1, y1, x2, y2); 
      strokeCount++; 
      drawTree(gc, x2, y2, angle - Math.PI/8, depth - 1); 
      drawTree(gc, x2, y2, angle + Math.PI/8, depth - 1); 
     } 
    } 

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

} 
+0

Danke, dass Sie sich die Zeit nehmen sie im Detail zu erklären. Ich werde es studieren und versuchen zu implementieren. – c0der

+0

Leider löst der Snapshot " ' IllegalStateException "aus, wenn diese Methode für einen anderen Thread als den JavaFX-Anwendungsthread aufgerufen wird." Sehen wir uns einen alternativen Ansatz an. – trashgod

+0

Zunächst habe ich wie gesagt ohne Hintergrundupdates eine freistehende "Hintergrund" -Leinwand aufgetragen. Wenn das Zeichnen fertig ist, kopiere ich den Hintergrundbereich auf die angezeigte Leinwand. Das funktioniert gut und vielleicht hat es eine bessere Gesamtstruktur. Es hilft mir immer noch nicht, den Unterschied zwischen der berechneten Zeit und der tatsächlichen Zeit zu lösen. – c0der