如何使窗格停留在连接javafx中的draggablenode的线上

How to make an pane stay over line connecting draggablenode in javafx

我正在设计一个 UI 具有可拖动节点的图形结构。在图中,我有一个名为 relation 的组件(它是一个窗格),它显示了两个节点之间的 link。 我希望关系保持不变并与线中间的线一起移动。

目前的UI设计如下图

而预期的是这样的:

线段终点坐标修改时需要刷新节点位置。为避免每次布局传递多次触发计算,我建议从父级的 layoutChildren 方法执行此操作,但您也可以从 startXendY 的侦听器执行此操作... 特性。不过这会导致一些不必要的计算。

关于计算节点的位置:节点的中心需要与线的中点对齐,因此需要求解以下等式markTopLeft

markTopLeft + (markWidth, markHeight) / 2 = (lineStart + lineEnd) / 2

markTopLeft = (lineStart + lineEnd - (markWidth, markHeight)) / 2

示例

允许自定义布局计算的窗格

public class PostProcessPane extends Pane {

    private final Set<Node> modifiedChildren = new HashSet<>();
    private final Set<Node> modifiedChildrenUnmodifiable = Collections.unmodifiableSet(modifiedChildren);
    private final List<Consumer<Set<Node>>> postProcessors = new ArrayList<>();

    public List<Consumer<Set<Node>>> getPostProcessors() {
        return postProcessors;
    }

    private final ChangeListener listener = (o, oldValue, newValue) -> modifiedChildren.add((Node) ((ReadOnlyProperty) o).getBean());

    private void initListener() {
        getChildren().addListener((ListChangeListener.Change<? extends Node> c) -> {
            while (c.next()) {
                if (c.wasRemoved()) {
                    for (Node n : c.getRemoved()) {
                        n.boundsInParentProperty().removeListener(listener);
                    }
                }
                if (c.wasAdded()) {
                    for (Node n : c.getAddedSubList()) {
                        n.boundsInParentProperty().addListener(listener);
                    }
                }
            }
        });
    }

    public PostProcessPane() {
        initListener();
    }

    public PostProcessPane(Node... children) {
        super(children);
        initListener();

        for (Node n : children) {
            n.boundsInParentProperty().addListener(listener);
        }
    }

    @Override
    protected void layoutChildren() {
        super.layoutChildren();

        if (!modifiedChildren.isEmpty()) {
            for (Consumer<Set<Node>> processor : postProcessors) {
                processor.accept(modifiedChildrenUnmodifiable);
            }
            modifiedChildren.clear();
        }

    }

}

用法

@Override
public void start(Stage primaryStage) throws Exception {
    Rectangle r1 = new Rectangle(200, 50, Color.BLUE);
    Rectangle r2 = new Rectangle(200, 50, Color.RED);
    Rectangle mark = new Rectangle(200, 50, Color.YELLOW);
    Line line = new Line();

    r1.setX(20);
    r2.setX(380);
    r2.setY(450);

    PostProcessPane root = new PostProcessPane(line, r1, r2, mark);
    root.getPostProcessors().add(changedNodes -> {
        if (changedNodes.contains(r1) || changedNodes.contains(r2) || changedNodes.contains(mark)) {
            Bounds bounds1 = r1.getBoundsInParent();
            Bounds bounds2 = r2.getBoundsInParent();

            // refresh line ends
            line.setStartX(bounds1.getMinX() + bounds1.getWidth() / 2);
            line.setStartY(bounds1.getMaxY());
            line.setEndX(bounds2.getMinX() + bounds2.getWidth() / 2);
            line.setEndY(bounds2.getMinY());

            // recalculate mark position
            mark.setX((line.getStartX() + line.getEndX() - mark.getWidth()) / 2);
            mark.setY((line.getStartY() + line.getEndY() - mark.getHeight()) / 2);
        }
    });

    // add some movement for the nodes
    Timeline timeline = new Timeline(
            new KeyFrame(Duration.ZERO,
                    new KeyValue(r1.xProperty(), r1.getX()),
                    new KeyValue(r1.yProperty(), r1.getY()),
                    new KeyValue(r2.xProperty(), r2.getX())),
            new KeyFrame(Duration.seconds(1),
                    new KeyValue(r2.xProperty(), r1.getX())),
            new KeyFrame(Duration.seconds(2),
                    new KeyValue(r1.xProperty(), r2.getX()),
                    new KeyValue(r1.yProperty(), r2.getY() / 2))
    );
    timeline.setAutoReverse(true);
    timeline.setCycleCount(Animation.INDEFINITE);
    timeline.play();

    Scene scene = new Scene(root);

    primaryStage.setScene(scene);
    primaryStage.show();
}