【问题标题】:Force relayout of Java FX Chart programatically以编程方式强制重新布局 Javafx 图表
【发布时间】:2020-08-11 16:53:45
【问题描述】:

我已经对 Java FX 折线图进行了编程更改,我需要一种编程方式来强制重新布局 JavaFX 图表。这个问题之前已经被问过/回答过,但不是在我的上下文中。

我已经尝试了作为该问题答案的典型方法(请参阅下面的完整、最小的示例代码以及在线尝试解决问题)。这个问题的典型解决方案都不起作用。

具体来说(sp 是 StackPane):

    sp.requestLayout(); // does not work

    sp.applyCss(); 
    sp.layout();        // does not work

将上述代码放在.runLater() 中不起作用。

我知道我的更改显示在图表中,因为 (1) 当我手动调整图表大小时,我的变化突然出现 (2) 当我以编程方式使用“resize”方法时,我的更改出现但出现了不同的错误(加上只有父节点应该使用“resize”方法 - 而不是我们的程序员)。

下面是重现问题的最小完整代码集。当您运行代码时,我以编程方式将其中一个数据点更改为在显示图表时更大。此调整大小正常工作。当您右键单击图表时,会出现一个带有一个选项的上下文菜单(“调整所有点的大小”)。当您选择该单个选项时,我的代码会调整所有点的大小 - 但是 - 没有一个数据点会在视觉上调整大小。如果我通过拖动一侧手动调整图表大小,图表会重新布局,所有数据节点大小会立即在视觉上更改为正确的大小(我以编程方式将它们设置为的大小)。

如何强制重新布局以编程方式进行,而我可以手动强制进行?我不想做一个 hack(比如以编程方式将舞台大小设置为小 1 个像素,然后将其设置为大一个像素)。

注意:我已经读到在布局进行时尝试执行requestLayout() 将被忽略,因此可能会发生类似的事情。我认为runLater() 中的requestLayout() 可以解决正在进行的 Layout() 的问题,但这也不起作用。

更新:建议将缩放作为更改 StackPane 大小的替代方法。这个解决方案可能对某些人有帮助,但对我没有帮助。缩放符号的外观与更改区域大小并允许“符号”增长到该大小的外观不同。

总的来说,这是我的第一篇 stackoverflow 帖子。因此,感谢我过去在此论坛中使用过的所有示例,并在此先感谢您对此问题的回答。

import java.util.Random;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.LineChart.SortingPolicy;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart.Data;
import javafx.scene.chart.XYChart.Series;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;


public class dummy  extends Application {

    @Override
    public void start(Stage primaryStage) {
        Random random = new Random();
        final NumberAxis xAxis = new NumberAxis();
        final NumberAxis yAxis = new NumberAxis();
        xAxis.setLabel("X");
        yAxis.setLabel("Y");
        final LineChart<Number,Number> lineChart = new LineChart<Number,Number>(xAxis,yAxis);   

        Series<Number,Number> series = new Series<Number,Number>();
        series.setName("Dummy Data");
        // Generate data
        double x = 0.0;     
        double y = 0.0;
        for (int i = 0; i < 10; i++) {
            Data<Number,Number> data  = new Data(x += random.nextDouble(), y+=random.nextDouble());
            series.getData().add(data);
        }
        lineChart.getData().add(series);
        lineChart.setTitle("Random Data");
        lineChart.setAxisSortingPolicy(SortingPolicy.NONE); 

        Scene scene = new Scene(lineChart,1200,600);

        Stage stage = new Stage();
        stage.setScene(scene);
        stage.show();

        // This resizes the first data point directly (this resize is displayed correctly when program is run)
        Node node = series.getData().get(0).getNode();
        setSize((StackPane)node,20);

        // The context menu is invoked by a right click on the line Chart.  It will resize the data point based on a context menu pick
        // this resize does not work....unless I resize the window manually which causes a refresh/re-layout of the chart).
        lineChart.setOnMouseClicked(new EventHandler<MouseEvent>() {
            @Override public void handle(MouseEvent mouseEvent) {
                if (MouseButton.SECONDARY.equals(mouseEvent.getButton())) {
                    Scene scene = ((Node)mouseEvent.getSource()).getScene();
                    ContextMenu menu = createMenu(lineChart);
                    menu.show(scene.getWindow(), mouseEvent.getScreenX(), mouseEvent.getScreenY());
                }  
            }
        });
    }

