JavaFX:在折线图上添加标签并根据缩放更新它们

JavaFX8 : add label on linechart and update them according to a zoom

我是 JavaFX 的新用户,我的实习项目遇到了一些问题。我必须制作一个 GUI 来绘制一个光谱并在每个峰的顶部添加标签(x 值)。用户可以放大图表,因此必须更新标签。

我使用第一种方法基于此添加标签 post :write text on a chart 但是我遇到了一些问题,只有一半的标签可见(见图)和标签位置的更新缩放后效果不好。我检查了每个标签的值和位置,它们都很好,如果我在我的图表上应用一个动作,标签的后半部分会出现,所以我不知道造成这种误解的原因。我的更新(标签位置)功能是缩放的上界 yaxis 的侦听器,但要获得良好的位置,我需要手动使用我的更新按钮,如果有人有想法,我找不到解决方案?

我目前使用anchorpane,使用stackpane会更好吗?

我放了一部分代码:

public class PaneController implements Initializable{
 @FXML
 private LineChart<Number, Number> lineChart;
 private ObservableList<Annotation> annotationNodes = FXCollections.observableArrayList();

 @Override
 public void initialize(URL location, ResourceBundle resources) {
  // TODO Auto-generated method stub
  lineChart.setLegendVisible(false);
  lineChart.setCreateSymbols(false);
  lineChart.setData(createData());
  //create labels
  createLabels(lineChart.getData());
  //frame listener
  ((AnchorPane)lineChart.getParent()).heightProperty().addListener((num)-> update());
     //axis listener
  ((NumberAxis)lineChart.getYAxis()).upperBoundProperty().addListener((num)-> update());
     //update listener on middle button
     lineChart.getParent().addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
         @Override
         public void handle(MouseEvent event) {
            if( event.getButton() == MouseButton.MIDDLE){
             update();
             }
         }
     });
     MyJFXChartUtil.setupZooming( lineChart );
 }
 
 /** create data for the line chart */
 public ObservableList<Series<Number,Number>> createData(){
   ObservableList<Series<Number,Number>> data = FXCollections.observableArrayList();
   for(int i=1;i<=10;i++){
    XYChart.Series<Number, Number> peakSelect = new XYChart.Series<>();
    peakSelect.getData().add(new XYChart.Data<Number, Number>(i*2,0));
    peakSelect.getData().add(new XYChart.Data<Number, Number>(i*2,i));
    data.add(peakSelect);
   }
   return data;
 }
 
   /** place a x-value label on each peak */
   private void createLabels(ObservableList<Series<Number,Number>> data){
      Pane pane = (Pane) lineChart.getParent();
      //clear old annotations
      pane.getChildren().clear();
      pane.getChildren().add(lineChart);
   for(int i=0;i<lineChart.getData().size();i++){
    for(XYChart.Data<Number, Number> value:lineChart.getData().get(i).getData()){
     if(value.getYValue().intValue()>0){
         //massAnnot
         Annotation massAnnot = new Annotation(new Label(""+value.getXValue()), value.getXValue().doubleValue(), value.getYValue().doubleValue());
            annotationNodes.add(massAnnot);
            //labelAnnot
         Annotation labelAnnot = new Annotation(new Label(""+(i+1)), value.getXValue().doubleValue(), 11);
            annotationNodes.add(labelAnnot);
              }
    }
   }
      //add node to parent
      for (Annotation annot : annotationNodes) { 
        pane.getChildren().add(annot.getLabel());
      }
   }
   
   /** update position of labels */
   private void update(){
    //clear nodes and add the linechart as children
    NumberAxis xAxis = (NumberAxis) lineChart.getXAxis();
    NumberAxis yAxis = (NumberAxis) lineChart.getYAxis();
    System.out.println(xAxis.getUpperBound()+" update "+yAxis.getUpperBound());
    Pane pane = (Pane) lineChart.getParent();
    
    pane.getChildren().clear();
    pane.getChildren().add(lineChart);
    int i = 0;
    for (Annotation annot : annotationNodes) {
     Node node = annot.getLabel();
     if(i%2==0){//massAnnot
      double x = lineChart.getXAxis().localToParent(xAxis.getDisplayPosition(annot.getX()), 0).getX() + lineChart.getPadding().getLeft();
      double y = yAxis.localToParent(0,yAxis.getDisplayPosition(annot.getY())).getY() + lineChart.getPadding().getTop();
      node.setLayoutX(x);
      node.setLayoutY(y - node.prefHeight(Integer.MAX_VALUE));
     }
     else{//labelAnnot
      double x = lineChart.getXAxis().localToParent(xAxis.getDisplayPosition(annot.getX()), 0).getX() + lineChart.getPadding().getLeft();
      double y = yAxis.localToParent(0,yAxis.getDisplayPosition(annot.getY())).getY() + lineChart.getPadding().getTop();
      node.setLayoutX(x);
      node.setLayoutY(y - node.prefHeight(Integer.MAX_VALUE));
     }
     node.autosize();
     pane.getChildren().add(node);
    }
   }
   
  class Annotation{
   private Label label;
   private double x;
   private double y;

   public Annotation(Label label, double x, double y) {
    this.label = label;
    this.x = x;
    this.y = y;
   }
   public Label getLabel(){
    return this.label;
   }
   
   public double getX(){
    return this.x;
   }
   
   public double getY(){
    return this.y;
   }
  } 
}
public class Main extends Application {
 @Override
 public void start(Stage primaryStage) {
  try {
   FXMLLoader loader = new FXMLLoader();
   loader.setLocation(Main.class.getResource("Pane.fxml"));
   AnchorPane rootPane = (AnchorPane) loader.load();
   Scene scene = new Scene(rootPane);
   primaryStage.setScene(scene);
   primaryStage.setTitle("Test");

   PaneController controller = loader.getController();
   
   primaryStage.show();
            
  } catch(Exception e) {
   e.printStackTrace();
  }
 }
 
