JavaFX Canvas neu zeichnen anstoßen

  • Themenstarter Themenstarter hk
  • Beginndatum Beginndatum

hk

Bekanntes Mitglied
Hallo Forum,
ich gebe mittels JavaFX in einer Canvas mehrere Figuren aus und möchte nach jeder gezeichneten Figur eine Pause von 0,5 Sekunden einlegen. Das versuche ich mit einem Thread.sleep(500) nach jeder Figur.
Das Ergebnis ist, dass die Anzeige erst nach einiger Zeit und dann mit allen Figuren erfolgt. Ich brauche also eine Möglichkeit nach dem Zeichnen jeder Figur den Anstoß zur Anzeige zu geben.
lg heinz
 
Wenn du den GUI-Thread anhaeltst (zum Beispiel mit Thread.sleep), dann wird auch die GUI angehalten. Du musst derartige Verzoegerungen entweder "later" machen, oder mit einem Timer, oder komplett von einem anderen Thread.
 
Es gibt in so gut wieder jeder GUI in Java zwei Moeglichkeiten sich auf den GUI-Thread zu synchronisieren: Sofort und Spaeter. In JavaFx waere das fuer "Spaeter" Platform.runLater(Runnable).

"Spaeter" bedeutet fuer gewoehnlich "dann wenn die GUI keine Ereignisse mehr abzuarbeiten hat". Vielleicht nicht ganz so nuetzlich in deinem Fall, jetzt wo ich so darueber nachdenke.
 
Ich habe ein Beispiel meines Problems unter Verwendung eines java.util.Timer erstellt. Dabei sollen 5 Linien zeitverzögert gezeichnet werden.
Ergebnis: Die 5 Linien werden aber auf einmal gezeichnet, dann erfolgt die Zeitverzögerung und die 5 Linien werden - wohl endlos - weitergezeichnet.
Ich ersuche um Hilfe!
Java:
public class App extends Application {
  private static GraphicsContext gc;
  private static int posX = 0;
  public static Timer timer;
  @Override
  public void start(Stage stage) {
    Canvas canvas = new Canvas(600, 400);
    Pane root = new Pane();
    root.getChildren().add(canvas);
    var scene = new Scene(root, 640, 480);
    stage.setScene(scene);
    stage.show();

    gc = canvas.getGraphicsContext2D();
    timer = new Timer();

    // Hier sollen 5 Linien zeitverzögert gezeichnet werden
    for (int i = 0; i < 5; i++) {
      timer.schedule(new TimerTask() {
        @Override
        public void run() {
          gc.strokeLine(posX * 10 + 10, 10, posX * 10 + 10, 20);
          System.out.println("Lauf: " + posX);
          posX++;
        }
      }, 1000, 1000);
//      timer.cancel();

    }
  }
 
Du kannst den GraphicsContext nicht von einem anderen Thread aus verwenden, das ist, mh, nicht unterstuetzt. Sowohl Swing als auch JavaFX haben einen GUI/Event-Thread, auf dem muss alles was die GUI machen soll, gemacht werden, ansonsten ist es sehr undefiniert was genau passiert.

Du hast also deinen Timer, und in dem musst du dich dann zurueck auf den GUI-Thread synchronisieren und dann auf dem GUI-Thread die Linie zeichnen.
 
Dann schaue dir doch mal an wie der Timer arbeitet.

Du hast eine for Schleife in der startest du mittels schedule den Timer.
Und der läuft dann endlos .

Falsch ist es erstmal den Timer in der for immer wieder neu zu starten und auch somit immer wieder einen neuen TimerTast zu erzeugen.
In der Run Methode zählst du doch deine PosX Variable hoch. Also wenn die zb 5 ist könntest du den Timer beenden.

Den Timer hast du ja als Instanzvariable also kannst du auch in dem TimerTast in der run darauf zugreifen und den Timer stoppen mit cancel()

Die for brauchst du eigentlich nicht.
 
Java:
gc = canvas.getGraphicsContext2D();
    timer = new Timer();

