JavaFX PiChart,我的悬停值闪烁

JavaFX PiChart, my hover values blink

上下文:

嗨! 我正在尝试创建一个小弹出窗口,当鼠标悬停时显示 slice 的值,在我的 PieChart(使用 JavaFX)上。

我在 LineChartAreaChart 等上取得了成功。感谢这个 post:JavaFX LineChart Hover Values(感谢非常感谢 Jewelsea 的帮助)。

问题 (1/2):

但是对于 PieChart,我有一个问题:弹出窗口在闪烁 oO

我的代码:

语法颜色:https://bpaste.net/show/12838ad6b2e2

import java.util.ArrayList;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.chart.PieChart;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import com.alpha.client.view.nodes.stats.statsEngine.beans.ListRepere;
import com.alpha.client.view.nodes.stats.statsEngine.beans.OptionsChart;
import com.alpha.client.view.nodes.stats.statsEngine.beans.ValueStat;

/**
 *
 * @author Zombkey
 */
public class PieChartNode implements ChartNode {

//My personnal attributes
private ListRepere categories;
private ArrayList<ValueStat> values;
//The PieChart
private PieChart chart;

//The data of Chart, will be fill by a thread
private ObservableList<PieChart.Data> pieChartData;
//The node which contain chart and label
private Group group;
//The Label 
private final Label caption;

public PieChartNode(ListRepere categories, ArrayList<ValueStat> values, OptionsChart optionsChart) {
    this.categories = categories;
    this.values = values;

    //New Group
    group = new Group();
    //I must use a StackPane to place Label hover Chart
    StackPane pane = new StackPane();
    group.getChildren().add(pane);

    //Init' PieChart
    pieChartData = FXCollections.observableArrayList();
    chart = new PieChart(pieChartData);
    chart.setStartAngle(180.0);
    //Add chart to StackPane
    pane.getChildren().add(chart);

    //Init Popup(Label)
    caption = new Label("");
    caption.setVisible(false);
    caption.getStyleClass().addAll("chart-line-symbol", "chart-series-line");
    caption.setStyle("-fx-font-size: 12; -fx-font-weight: bold;");
    caption.setMinSize(Label.USE_PREF_SIZE, Label.USE_PREF_SIZE);
    //Add Label to StackPane
    pane.getChildren().add(caption);
}

@Override
public Node getNodeGraph() {
    return (Node) group;
}

@Override
public Task initTaskFormat() {
    Task<Void> task = new Task<Void>() {
        @Override
        protected Void call() throws Exception {
            //i and sizeOfallElements are just use for ProgressBar 
            int i = 0;
            int sizeOfallElements = values.size();
            updateProgress(i, sizeOfallElements);

            //For Each ValueStat (a Personnal pojo Class), I must create a slice 
            for (ValueStat v : values) {
                //Create the PieChart.Data and add it to ObservableList
                PieChart.Data dataTemp = new PieChart.Data(v.getCategorie().getStringName(), v.getDoubleValue());
                pieChartData.add(dataTemp);

                //HERE, the interessante code !
                //At the same way that the LineChart, I add Event when mouse entered and mouse exited.
                //When mouse entered (on the slice of PieChart)
                dataTemp.getNode().addEventHandler(MouseEvent.MOUSE_ENTERED,
                        new EventHandler<MouseEvent>() {
                            @Override
                            public void handle(MouseEvent e) {
                                System.out.println("MOUSE_ENTERED : "+dataTemp.getName());
                                //I display Label
                                caption.setVisible(true);
                                //I move Label near the mouse cursor
                                caption.setTranslateX(e.getX());
                                caption.setTranslateY(e.getY());
                                //I hide the mouse cursor
                                dataTemp.getNode().setCursor(Cursor.NONE);
                                //I change text of Label
                                caption.setText(String.valueOf(dataTemp.getPieValue()) + "\n" + dataTemp.getName());
                                //I try to change the frame color of Label
                                caption.getStyleClass().add(dataTemp.getNode().getStyleClass().get(2));
                            }
                        });

                //When mouse exited (the slice of PieChart)
                dataTemp.getNode().addEventHandler(MouseEvent.MOUSE_EXITED,
                        new EventHandler<MouseEvent>() {
                            @Override
                            public void handle(MouseEvent e) {
                                System.out.println("MOUSE_EXITED : "+dataTemp.getName());
                                //I Hide Label
                                caption.setVisible(false);
                                //I show the mouse cursor
                                dataTemp.getNode().setCursor(Cursor.DEFAULT);
                            }
                        });
                //Update progress
                updateProgress(i++, sizeOfallElements);
            }
            return null;
        }
    };
    return task;
}
}

