【问题标题】:JavaFX custom chart class - how to bind a node's layoutX and layoutY properties to the display positions of a NumberAxis?JavaFX 自定义图表类 - 如何将节点的 layoutX 和 layoutY 属性绑定到 NumberAxis 的显示位置?
【发布时间】:2021-12-07 17:27:13
【问题描述】:

我正在编写一个基本的Candlestick 图表类,其中烛台被创建为Regions,并通过将它们的layoutXlayoutY 值设置为相关轴的getDisplayPosition() 来绘制。

例如,要在 X 轴上绘制值为 3 的烛台,我这样做:

candlestick.setLayoutX(xAxis.getDisplayPosition(3));

当舞台调整大小或放大或缩小轴时,必须重置烛台的布局值,以便图表正确呈现。我目前正在通过ChangeListeners 处理此事件以调整大小事件,Button.setOnAction()s 以进行缩放。

但是,我宁愿将烛台的布局属性绑定到轴的显示位置,也不愿设置/重置布局值,但找不到 NumberAxis 的“displayPositionProperty”(或类似的)。

可以这样做吗?我将绑定到哪个NumberAxis 属性?即。

candlestick.layoutXProperty().bind(xAxis.WHICH_PROPERTY?);

另外,绑定属性会比重置布局位置更有效吗?有些图表可能有数千个烛台,但在我弄清楚如何编写绑定代码之前,我无法测试资源使用情况。

我已经尝试将烛台缩放到轴的比例,但不能使用这种方法,因为缩放 Region 会影响其边框宽度。对于某些类型的烛台,这可能会改变其含义。

我还玩过 Ensemble 烛台演示图表。这对我很有帮助,但对我的需求来说太简单了。

这是一个演示我的方法的 MVCE。任何重新绑定的指导将不胜感激。

我正在使用 OpenJFX 17。

package test023;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class Test023 extends Application {

    @Override
    public void start(Stage stage) {    

        NumberAxis xAxis = new NumberAxis(0D, 10D, 1D);
        Pane pChart = new Pane();
        Pane pAxis = new Pane();
        VBox vb = new VBox();
        BorderPane bp = new BorderPane();

        pChart.setPrefHeight(100D);
        pAxis.getChildren().add(xAxis);
        xAxis.prefWidthProperty().bind(pAxis.widthProperty());
        xAxis.setAnimated(false);

        vb.setPadding(new Insets(10D));
        vb.getChildren().addAll(pChart, pAxis);

        Region point = new Region();
        point.setPrefSize(5D, 5D);
        point.setStyle("-fx-background-color: black;");

        pChart.getChildren().add(point);

        //Plot the point in its initial position (value 3 on the axis)
        double pointXValue = 3D;
        plotPoint(point, pointXValue, xAxis);

        //*****************************************************************
        //Can the listeners and button.setOnActions be replaced by binding
        //the point's layout value to the axis display position?
        //*****************************************************************

        //Handle resize events
        pChart.widthProperty().addListener((obs, oldVal, newVal) -> {
            plotPoint(point, pointXValue, xAxis);
        });

        stage.maximizedProperty().addListener((obs, oldVal, newVal) -> {
            plotPoint(point, pointXValue, xAxis);
        });        

        //Handle zooming (hard-coded upper and lower bounds for the 
        //sake of simplicity)
        Button btnZoomIn = new Button("Zoom in");
        btnZoomIn.setOnAction((event) -> {
            xAxis.setLowerBound(2D);
            xAxis.setUpperBound(8D);
            xAxis.layout();
            plotPoint(point, pointXValue, xAxis);
        });

        Button btnZoomOut = new Button("Zoom out");
        btnZoomOut.setOnAction((event) -> {
            xAxis.setLowerBound(0D);
            xAxis.setUpperBound(10D);
            xAxis.layout();
            plotPoint(point, pointXValue, xAxis);
        });

        bp.setCenter(vb);
        bp.setTop(new HBox(btnZoomIn, btnZoomOut));

        stage.setScene(new Scene(bp));
        stage.setTitle("Test bind layoutX");
        stage.setWidth(400D);
        stage.setHeight(200D);
        stage.show();

    }

    private void plotPoint(Region region, double axisPos, NumberAxis axis) {

        Platform.runLater(() -> {
            double posX = axis.getDisplayPosition(axisPos);
            region.setLayoutX(posX);
            region.setLayoutY(80D);
        });

    }

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

}

【问题讨论】:

  • 另请参阅*.com/questions/38871202/… 了解有关此问题的规范方法。
  • 感谢您的参考。我饶有兴趣地读了它,明白了你在说什么。我将尝试继承 Chart 类并覆盖 layoutPlotChildren() 方法。任何使添加趋势线和形状等内容变得更容易的事情都值得做。

标签: javafx charts openjfx


【解决方案1】:

@LukasOwen 回答 +1,回答您与绑定相关的实际问题。

但正如您所知,每个问题都有不止一种方法,我建议使用我的方法,考虑到可扩展性(添加许多点)和过多的绑定(对于每个点)。

这种方法的关键是:

  • 您将所有点数及其节点添加到地图中。
  • 每次渲染 xAxis 时,都会更新所有点的位置。因此,如果您调整窗口大小、更改范围或最大化窗口,这将隐式完成。

以下是该方法的示例:

import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.chart.NumberAxis;
import javafx.scene.control.Button;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import java.util.HashMap;
import java.util.Map;

public class Test023 extends Application {

    Map<Double, Region> plotPoints = new HashMap<>();
    double yOffset = 80D;
    Pane pChart;