    private void setSize(StackPane sp, int size) {
        sp.setMinSize(size, size);
        sp.setMaxSize(size, size);
        sp.setPrefSize(size, size);
    }
    // this creates a context menu that will allow you to resize all the data point nodes
    private ContextMenu createMenu(LineChart<Number,Number> lineChart) {

        final ContextMenu contextMenu = new ContextMenu();

        final MenuItem resize = new MenuItem("Resize ALL the points");
        resize.setOnAction(new EventHandler<ActionEvent>() {
            @Override public void handle(ActionEvent event) {
                for (Series<Number, Number> series : lineChart.getData()) {
                    for (Data<Number, Number> data : series.getData()) {
                        StackPane sp = (StackPane)data.getNode();
                        setSize (sp, 20);
                        // The above resizes do not take effect unless/until I manually resize the chart.

                        // the following two calls do not do anything;
                        sp.applyCss();
                        sp.layout();

                        // The request to layout the node does nothing
                        sp.requestLayout();

                        // Doing both of the above as runLaters does nothing
                        Platform.runLater(()->{sp.applyCss();sp.layout();});
                        Platform.runLater(()->{sp.requestLayout();});

                        // Going after the parent does nothing either
                        Group group = (Group)sp.getParent();
                        group.applyCss();
                        group.layout();
                        group.requestLayout();

                        // Going after the parent in a run later does nothing
                        Platform.runLater(()->{
                            group.applyCss();
                            group.layout();
                            group.requestLayout();
                        });

                        // note... doing a resize [commented out below] will work-ish.
                        // The documentation says NOT to use it thought that as it is for internal use only.
                        // By work-ish, the data points are enlarged BUT they are no longer centered on the data point
                        // When I resize the stage they get centered again - so this "solves" my original problem but causes a different problem
                        ////////////////////////////////////
                        //  sp.resize(20, 20);            // Uncomment this line to see how it mostly works but introduces a new issue
                        ////////////////////////////////////
                    }
                }
            }
        });
        contextMenu.getItems().add(resize);
        return contextMenu;
    }

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

【问题讨论】:

  • for 循环结束后添加一个虚拟样式表lineChart.getStylesheets().add(""); 就可以了。这有点骇人听闻。所以我不是特别喜欢。我希望会出现更好的。
  • 看起来像一个错误

标签: java javafx charts


【解决方案1】:

您无需将该节点转换为 StackPane 并设置大小。您需要使用setScaleX()setScaleY() 方法

    Node node = series.getData().get(0).getNode();
    node.setScaleX(20);
    node.setScaleY(20);

    @Override
    public void handle(ActionEvent event) {
        for (Series<Number, Number> series : lineChart.getData()) {
            for (Data<Number, Number> data : series.getData()) {
                Node node = data.getNode();
                node.setScaleY(20);
                node.setScaleX(20);
            }
        }
    }

【讨论】:

  • 使用ScaleXScale Y 解决了一个不同的问题,其外观和感觉与所要求的不同。 StackPane 的几种类型的更改(包括您的解决方案...缩放)将立即生效。我对 StackPane 大小的更改不会。使堆栈窗格大小的更改立即生效的编程解决方案是我正在寻找的解决方案。 + 表示快速响应!
【解决方案2】:

您可以使用例如强制重新布局内部类

  class LineChartX<X, Y> extends LineChart<X, Y>
  {
    public LineChartX(@NamedArg("xAxis") Axis<X> xAxis, @NamedArg("yAxis") Axis<Y> yAxis)
    {
      super(xAxis, yAxis);
    }

    @Override
    public void layoutPlotChildren()
    {
      super.layoutPlotChildren();
    }
  }

打电话

lineChart.layoutPlotChildren();

在您的菜单操作中。

简单的单行解决方案:

LineChart 场景图中的节点具有以下父子关系: 窗格 chartContent - 组 plotArea - 组 plotContent - 路径 seriesLine;

在类 XYChart 中定义的组 plotArea 的布局请求被抑制:

private final Group plotArea = new Group(){
  @Override public void requestLayout() {} // suppress layout requests
};

但 Pane chartContent 接受布局请求:

Node node = series.getNode();
if (node instanceof Path) {
  Path seriesLine = (Path) node;
  Parent parent = seriesLine.getParent();
  if (parent instanceof Group) {
    Group plotContent = (Group) parent;
    parent = plotContent.getParent();
    if (parent instanceof Group) {
      Group plotArea = (Group) parent;
      parent = plotArea.getParent();
      if (parent instanceof Pane) {
        Pane chartContent = (Pane) parent;
        chartContent.requestLayout();
      }
    }
  }
}

因此可以通过添加这一行来强制重新布局图表

series.getNode().getParent().getParent().getParent().requestLayout();

到您的菜单操作处理程序的末尾。

【讨论】:

  • 基本上您扩展了 LineChart 类并进行了“受保护”调用layoutPlotChildren()“public”。我在我的 for 循环之后放置了现在的公共调用,它运行良好。我喜欢这个解决方案的地方在于,您可以使用工厂方法创建扩展类的实例,但只返回一个 LineChart 对象。您可以将此 LineChart 对象视为在任何地方隐藏其真实性质的 LineChart 对象。然后在此例程中,您可以将其转换回 LineChartX 并调用它现在的公共 layoutPlotChildren()。唯一的妥协/欺骗是使用工厂方法开始!
  • 注意:作为对其他人的警告,我认为此解决方案不适用于 JavaFX 模块。我相信基于反射的方法也可以访问layoutPlotChildren() 方法也将起作用。基本上这个解决方案和基于反射的解决方案打破了 Java FX 的定义封装(如果 Java FX 设计者没有过度限制访问,则不需要)
  • @kleopatra,他们没有。我犯了错误并且记错了我在模块和私有变量/方法方面遇到的问题,我混淆了这两者。感谢您发现错误。作为解释,我遇到了成员是私有的并且子类化没有让您访问的问题。我做了拆分包的技巧并复制了代码并以这种方式访问​​了私有成员。此技巧不适用于不允许拆分包的模块。我的经验法则是“无法访问模块中的私有成员(通过反射除外)”。 LayoutPlotChildren() 受到保护 - 所以没有问题。
  • 我接受@kuleis 答案的简单解决方案 部分作为最佳答案。它很简单(1 班轮)。它是直接的(它使用适当的requestLayout() 调用)。它是特定的(它基于一个系列而不是基于整个 LineChart)。它解释了它为什么起作用以及为什么在父子树的较低级别使用requestLayout() 的其他方法不起作用。注意:如果 Java FX 内部发生变化,它可能会在未来某个时间失败,但如果发生这种情况,底层策略/方法可能会被重用。
  • 我的好(和赞成)是原始的,还原
【解决方案3】:

@c0der 在我的原始帖子中以评论形式发布了一个解决方案,该解决方案有效但产生了运行时警告(在 Eclipse 中)。他的解决方案是在 for 循环结束后使用lineChart.getStylesheets().add(""); 在 lineChart 级别添加一个虚拟样式表。此代码产生警告“2020 年 4 月 28 日上午 9:01:12 com.sun.javafx.css.StyleManager loadStylesheetUnPrivileged WARNING: Resource "" not found."

在不引起运行时警告的情况下,加载一个空的 .css 文件并将其添加为样式表:

lineChart.getStylesheets().add(CSS.class.getResource("Empty.css").toExternalForm()); // note: I keep my .css resource files at the same location as my CSS class // which is why I have the code "CSS.class" above

这个单行解决方案只工作一次,但我怀疑它会工作多次。我通过每次连续选择“调整所有点的大小”(在我上面的虚拟代码中)将 StackPane 的大小增加 5 来对其进行测试。果然,它只在第一次工作。

但是,我在该单行解决方案之前添加了无操作代码 lineChart.getStylesheets().replaceAll((s)-&gt;s+" ");,现在它可以连续工作多次。 这两行代码不管我执行多少次

lineChart.getStylesheets().replaceAll((s)->s+" ");
lineChart.getStylesheets().add(CSS.class.getResource("Empty.css").toExternalForm()); ` 

它 (1) 有效并且 (2) lineChart StyleSheets 列表的大小没有增长到超过 1 的大小。所以一个神秘的解决方案。

注意:如果您有一个现有的样式表(我在上面的虚拟示例中没有)lineChart.getStylesheets().replaceAll((s)-&gt;s+" "); 本身也可以工作。出于某种原因,lineChart.getStylesheets().replaceAll((s)-&gt;s); 没有在末尾添加“”是行不通的。

注意:我最初认为我必须编写一个切换解决方案来添加 Empty.css 并使用备用调用删除 Empty.css,但事实证明这是不必要的。

底线:如果您有现有的样式表lineChart.getStylesheets().replaceAll((s)-&gt;s+" "); 有效。如果您没有现有的 StyleSheet,请添加一个空的 .css 文件作为 StyleSheet,并结合上述 replaceAll 工作。

再次感谢 @c0der 的原创方法。

【讨论】:

  • 我确实提出了这个骇人听闻的解决方案,但我不喜欢它。如果您添加 css 资源,请考虑使用它们到 change the size of the data points
  • 谢谢@c0der。仅使用 css 的方法并不吸引人。我有 18 个符号,在运行时可以调整大小、重新着色和填充或不填充。这 18 个符号在 .css 中,但许多不是方形的,并且使用特定的 css 填充(例如 4x、7x、4x、7x)来保持它们的形状。这些填充是特定于大小的。每个符号有 20 个大小,我将有 18*20 个 css 符号定义(或者我在运行时根据大小乘数更改 css 设置)。我的非 css 调整工作直到我将调整代码放在上下文菜单下,Java FX 错误破坏了该方法。因此我的问题是如何强制布局。
猜你喜欢
  • 1970-01-01
  • 2012-07-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-02
相关资源
最近更新 更多