 public static void main(String[] args) {
  launch(args);
 }
}
<?xml version="1.0" encoding="UTF-8"?>

<?import javafx.scene.chart.*?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>

<AnchorPane xmlns="http://javafx.com/javafx/8.0.40" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.PaneController">
   <children>
      <LineChart fx:id="lineChart" prefHeight="400.0" prefWidth="500.0" AnchorPane.bottomAnchor="0.0" AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0">
        <xAxis>
          <NumberAxis side="BOTTOM" />
        </xAxis>
        <yAxis>
          <NumberAxis side="LEFT" />
        </yAxis>
      </LineChart>
   </children>
</AnchorPane>

似乎更新被调用得很快,轴还没有更新,所以当你更新标签位置时,none 布局值已经更新。您可以将代码更改为:

private void update() {
    runIn(50, () -> {
        /*your current code*/
    }
}

private void runIn(int ms, Runnable callback) {
    new Thread(() -> {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) { e.printStackTrace(); }
        Platform.runLater(callback);
    }).start();
}

之所以可行,是因为我猜测最长的 Pulse(一个屏幕布局和渲染)需要多长时间。您可以通过将 -Djavafx.pulseLogger=true 标志添加到您的 JVM args 来查看脉冲需要多长时间。每个脉冲都会显示在控制台上。您正在寻找这个:

...
PULSE: 49 [15ms:29ms]
...

29ms 是布局和渲染场景所花费的时间,因为 50ms 长了一点,当 update() 运行 它得到了新的值。但是,假设我们有一台慢速机器,可能需要 60ms 个脉冲,需要 update() 才能获得正确的值。然后您需要将 50ms 更改为 70ms。最好在pulse上打个钩,看看什么时候结束,然后运行update()。不幸的是,我不知道这样的钩子是否存在。