    @Override
    public void start(Stage stage) {
        NumberAxis xAxis = new NumberAxis(0D, 10D, 1D);
        xAxis.needsLayoutProperty().addListener((obs, old, needsLayout) -> {
            if(!needsLayout) {
                plotPoints.forEach((num, point) -> {
                    double posX = xAxis.getDisplayPosition(num);
                    point.setLayoutX(posX);
                    point.setLayoutY(yOffset);
                });
            }
        });
        pChart = new Pane();
        Pane pAxis = new Pane();
        VBox vb = new VBox();
        BorderPane root = new BorderPane();

        pChart.setPrefHeight(100D);
        pAxis.getChildren().add(xAxis);
        xAxis.prefWidthProperty().bind(pAxis.widthProperty());
        xAxis.setAnimated(false);

        vb.setPadding(new Insets(10D));
        vb.getChildren().addAll(pChart, pAxis);

        addPoint(3D, "black");
        addPoint(4D, "red");
        addPoint(5D, "blue");

        //Handle zooming (hard-coded upper and lower bounds for the sake of simplicity)
        Button btnZoomIn = new Button("Zoom in");
        btnZoomIn.setOnAction((event) -> {
            xAxis.setLowerBound(2D);
            xAxis.setUpperBound(8D);
        });

        Button btnZoomOut = new Button("Zoom out");
        btnZoomOut.setOnAction((event) -> {
            xAxis.setLowerBound(0D);
            xAxis.setUpperBound(10D);
        });

        root.setCenter(vb);
        root.setTop(new HBox(btnZoomIn, btnZoomOut));

        stage.setScene(new Scene(root));
        stage.setTitle("Test bind layoutX");
        stage.setWidth(400D);
        stage.setHeight(200D);
        stage.show();
    }

    private void addPoint(double num, String color) {
        Region point = new Region();
        point.setPrefSize(5D, 5D);
        point.setStyle("-fx-background-color: " + color);
        plotPoints.put(num, point);
        pChart.getChildren().add(point);
    }

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

【讨论】:

  • 谢谢。我不知道axis.needsLayoutProperty()。它工作得很好,比使用基于舞台的监听器要优雅得多。我已经将节点映射到点,所以你的方法很适合我正在做的事情。我会进行一些压力测试,看看情况如何。顺便说一句,第一次单击按钮时放大不起作用。我将xAxis.layout(); 添加到按钮setOnActions,我假设它强制轴布局()并因此触发侦听器。这是正确的方法吗?
  • 我认为你不需要调用布局方法。对我来说,它运行良好(JavaFX 8)而不调用布局。当您更新边界时,ValueAxis.java 的内部代码会调用 invalidateRange() 和 requestAxisLayout()。我不确定为什么它不适合你。
  • 关于 needsLayoutProperty() 它是父节点的属性。因此,对于任何继承 Parent 的节点都具有此属性,并且您可以在节点完全呈现时进行监听(又名 needsLayout=false)
  • 我刚刚在 JavaFX 8 中尝试过,是的,它确实可以正常工作。它一定是 OpenJFX 17 的东西。我将不得不继续强制缩放布局。也感谢needsLayoutProperty() 提供的额外信息。知道这很有用。干杯。
【解决方案2】:

这样的事情会起作用

@Override
public void start(Stage stage) {    

    NumberAxis xAxis = new NumberAxis(0D, 10D, 1D);
    Pane pChart = new Pane();
    Pane pAxis = new Pane();
    VBox vb = new VBox();
    BorderPane bp = new BorderPane();

    pChart.setPrefHeight(100D);
    pAxis.getChildren().add(xAxis);
    xAxis.prefWidthProperty().bind(pAxis.widthProperty());
    xAxis.setAnimated(false);

    vb.setPadding(new Insets(10D));
    vb.getChildren().addAll(pChart, pAxis);

    Region point = new Region();
    point.setPrefSize(5D, 5D);
    point.setStyle("-fx-background-color: black;");

    pChart.getChildren().add(point);

    //Plot the point in its initial position (value 3 on the axis)
    double pointXValue = 3D;

    point.setLayoutY(80D);
    
    point.layoutXProperty().bind(Bindings.createDoubleBinding(()-> {
        return xAxis.getDisplayPosition(pointXValue);
    }, xAxis.lowerBoundProperty(), xAxis.upperBoundProperty(), pChart.widthProperty()));
        
    //Handle zooming (hard-coded upper and lower bounds for the 
    //sake of simplicity)
    Button btnZoomIn = new Button("Zoom in");
    btnZoomIn.setOnAction((event) -> {
        xAxis.setLowerBound(2D);
        xAxis.setUpperBound(8D);
        xAxis.layout();
    });

    Button btnZoomOut = new Button("Zoom out");
    btnZoomOut.setOnAction((event) -> {
        xAxis.setLowerBound(0D);
        xAxis.setUpperBound(10D);
        xAxis.layout();
    });

    bp.setCenter(vb);
    bp.setTop(new HBox(btnZoomIn, btnZoomOut));

    stage.setScene(new Scene(bp));
    stage.setTitle("Test bind layoutX");
    stage.setWidth(400D);
    stage.setHeight(200D);
    stage.show();

}

这将创建一个自定义双重绑定,该函数会在每次更改依赖项时计算绑定的值,有关详细信息,请参阅createDoubleBinding​

【讨论】:

  • 感谢您的回复。它完美地工作。我以前没有使用过双重绑定,并且会阅读它们。我还会用大图表做一些压力测试,看看情况如何。干杯。