【问题标题】:Timing JavaFX Canvas Application定时 JavaFX 画布应用程序
【发布时间】:2017-05-18 14:18:35
【问题描述】:

为了练习 JavaFX,我构建了一个简单的应用程序,可以绘制 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");
    }
}


输出如预期:

我遇到的问题:

一个。 updateGraphics() 的时间打印输出显示 long 在三角形绘制完成之前(在我的机器上为 8 秒),因此它不能测量完整的过程。我该如何改进它?

b.在我的机器上,面板完全绘制需要 30-35 秒。类似的摇摆应用程序需要 4 秒。这可能表明我的 javafx 实现存在根本性错误。

【问题讨论】:

    标签: performance swing javafx graphics


    【解决方案1】:

    您的Task 在后台调用drawTriangle() 以更新Canvas。关联的GraphicsContext 要求“一旦将Canvas 节点附加到场景,就必须在JavaFX 应用程序线程上对其进行修改。”您的深度递归调用阻塞了 JavaFX 应用程序线程,从而阻止了及时的屏幕更新。相比之下,你的平台对System.out.println()的实现可能allow会及时上报。即使完全没有Task,也会看到时间差异。

    Canvas 很高兴,“如果它没有附加到任何场景,那么它可以被任何线程修改,只要它一次只能从一个线程使用。” A Task Which Returns Partial Results 中可能会建议一种方法。创建一个名义上的Task&lt;Image&gt;,在后台更新一个分离的Canvas。定期地,也许在每个递归级别,copyCanvas 并通过updateValue() 发布快照。封闭的Pane 可以监听任务的value 属性并通过drawImage() 更新封闭的Canvas,而不会阻塞JavaFX 应用程序线程。

    遗憾的是,快照“如果在 JavaFX 应用程序线程以外的线程上调用此方法,则抛出 IllegalStateException。”

    在下面显示的替代方案中,CanvasTask 扩展Task&lt;Canvas&gt; 并在循环的每次迭代中发布一个新的Canvas。封闭的CanvasTaskTest 监听value 属性,并在每次新的到达时替换以前的Canvas。下面的示例显示了一系列深度增加的分形树以及组成每个树所需的时间。请注意,在GraphicsContext 中,“每次调用都会将必要的参数推送到缓冲区,稍后它们将在脉冲结束时由渲染线程渲染到Canvas 节点的图像上。”这使 JavaFX 可以利用平台的rendering pipeline,但它可能会为大量笔画施加额外的开销。在实践中,数以万计的笔画会在不知不觉中减慢渲染速度,而数以百万计的重叠笔画可能是多余的。

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

    【讨论】:

    • 感谢您花时间详细解释。我会研究它并尝试实施。
    • 遗憾的是,快照“如果在 JavaFX 应用程序线程以外的线程上调用此方法,则抛出 IllegalStateException。”让我们看看另一种方法。
    • 首先我按照建议应用了一个分离的“背景”画布,没有临时更新。绘制完成后,我使用快照将背景画布复制到显示的画布上。这工作正常,并且可能具有更好的整体结构。它仍然不能帮助我解决计算时间和实际时间之间的差异。
    • 我已经在上面详细说明了。
    • 我的主要目的是在测量时间和实际时间之间建立更好的相关性。这是通过调整您提出的复制画布技术来实现的。在此过程中不需要更新画布,因此我在完成后复制“背景”画布。感谢您的详细解释和回答。我很感激。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-05-15
    • 1970-01-01
    • 2021-04-10
    相关资源
    最近更新 更多