    // Hier sollen 5 Linien zeitverzögert gezeichnet werden
 
      timer.schedule(new TimerTask() {
        @Override
        public void run() {
          gc.strokeLine(posX * 10 + 10, 10, posX * 10 + 10, 20);
          System.out.println("Lauf: " + posX);
          posX++;
          if(posX >= 5){
               timer.cancel();
          }
        }
      }, 1000, 1000);
//      timer.cancel();

    }
  }
 
Du kannst den GraphicsContext nicht von einem anderen Thread aus verwenden, das ist, mh, nicht unterstuetzt. Sowohl Swing als auch JavaFX haben einen GUI/Event-Thread, auf dem muss alles was die GUI machen soll, gemacht werden, ansonsten ist es sehr undefiniert was genau passiert.
Dem stimme ich so nicht zu. Ich glaube du verwechselt das mit Android da gegen Bildschirmausgaben nur auf dem UI Thread.
Er bekommt ja seine Ausgabe wie er sagt. Nur das es eben nicht nach seinem Zeitplan geht, und endlos läuft.
 
Er will ja auch nicht den Context ändern nur etwas auf ihm zeichnen.

Ich habe es mal mit meinen Änderungen laufen lasse und seine Lienen kommen der Reihe nach.
 
Java:
gc = canvas.getGraphicsContext2D();
    timer = new Timer();

    // Hier sollen 5 Linien zeitverzögert gezeichnet werden
 
      timer.schedule(new TimerTask() {
        @Override
        public void run() {
          gc.strokeLine(posX * 10 + 10, 10, posX * 10 + 10, 20);
          System.out.println("Lauf: " + posX);
          posX++;
          if(posX >= 5){
               timer.cancel();
          }
        }
      }, 1000, 1000);
//      timer.cancel();

    }
  }
Ja, das funktioniert! Aber ich brauche die umgebende Schleife. Der TimerTask soll immer nur eine Linie zeichnen.
 
Du solltest mal erklären was du genau willst.

Deine For Schleife läuft im main-Thread die wartet nicht ist in Windseile fertig.

Hat also sehr schnell deinen Timer 5 mal neu gestartet. Danach läuft er bis das Programm beendet wird.
 
Du solltest mal erklären was du genau willst.

Deine For Schleife läuft im main-Thread die wartet nicht ist in Windseile fertig.

Hat also sehr schnell deinen Timer 5 mal neu gestartet. Danach läuft er bis das Programm beendet wird.
Es ist ja nur ein Beispiel wie ich schon schrieb. Das eigentliche Ziel ist es ein Labyrinth zu erstellen und die einzelnen Schritte von Feld zu Feld optisch nachvollziehbar darzustellen. Natürlich ist der Code zwischen den Wechsel der Felder zeitintensiver als das Beispiel aber sicher nicht so lange dass sich die Verzögerungen nicht überlappen würden. Ich möchte die Fortschrittsanzeige mit rund 0,5 Sekunden anzeigen.
 
Ich sehe da zwei Probleme:

a) Du scheinst nicht richtig verstanden zu haben, wie Timer funktioniert. Die Tatsache, dass Du einen Timer in einer Schleife mehrfach starten (schedule Aufruf) willst, zeigt, dass die Dokumentation ignoriert wurde. Und da wirst Du ja eine Exception bekommen haben - das wäre spätestens der Zeitpunkt, in die Dokumentation zu schauen. Dann findet sich z.B. direkt:
IllegalStateException - if task was already scheduled or cancelled, timer was cancelled, or timer thread terminated.

Also: Schau in die Dokumentation der API (z.B. Timer (Java SE 17 & JDK 17) (oracle.com) für Java 17)

b) Divide and Conquer - teile doch bitte das Problem stärker in Teilprobleme auf. Dann wird direkt vieles einfacher, denn Du hast dann viele kleine Probleme, die Du lösen kannst (und dessen Lösung du verifizieren kannst! Sprich: Testen!) Damit hat man es dann auch deutlich einfacher, ein Problem zu identifizieren und zu beschreiben. Mich irritiert, dass ich irgendwie nirgends eine Fehlerbeschreibung von Dir sehe, wenn es da um Code von Dir geht mit dem Timer. Denn ich hätte da z.B. eine Exception erwartet und sowas sollte beschrieben werden.

