如何让 JavaFX 控件在初始化后立即实现它的 localBounds?

How do I get a JavaFX control to realize it's localBounds immediately upon initialization?

我已经创建了自己的 Marquee class / 控件,它在加载前几秒后工作正常。但是,一旦应用程序首次加载,选取框就会认为它的本地边界是 maxWidth: 0.0 minWidth: 2.0。下面是 Marquee class 代码和我用来在我的测试应用程序中加载它的代码。

选取框 Class:

public class Marquee extends HBox {
        /**
         * Add a node to the Marquee control
         * @param node - a node to add 
         */
        public void add(Node node) {
            getChildren().add(node);
        }

        /**
         * Add a list of nodes to the Marquee control
         * @param observable list - an observable list to add
         */
        public void addAll(ObservableList<Node> list) {
            getChildren().addAll(list);
        }

        /**
         * Default Constructor: Initializes the Marquee Object with default settings:
         * Empty Array List of Nodes, initial delay, Direction.LEFT, Duration.seconds(10), Interpolator.LINEAR, 10)
         */
        public Marquee() 
        {
            this(FXCollections.observableArrayList(new ArrayList<Node>()), Duration.seconds(3), Direction.LEFT, Duration.seconds(10), Interpolator.LINEAR, 10.0);
        }

        /**
         * Constructor: Initializes the Marquee Object with default settings
         * @param observable list  
         */
        public Marquee(ObservableList<Node> nodes) 
        {
            this(nodes, Duration.seconds(3), Direction.LEFT, Duration.seconds(10), Interpolator.LINEAR, 10.0);
        }

        /**
         * Constructor: Initializes the Marquee Object with default settings
         * @param observable list  
         * @param duration - usually in seconds i.e. Duration.seconds(10)
         */
        public Marquee(ObservableList<Node> nodes, Duration duration) {
            this(nodes, Duration.seconds(3), Direction.LEFT, duration, Interpolator.LINEAR, 10.0);
        }

        /**
         * Constructor: Initializes the Marquee Object with default settings
         * @param observable list 
         * @param direction - an enum, i.e Direction.LEFT or Direction.RIGHT 
         * @param duration - usually in seconds i.e. Duration.seconds(10)
         */
        public Marquee(ObservableList<Node> nodes, Direction direction, Duration duration) {
            this(nodes, Duration.seconds(3), direction, duration, Interpolator.LINEAR, 10.0);
        }

        /**
         * Constructor: Initializes the Marquee Object with default settings
         * @param observable list 
         * @param duration - usually in seconds i.e. Duration.seconds(10)
         * @param interpolator - effects the translation behavior, i.e 
         * Interpolator.EASE_BOTH, or EASE_LINEAR
         */
        public Marquee(ObservableList<Node> nodes, Duration duration, Interpolator interpolator) 
        {
            this(nodes, Duration.seconds(3), Direction.LEFT, duration, interpolator, 10.0);
        }

        /**
         * Constructor: Initializes the Marquee Object with default settings:
         * @param observable list 
         * @param initialDelay - the amount of time before the marquee will begin scrolling
         * after the application has loaded
         * @param direction - an enum, i.e Direction.LEFT or Direction.RIGHT 
         * @param duration - usually in seconds i.e. Duration.seconds(10)
         * @param interpolator - effects the translation behavior, i.e 
         * Interpolator.EASE_BOTH, or EASE_LINEAR
         */
        public Marquee(ObservableList<Node> list, Duration initialDelay, Direction direction, Duration duration, Interpolator interpolator) {

            this(list, initialDelay, direction, duration, interpolator, 10.0);
        }