问题 (2/2):

问题是事件(MOUSE_ENTERED 和 MOUSE_EXITED)发出的次数过多,而不是一次。

例如: 我刚放进去,然后放下,我的鼠标悬停了一片。 这是控制台上的结果:

MOUSE_ENTERED : BC
MOUSE_EXITED : BC
MOUSE_ENTERED : BC
MOUSE_EXITED : BC
MOUSE_ENTERED : BC
MOUSE_EXITED : BC
MOUSE_ENTERED : BC
MOUSE_EXITED : BC

有人知道为什么会出现事件错误吗?

谢谢:)

不是label造成的闪烁效果?
当您显示标签时,表示您退出了被监听的节点。这会导致隐藏标签。当标签消失时,它会在节点上触发鼠标输入事件,显示标签等。
未经测试,只是一个想法。


编辑:
如果我是对的,请尽量避免将标签放在鼠标指针下:

caption.setTranslateX(e.getX()+10);
caption.setTranslateY(e.getY()+10);

例如(10 是一个幻数,取决于插图等)

感谢大家的帮助。

@maskacovnik 找到问题,@James_D 找到一个很酷的解决方案,@ItachiUchiha 把我的图片放在我的 post 上:D

现在,我的新代码。

import java.util.ArrayList;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.concurrent.Task;
import javafx.event.EventHandler;
import javafx.scene.Cursor;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.chart.PieChart;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import com.alpha.client.view.nodes.stats.statsEngine.beans.ListRepere;
import com.alpha.client.view.nodes.stats.statsEngine.beans.OptionsChart;
import com.alpha.client.view.nodes.stats.statsEngine.beans.ValueStat;

public class PieChartNode implements ChartNode {

    //My personnal attributes
    private ListRepere categories;
    private ArrayList<ValueStat> values;
    //The PieChart
    private PieChart chart;

    //The data of Chart, will be fill by a thread
    private ObservableList<PieChart.Data> pieChartData;
    //The node which contain chart and label
    private Group group;
    //The Label 
    private final Label caption;

    public PieChartNode(ListRepere categories, ArrayList<ValueStat> values, OptionsChart optionsChart) {
        this.categories = categories;
        this.values = values;

        //New Group
        group = new Group();
        //I must use a StackPane to place Label hover Chart
        StackPane pane = new StackPane();
        group.getChildren().add(pane);

        //Init' PieChart
        pieChartData = FXCollections.observableArrayList();
        chart = new PieChart(pieChartData);
        chart.setStartAngle(180.0);
        //Add chart to StackPane
        pane.getChildren().add(chart);

        //Init Popup(Label)
        caption = new Label("");
        caption.setVisible(false);
        caption.getStyleClass().addAll("chart-line-symbol", "chart-series-line");
        caption.setStyle("-fx-font-size: 12; -fx-font-weight: bold;");
        caption.setMinSize(Label.USE_PREF_SIZE, Label.USE_PREF_SIZE);
        //Add Label to StackPane
        pane.getChildren().add(caption);
    }

    @Override
    public Node getNodeGraph() {
        return (Node) group;
    }