Das einfach kurz von meiner Seite als Input für Dein weiteres Vorgehen.
 
Ich sehe da zwei Probleme:

a) Du scheinst nicht richtig verstanden zu haben, wie Timer funktioniert. Die Tatsache, dass Du einen Timer in einer Schleife mehrfach starten (schedule Aufruf) willst, zeigt, dass die Dokumentation ignoriert wurde. Und da wirst Du ja eine Exception bekommen haben - das wäre spätestens der Zeitpunkt, in die Dokumentation zu schauen. Dann findet sich z.B. direkt:


Also: Schau in die Dokumentation der API (z.B. Timer (Java SE 17 & JDK 17) (oracle.com) für Java 17)

b) Divide and Conquer - teile doch bitte das Problem stärker in Teilprobleme auf. Dann wird direkt vieles einfacher, denn Du hast dann viele kleine Probleme, die Du lösen kannst (und dessen Lösung du verifizieren kannst! Sprich: Testen!) Damit hat man es dann auch deutlich einfacher, ein Problem zu identifizieren und zu beschreiben. Mich irritiert, dass ich irgendwie nirgends eine Fehlerbeschreibung von Dir sehe, wenn es da um Code von Dir geht mit dem Timer. Denn ich hätte da z.B. eine Exception erwartet und sowas sollte beschrieben werden.

Das einfach kurz von meiner Seite als Input für Dein weiteres Vorgehen.
Ich weiß es. Aber mein Englisch ist nicht sehr gut und die Übersetzungen sind nicht sehr hilfreich.
 
Ich habe nun eine Version mit "Timeline" erstellt und diese funktioniert einwandfrei. Ob sie programmiertechnisch "sauber" ist weiß ich nicht.
Java:
public class App extends Application {

  private static GraphicsContext gc;
  private int count;
  @Override
  public void start(Stage stage) {
    Canvas canvas = new Canvas(600, 400);
    Pane root = new Pane();
    root.getChildren().add(canvas);
    var scene = new Scene(root, 640, 480);
    stage.setScene(scene);
    stage.show();
    gc = canvas.getGraphicsContext2D();
    count = 1;
    loop();
  }

