JavaFX getting single resize events
Asked Answered
N

3

6

This question is rather simple: Is it possible to receive resize events that only trigger once, even if width and height change at the same time?

I have an application that calculates an image in the size of the window pixel per pixel. When the window resizes the image is being calculated again. The problem is, that when listening to the widthProperty() and heightProperty() there will always be two events that fire, even if width and height changed in the same loop cycle. This results in one redundant calculation. Is there a way to listen for resizes once per update?

A simple example:

import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        Group root = new Group();

        primaryStage.setScene(new Scene(root));
        primaryStage.show();

        primaryStage.setWidth(800);
        primaryStage.setHeight(800);

        primaryStage.widthProperty().addListener((observable, oldValue, newValue) ->
                System.out.println("old: (" + oldValue + ", " + primaryStage.getHeight() + "); " 
                                 + "new: (" + newValue + ", " + primaryStage.getHeight() + ")")
        );

        primaryStage.heightProperty().addListener((observable, oldValue, newValue) ->
                System.out.println("old: (" + primaryStage.getWidth() + ", " + oldValue + "); " 
                                 + "new: (" + primaryStage.getWidth() + ", " + newValue + ")")
        );

        primaryStage.setWidth(400);
        primaryStage.setHeight(400);
    }
}

This prints:

old: (800.0, 800.0); new: (400.0, 800.0)
old: (400.0, 800.0); new: (400.0, 400.0)

But I want this as an output only:

old: (800.0, 800.0); new: (400.0, 400.0)
Negate answered 27/3, 2018 at 12:8 Comment(1)
new: (400.0, 800.0) becames old: (400.0, 800.0) and the new is already set to new: (400.0, 400.0) .idea use externe variable to snapchat the first attemp.Bromidic
W
6

There's no nice way to do this. You can effect something similar with a bit of a hack using Platform.runLater(). This works because both changes (width and height) are triggered from the same event, which is processed in its entirety before a Platform.runLater(...) can be triggered. Setting the variable to null and ignoring changes if it is not null ensures multiple changes from the same event are coalesced into a single call to Platform.runLater().

import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class Main extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        Group root = new Group();

        primaryStage.setScene(new Scene(root));
        primaryStage.show();

        primaryStage.setWidth(800);
        primaryStage.setHeight(800);

        ChangeListener<Number> listener = new ChangeListener<Number>() {
            private Point2D stageSize = null ;
            private Point2D previousStageSize = new Point2D(primaryStage.getWidth(), primaryStage.getHeight());
            @Override
            public void changed(ObservableValue<? extends Number> arg0, Number arg1, Number arg2) {
                if (stageSize == null) {
                    Platform.runLater(() -> {
                        System.out.printf("Old: (%.1f, %.1f); new: (%.1f, %.1f)%n", 
                                previousStageSize.getX(), previousStageSize.getY(), 
                                stageSize.getX(), stageSize.getY());
                        previousStageSize = stageSize;
                        stageSize = null;
                    });
                }
                stageSize = new Point2D(primaryStage.getWidth(), primaryStage.getHeight());                
            }


        };

        primaryStage.widthProperty().addListener(listener);
        primaryStage.heightProperty().addListener(listener);


        primaryStage.setWidth(400);
        primaryStage.setHeight(400);
    }
}

While this is arguably a bit of a hack, processing changes only when there are no other changes pending might actually be exactly what you are looking for in this use case.

Wasserman answered 27/3, 2018 at 12:29 Comment(3)
Thank you this is probably the most elegant solution. I wish the developers of JavaFX had thought about use cases like mine. Maybe with the decoupling from the JDK in Java 11 such feature could be implemented more easily.Negate
@Negate Have a look at ReactFX, which has lots of functionality similar to the functionality you are asking for.Wasserman
That looks promising! It even addresses this use case.Negate
V
2

Not with a listener.

Even adding the listener to the layoutBounds property won't help.

The only workaround I know about would be overriding the layoutChildren method of the root node, which is invoked during layout after assigning the size to the Parent:

Region root = new Region() {

    @Override
    protected void layoutChildren() {
        super.layoutChildren(); 
        System.out.println("new: (" + getWidth() + ", " + getHeight() + ");");
    }

};

Note: JavaFX 9 allows you to add a postLayoutPulseListener to the scene but this may trigger even if the size does not change:

scene.addPostLayoutPulseListener(() -> {
    System.out.println("new: (" + primaryStage.getWidth() + ", " + primaryStage.getHeight() + ")");
});
Varini answered 27/3, 2018 at 12:29 Comment(0)
C
1

This appears to work nicely, and I hope it’s self-explanatory.

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;

import java.util.ArrayList;
import java.util.List;

class MyPane extends Pane {

  List<Runnable> batch = new ArrayList<>();
  double x;
  double y;

  MyPane() {
    setOnMouseClicked(e -> {
      if (!e.isAltDown  ())  setTranslateX(getTranslateX() + 1);
      if (!e.isShiftDown())  setTranslateY(getTranslateY() + 2);
    });
    translateXProperty().addListener((observableValue, before, after) -> {
      x = (double) after;
      System.out.println("x = " + after);
      runLaterInBatch(() -> System.out.printf("x changed.  x %3g = y %3g\n", x, y));
    });
    translateYProperty().addListener((observableValue, before, after) -> {
      y = (double) after;
      System.out.println("y = " + after);
      runLaterInBatch(() -> System.out.printf("y changed.  x %3g = y %3g\n", x, y));
    });
  }

  // Later, run only the last in the batch.
  void runLaterInBatch(Runnable r) {
    batch.add(r);
    Platform.runLater(() -> {
      if (batch.isEmpty())  return;
      System.out.printf("batch count: %d\n", batch.size());
      batch.get(batch.size() - 1).run();
      batch.clear();
      System.out.println();
    });
  }
}


public class RunLaterInBatchApp extends Application {

  @Override
  public void start  (Stage stage)  {
    var myPane = new MyPane();
    var scene  = new Scene(myPane, 400, 300);
    stage.setScene(scene);
    stage.show();
  }

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

// JavaFX getting single resize events
// https://mcmap.net/q/1689239/-javafx-getting-single-resize-events
//
// Output after Shift-Click, Alt-Click, Click:
//
// x = 1.0
// batch count: 1
// x changed.  x 1.00000 = y 0.00000
//
// y = 2.0
// batch count: 1
// y changed.  x 1.00000 = y 2.00000
//
// x = 2.0
// y = 4.0
// batch count: 2
// y changed.  x 2.00000 = y 4.00000
Cartage answered 9/6, 2021 at 0:57 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.