    @Override
    public Task initTaskFormat() {
        Task<Void> task = new Task<Void>() {
            @Override
            protected Void call() throws Exception {
                //i and sizeOfallElements are just use for ProgressBar 
                int i = 0;
                int sizeOfallElements = values.size();
                updateProgress(i, sizeOfallElements);

                //For Each ValueStat (a Personnal pojo Class), I must create a slice 
                for (ValueStat v : values) {
                    //Create the PieChart.Data and add it to ObservableList
                    PieChart.Data dataTemp = new PieChart.Data(v.getCategorie().getStringName(), v.getDoubleValue());
                    pieChartData.add(dataTemp);

                    //At the same way that the LineChart, I add Event when mouse entered and mouse exited.
                    //When mouse entered (on the slice of PieChart)
                    dataTemp.getNode().addEventHandler(MouseEvent.MOUSE_ENTERED,
                            new EventHandler<MouseEvent>() {
                                @Override
                                public void handle(MouseEvent e) {
                                    //Set Label ignores the mouse 
                                    caption.setMouseTransparent(true);
                                    //I move Label near the mouse cursor, with a offset !
                                    caption.setTranslateX(e.getX());
                                    caption.setTranslateY(e.getY()+20);
                                    //I change text of Label
                                    caption.setText(String.valueOf(dataTemp.getPieValue()) + "\n" + dataTemp.getName());
                                    //Change the color of popup, to adapt it to slice
                                    if(caption.getStyleClass().size() == 4){
                                        caption.getStyleClass().remove(3);
                                    }
                                    caption.getStyleClass().add(dataTemp.getNode().getStyleClass().get(2));
                                    //I display Label
                                    caption.setVisible(true);
                                }
                            });
                    //Need to add a event when the mouse move hover the slice
                    //If I don't the popup stay blocked on edges of the slice.
                    dataTemp.getNode().addEventHandler(MouseEvent.MOUSE_MOVED,
                            new EventHandler<MouseEvent>() {
                                @Override
                                public void handle(MouseEvent e) {
                                    //Keep Label near the mouse
                                    caption.setTranslateX(e.getX());
                                    caption.setTranslateY(e.getY()+20);
                                }
                            });

                    //When mouse exited (the slice of PieChart)
                    dataTemp.getNode().addEventHandler(MouseEvent.MOUSE_EXITED,
                            new EventHandler<MouseEvent>() {
                                @Override
                                public void handle(MouseEvent e) {
                                    //I Hide Label
                                    caption.setVisible(false);
                                }
                            });
                    //Update progress
                    updateProgress(i++, sizeOfallElements);
                }
                return null;
            }
        };
        return task;
    }
}

结果如下:

我有同样的问题,但也想确保弹出窗口可以延伸到图表之外,即当文本不适合图表时,它不会被截断。这是使用工具提示而不是标签的解决方案:

public class ChartHoverUtil<T> {
   public static void setupPieChartHovering(PieChart chart) {
      new ChartHoverUtil<PieChart.Data>(
            data -> String.format("Value = ", data.getPieValue()),
            data -> data.getNode())
            .setupHovering(chart.getData());
   }

   private final Tooltip tooltip = new Tooltip();
   private final SimpleBooleanProperty adjustingTooltip = new SimpleBooleanProperty(false);
   private final Function<T, String> textProvider;
   private final Function<T, Node> nodeProvider;

   private EventHandler<MouseEvent> moveHandler = new EventHandler<MouseEvent>() {
      @Override
      public void handle(MouseEvent e) {
         if (tooltip.isShowing()) {
            setLabelPosition(e);
         }
      }
   };

   private EventHandler<MouseEvent> enterHandler = new EventHandler<MouseEvent>() {
      @Override
      public void handle(MouseEvent e) {
         adjustingTooltip.set(true);

         Node chartNode = (Node) e.getSource();

         tooltip.show(chartNode, e.getScreenX(), e.getScreenY());
         setLabelPosition(e);

         ObservableBooleanValue stillHovering = chartNode.hoverProperty().or(adjustingTooltip);
         stillHovering.addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean nowHovering) {
               if (!nowHovering) {
                  stillHovering.removeListener(this);
                  tooltip.hide();
               }
            }
         });

         T chartData = (T) chartNode.getUserData();
         String txt = textProvider.apply(chartData);
         tooltip.setText(txt);

         adjustingTooltip.set(false);
      }
   };

   public ChartHoverUtil(Function<T, String> textProvider, Function<T, Node> getNode) {
      this.textProvider = textProvider;
      this.nodeProvider = getNode;
      tooltip.addEventFilter(MouseEvent.MOUSE_MOVED, moveHandler);
   }

   public void setupHovering(Collection<T> data) {
      for (T chartData : data) {
         Node node = nodeProvider.apply(chartData);
         node.setUserData(chartData);
         setupNodeHovering(node);
      }
   }

   private void setupNodeHovering(Node node) {
      node.addEventFilter(MouseEvent.MOUSE_MOVED, moveHandler);
      node.addEventHandler(MouseEvent.MOUSE_ENTERED, enterHandler);
      // Do not use MOUSE_EXIT handler because it is triggered immediately when showing the tooltip
   }

   private void setLabelPosition(MouseEvent e) {
      adjustingTooltip.set(true);

      tooltip.setAnchorX(e.getScreenX());
      tooltip.setAnchorY(e.getScreenY() + 20);

      adjustingTooltip.set(false);
   }
}