  // Zeitverzögerte Ausgabe
  private void loop() {
    Timeline timeline = new Timeline(new KeyFrame(Duration.seconds(0.5)));
    timeline.setCycleCount(1);
    // Erst weitermachen wenn Zyklus beendet
    timeline.setOnFinished((event) -> {
      gc.strokeLine(count * 10, 10, count * 10, 20);
      System.out.println("Lauf: " + count);
      if (count++ < 5) {
        loop();
      }
    }); //end setOnFinished -----------------------
    timeline.play();
  }
Danke vorerst an alle die mir geholfen haben!
 
Sei mir nicht böse, aber der Code ist irgendwie dubios. Timeline ist eine interessante Klasse, aber sie dient eigentlich zu etwas anderen Dingen wie z.B. Transformationen oder so. Du nutzt Timeline aber ganz offensichtlich schlicht als extrem dummen Timer, indem Du die TimeLine eigentlich nichts machen lässt außer zu warten und Du dann nach Abschluss etwas machst. 🙂

Was Du brauchst ist ein Timer. (Unter JavaFX macht es dann ggf. Sinn, die Klasse AnimationTimer zu nutzen.)

Der Timer ruft dann in gewissen Abständen immer wieder etwas auf. Wenn Du also etwas 5 Mal machen willst, dann musst da da keinen Timer 5 Mal starten sondern den Timer einfach 5 Mal durchlaufen lassen. Diese Anzahl kann man indirekt vorgeben, denn es gibt nicht nur den Abstand sondern auch die Dauer. Also wenn Du jede Sekunde etwas machen willst und das 5 Mal, dann muss der Timer 5 Sekunden laufen.

Bei Deinem Code:
Java:
    for (int i = 0; i < 5; i++) {
      timer.schedule(new TimerTask() {
        @Override
        public void run() {
          gc.strokeLine(posX * 10 + 10, 10, posX * 10 + 10, 20);
          System.out.println("Lauf: " + posX);
          posX++;
        }
      }, 1000, 1000);
//      timer.cancel();

    }
muss also die Schleife weg. Und damit es 5 Mal läuft, passt Du die Dauer des Timers an auf 5000:

Java:
      timer.schedule(new TimerTask() {
        @Override
        public void run() {
          gc.strokeLine(posX * 10 + 10, 10, posX * 10 + 10, 20);
          System.out.println("Lauf: " + posX);
          posX++;
        }
      }, 1000, 5000);

Und die , 1000, 5000 kannst Du natürlich über Variablen machen:
Java:
int pause = 1000;
int count = 5;

// , pause, count * pause);
Das macht es dann deutlich lesbarer und einfacher anzupassen.
 
Hallo hast du dir diesen Satz aus der doku an gesehen

Warning : A running Timeline is being referenced from the FX runtime. Infinite Timeline might result in a memory leak if not stopped properly. All the objects with animated properties would not be garbage collected.
 
der letzte wert beim Timer ist die Periodendauer nicht di Anzahl der Wiederholungen.
Ja, da hatte ich mich tatsächlich vertan. Danke für den Hinweis.

Und das Problem mit der Veränderung außerhalb des UI Threads ist hier natürlich auch noch nicht behandelt.

Aber der wichtige Punkt ist halt, dass der Timer mehrere Aufrufe nacheinander macht und daher keine Schleife mehr notwendig ist.
 
beim

der letzte wert beim Timer ist die Periodendauer nicht di Anzahl der Wiederholungen.
bei 5000 würde er 5 sec waten bis er wieder die run methode auftuft.

Ich glaube ich habe verstanden! Ich wollte immer die Verarbeitung in einer Schleife durchführen und dann mit dem Timer ausgeben. Nun weiß ich dass ich die Verarbeitung inkl. Ausgabe Timer-gesteuert durchführen muss. Also zurück zum Timer.
Danke!
 
Das sieht nun so aus:
Code:
public class App extends Application {

  private static GraphicsContext gc;
  private int posX = 1;
  private Timer timer;

