JavaFX 获取单个调整大小事件

JavaFX getting single resize events

这个问题很简单:是否可以接收只触发一次的调整大小事件,即使 widthheight 同时发生变化?

我有一个应用程序可以计算每个像素 window 像素大小的图像。当 window 调整图像大小时,将再次计算图像。问题是,当监听 widthProperty()heightProperty() 时,总会触发两个事件,即使 widthheight 在同一个循环周期中发生变化。这导致一次冗余计算。有没有办法在每次更新时监听一次调整大小?

一个简单的例子:

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

这会打印:

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

但我只想将其作为输出:

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

没有听众。

即使将侦听器添加到 layoutBounds 属性 也无济于事。

我知道的唯一解决方法是覆盖根节点的 layoutChildren 方法,该方法在将大小分配给 Parent:

之后在布局期间调用
Region root = new Region() {

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

};

注意:JavaFX 9 允许您向场景添加 postLayoutPulseListener 但这可能会触发即使大小不变:

scene.addPostLayoutPulseListener(() -> {
    System.out.println("new: (" + primaryStage.getWidth() + ", " + primaryStage.getHeight() + ")");
});

没有好的方法可以做到这一点。您可以使用 Platform.runLater() 通过一些 hack 来实现类似的效果。这是可行的,因为两个更改(宽度和高度)都是从同一事件触发的,该事件在触发 Platform.runLater(...) 之前被完整处理。将变量设置为 null 并忽略更改(如果它不为 null)可确保将来自同一事件的多个更改合并到对 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);
    }
}

虽然这可以说是有点 hack,但仅当没有其他未决更改时才处理更改可能正是您在此用例中所寻找的。

这似乎工作得很好,我希望它是不言自明的。

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
// 
//
// 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