        /**
         * Preferred Constructor: Initializes the Marquee Object with your preferred settings
         * 
         * @param observable list 
         * @param initialDelay - the amount of time before the marquee will begin scrolling
         * after the application has loaded
         * @param direction - an enum, i.e Direction.LEFT or Direction.RIGHT 
         * @param duration - usually in seconds i.e. Duration.seconds(10)
         * @param interpolator - effects the translation behavior, i.e 
         * Interpolator.EASE_BOTH, or EASE_LINEAR
         * @param nodeSpacing - a double value that determines how far apart 
         * each element in the marquee will be placed from one another
         */
        public Marquee(ObservableList<Node> list, Duration initialDelay, Direction direction, Duration duration, Interpolator interpolator, double nodeSpacing) {
            super();
            getChildren().addAll(list);
            setSpacing(nodeSpacing);
            delay = initialDelay;
            this.direction = direction;
            this.duration = duration;
            this.interpolator = interpolator;
        }

        public enum Direction {
            LEFT, RIGHT
        };

        private Direction direction;
        private TranslateTransition animation;
        private Duration duration;

        /**
         * This begins the animation of the Marquee. By default this method 
         * calculates the width of the Marquee's parent and uses that as its 
         * start point. When the nodes inside the Marquee have reached the outer
         * bounds of its parent the Marquee will stop and reset. Note: If the
         * application is resized, the animation will need to be stopped and
         * restarted. The Marquee will recalculate its translation requirements each
         * cycle so if the user resizes it's parent, the Marquee will conform.
         * 
         * @param duration
         *            the amount of time the translation should take
         */
        public void animate() {

            animation = new TranslateTransition(duration, this);
            double maxWidth = getBoundsInLocal().getMaxX();
            double minWidth = getBoundsInLocal().getMinX() - getContentsWidth();

            switch (direction) {
            case LEFT:
                animation.setToX(minWidth);
                animation.setFromX(maxWidth);
                break;
            case RIGHT:
                animation.setToX(maxWidth);
                animation.setFromX(minWidth);
                break;
            default:
                animation.setToX(minWidth);
                animation.setFromX(maxWidth);
                break;
            }

            animation.setCycleCount(1);
            animation.setInterpolator(getInterpolator());
            animation.setDelay(delay);
            animation.playFromStart();

            animation.setOnFinished(new EventHandler<ActionEvent>() {
                @Override
                public void handle(ActionEvent event)
                {
                    stopAnimation();
                    recycleAnimation();
                }

            });
        }

        private Duration delay;
        public Duration getDelay() {
            return delay;
        }

        public void setDelay(Duration delay) {
            this.delay = delay;
        }

        private Interpolator interpolator;

        /**
         * How the Marquee transitions its content into and out of FOV.
         * Options are: 
         * DISCRETE (DO NOT USE), EASE_IN, EASE_OUT, EASE_BOTH, and LINEAR (DEFAULT).
         * Any change to the Interpolator will take affect after the current cycle
         * ends.
         * Suggested Usage: setInterpolator(Interpolator.LINEAR)
         */
        public void setInterpolator(Interpolator interpolator)
        {
            this.interpolator = interpolator;
        }

        /**
         * The Interpolator of the Marquee.
         * @return Interpolator
         */
        public Interpolator getInterpolator()
        {
            return interpolator;
        }

        public void recycleAnimation()
        {
            setDelay(Duration.ZERO);
            animate();
        }

        /**
         * Stop animation of Marquee
         */
        public void stopAnimation() {
            animation.stop();
        }

        /**
         * Set the default spacing between nodes in the Marquee Default is set to
         * 5.0
         */
        public void setNodeSpacing(double value) {
            setSpacing(value);
        }

        /**
         * Get the current spacing between nodes in the Marquee
         * 
         * @return double
         */
        public double getNodeSpacing() {
            return getSpacing();
        }

        private int getContentsWidth()
        {
            int width = 0;

            for(Node node : getChildrenUnmodifiable())
            {
                width += node.boundsInLocalProperty().get().getWidth();
            }

            return width;
        }

    }

和我的主要 Class