  @Override
  public void start(Stage stage) {
    Canvas canvas = new Canvas(600, 400);
    Pane root = new Pane();
    root.getChildren().add(canvas);
    var scene = new Scene(root, 640, 480);
    stage.setScene(scene);
    stage.show();
    gc = canvas.getGraphicsContext2D();
    int posX = 1;
    timer = new Timer();
    timer.schedule(new TimerTask() {
      @Override
      public void run() {
        loop();
      }
    }, 0, 1000);
  }
  private void loop() {
    gc.strokeLine(posX * 10, 10, posX * 10, 20);
    System.out.println("Lauf: " + posX);
    posX++;
    if(posX > 5) timer.cancel();
  }
Danke nochmals!
 
Und das Problem mit der Veränderung außerhalb des UI Threads ist hier natürlich auch noch nicht behandelt.

Um die Bildschirm Ausgabe sicher auf den FX-Thread auszuführen würde ich es vielleicht so machen.
Mit dem schon erwähnten runLater Runnable.

Java:
   private void loop() {
            Platform.runLater( () -> {
                        gc.strokeLine(posX * 10, 10, posX * 10, 20);
                    }
            );
            System.out.println("Lauf: " + posX);
            posX++;
            if(posX > 5) timer.cancel();
        }
 
Um die Bildschirm Ausgabe sicher auf den FX-Thread auszuführen würde ich es vielleicht so machen.
Mit dem schon erwähnten runLater Runnable.

Java:
   private void loop() {
            Platform.runLater( () -> {
                        gc.strokeLine(posX * 10, 10, posX * 10, 20);
                    }
            );
            System.out.println("Lauf: " + posX);
            posX++;
            if(posX > 5) timer.cancel();
        }
Was ist dann sicherer?
 
Ich würde den bereits in #8 erwähnten AnimationTimer benutzen. Dann sollte die Ausführung bereits im UI Thread sein.


Aber Das sind dann Feinheiten.
The class AnimationTimer allows to create a timer, that is called in each frame while it is active. An extending class has to override the method handle(long) which will be called in every frame. The methods start() and stop() allow to start and stop the timer.

Was ist mit frame gemeint?
Was ist eine erweiterte Klasse?
 
Der AnimationTimer hat in der handle Methode daher einen Zeitstempel in Nanosekunden. Hier muss man tatsächlich eigenständig prüfen, ob die gewünschte Zeit verstrichen ist.

Dazu merkst Du Dir einen Zeitstempel, wann es das letzte Mal gelaufen ist um dann halt über die Differenz zu gehen.

Das sieht dann erst einmal komplexer aus, aber man hat den Vorteil, dass man im UI Thread ist und es keine weiteren Verzögerungen durch runLater Aufrufe gibt.

Und da Du eine fortlaufende Zeit hast, kannst Du sehr genau arbeiten. Du hast also einen Abstand von x ns und kannst daher prüfen, ob parameter - lastRun >= x ist. Und wenn du vermeiden willst, dass es eine immer größere Ungenauigkeit gibt, kannst Du lastRun nicht auf den Parameter setzen sondern auf lastRun + x.

Also angenommen, alle 500_000 ns soll etwas ausgeführt werden. Dann hast Du evtl. eine Ausführung nach 517_000 ns, also 17 ms "zu spät". Diese Verspätung würde sich immer weiter addieren, wenn Du 500_000 ns nach der letzten Ausführung nehmen würdest. Ist das so bereits verständlich?

Generell ist es so, dass es immer viele Lösungsideen gibt. Die Lösung mit dem Timer ist schon in Ordnung und Du kannst bei der Lösung bleiben. Wobei es nie schadet, da auch etwas mit unterschiedlichen Klassen zu spielen um diese kennen zu lernen. Aber wichtig ist halt das Verständnis: Deine Lösung ist eine valide Lösung und es spricht nicht wirklich etwas gegen genau diese Lösung!
 
Der AnimationTimer hat in der handle Methode daher einen Zeitstempel in Nanosekunden. Hier muss man tatsächlich eigenständig prüfen, ob die gewünschte Zeit verstrichen ist.

Dazu merkst Du Dir einen Zeitstempel, wann es das letzte Mal gelaufen ist um dann halt über die Differenz zu gehen.

Das sieht dann erst einmal komplexer aus, aber man hat den Vorteil, dass man im UI Thread ist und es keine weiteren Verzögerungen durch runLater Aufrufe gibt.

Und da Du eine fortlaufende Zeit hast, kannst Du sehr genau arbeiten. Du hast also einen Abstand von x ns und kannst daher prüfen, ob parameter - lastRun >= x ist. Und wenn du vermeiden willst, dass es eine immer größere Ungenauigkeit gibt, kannst Du lastRun nicht auf den Parameter setzen sondern auf lastRun + x.

Also angenommen, alle 500_000 ns soll etwas ausgeführt werden. Dann hast Du evtl. eine Ausführung nach 517_000 ns, also 17 ms "zu spät". Diese Verspätung würde sich immer weiter addieren, wenn Du 500_000 ns nach der letzten Ausführung nehmen würdest. Ist das so bereits verständlich?

Generell ist es so, dass es immer viele Lösungsideen gibt. Die Lösung mit dem Timer ist schon in Ordnung und Du kannst bei der Lösung bleiben. Wobei es nie schadet, da auch etwas mit unterschiedlichen Klassen zu spielen um diese kennen zu lernen. Aber wichtig ist halt das Verständnis: Deine Lösung ist eine valide Lösung und es spricht nicht wirklich etwas gegen genau diese Lösung!
Verstanden, danke!
 

Zurück
Oben