public class Main extends Application {
    @Override
    public void start(Stage primaryStage) {
        try {

            BorderPane root = new BorderPane();

            ObservableList<Node> labels = FXCollections.observableArrayList();
            labels.add(new Label("Test Label 1"));
            labels.add(new Label("Test Label 2"));

            Marquee marqueeLeft = new Marquee(labels, Duration.ZERO, Direction.LEFT, Duration.seconds(10), Interpolator.EASE_BOTH, 10.0);
            root.setTop(marqueeLeft);

            final ObservableList<Node> labels2 = FXCollections.observableArrayList();
            labels2.add(new Label("Test Label 3"));
            labels2.add(new Label("Test Label 4"));
            final Marquee marqueeRight = new Marquee(labels2, Duration.ZERO, Direction.RIGHT, Duration.seconds(10), Interpolator.EASE_BOTH, 10.0);
            root.setBottom(marqueeRight);
            marqueeLeft.animate();
            marqueeRight.animate();

            Button button = new Button();
            button.setOnAction(new EventHandler<ActionEvent>() {
                @Override
                public void handle(ActionEvent event) {
                    System.out.println("Workin");
                    marqueeRight.add(new Label("Test Add Label"));
                }
            });

            root.setCenter(button);

            Scene scene = new Scene(root,600,300);
            scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
            primaryStage.setScene(scene);
            primaryStage.show();


        } catch(Exception e) {
            e.printStackTrace();
        }
    }

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

我试过等到显示舞台并尝试获取 parentBounds、localBounds 后加载选取框。它总是希望从 0.0、2.0 开始。

如有任何建议,我们将不胜感激。

在您的 Marquee class 中,为您调用 getContentWidth() 的节点设置动画,它只获取每个节点的宽度:

private int getContentsWidth(){
    int width = 0;
    for(Node node : getChildrenUnmodifiable()){
         width += node.boundsInLocalProperty().get().getWidth();
    }
    return width;
}

然后实例化选取框,并开始动画:

Marquee marqueeLeft = new Marquee(labels, Duration.ZERO, Direction.LEFT, Duration.seconds(10), Interpolator.EASE_BOTH, 10.0);
marqueeLeft.animate();

然后然后你展示舞台。

您的方法的问题就在于 getWidth():它将 return 始终为 0 直到 您显示舞台并布置节点。之后,它们将具有非零宽度。

现在,您必须等待第一个动画周期。当动画结束时,它会再次调用 animate()

public void recycleAnimation(){
    setDelay(Duration.ZERO);
    animate();
}

第二次,所有节点都在舞台上,并且有一个有效的宽度,跑马灯开始移动。

解决方法:移动动画调用即可:

Scene scene = new Scene(root,600,300);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();

marqueeLeft.animate();
marqueeRight.animate();

现在,在示例中,宽度将为 126 像素,选取框将按照您的预期开始移动。

编辑

如果在创建字幕之后显示舞台:

Scene scene = new Scene(root,600,300);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();

Marquee marqueeLeft = new Marquee(labels, Duration.ZERO, Direction.LEFT, Duration.seconds(10), Interpolator.EASE_BOTH, 10.0);
root.setTop(marqueeLeft);
marqueeLeft.animate();

这也行不通,因为对 animate() 的调用会在节点添加到场景后 立即 发生,而 JavaFX 应用程序线程不会有时间更新值。您可以阅读有关 JavaFX 架构的信息 here.

解决方案:让场景图有时间完成它的任务:

root.setTop(marqueeLeft);
root.setBottom(marqueeRight);

Platform.runLater(new Runnable() {

    @Override
    public void run() {
        marqueeLeft.animate();
        marqueeRight.animate();
    }
});

你让 JavaFX 在其标准脉冲过程中隐式处理布局传递,如 Jose Pereda 的回答中所建议的,或者你(如果你使用 Java 8+)可以 manually applyCSS trigger a layout pass

Pane parentNode = new Pane();
Scene scene = new Scene(parentNode);
Node child = new Button("XYZZY");
parentNode.getChildren().add(child);

System.out.println(button.getWidth()); // outputs 0 as width is not yet known.

parentNode.applyCss();
parentNode.layout();

System.out.println(button.getWidth()); // outputs non-0 as width is now known.