如何实现多个属性的绑定以相互监听?

How to implement bindings of multiple properties to all listen to each other?

我有两个可拖动的 Circle 节点,我想用 Line 连接它们(不是从它们的中心,而是从它们的周边)。但是当一个 Circle 在被拖动时改变了位置,LinestartXstartY 的值改变了所以 Line 成为了两个 Circle,与连接它们半径的线共线。

我的问题是 Line's startX, startY, endX, and endY each individually listen to or bind to both Circles' centerXPropertycenterYProperty 似乎过于冗长(或者更确切地说,绑定到具有这些属性值的计算), 因为这将导致总共 16 bindings/listeners.

我想知道是否有更简单或更方便的方法来完成此操作。我正在考虑创建一个 SimpleDoubleProperty,它将是 Line 对象 (y2 - y1)/(x2 - x1) 的斜率,绑定到两个 centerXPropertycenterYPropertys,并让 startXstartYendXendY 各自收听 属性,但我也不确定如何拥有单个 属性 绑定到这四个属性的结果计算。

这是我目前构建 Line 的方式。我正在试验 startXPropertystartYProperty 绑定并意识到它正确地更新了 Line 但只有当源 Circle source 被移动时,这促使我问这个问题。 endXPropertyendYProperty 仍然将 Line 锚定在目标圆的中心。如果需要,我可以提供我的完整代码,尽管我认为这足以满足我要完成的任务。

public GraphEdge(GraphNode source, GraphNode target) {
    this.source = source;
    this.target = target;
    this.setFill(Color.BLACK);
    this.startXProperty().bind(Bindings.createDoubleBinding(() -> {
        slope = (target.getCenterY() - source.getCenterY())/(target.getCenterX() - source.getCenterX());
        return source.getCenterX() + Math.cos(Math.atan(slope)) * source.getRadius();
        }, source.boundsInParentProperty()));
    this.startYProperty().bind(Bindings.createDoubleBinding(() -> {
        slope = (target.getCenterY() - source.getCenterY())/(target.getCenterX() - source.getCenterX());
        return source.getCenterY() + Math.sin(Math.atan(slope)) * source.getRadius();
    }, source.boundsInParentProperty()));
    this.endXProperty().bind(Bindings.createDoubleBinding(() -> {
        Bounds b = target.getBoundsInParent();
        return b.getMinX() + b.getWidth() / 2;
    }, target.boundsInParentProperty()));
    this.endYProperty().bind(Bindings.createDoubleBinding(() -> {
        Bounds b = target.getBoundsInParent();
        return b.getMinY() + b.getHeight() / 2;
    }, target.boundsInParentProperty()));
}

createDoubleBinding 接受依赖项列表。您应该列出每行 属性 所依赖的所有属性。

this.startXProperty().bind(Bindings.createDoubleBinding(
    () -> {
        double slope = (target.getCenterY() - source.getCenterY())/(target.getCenterX() - source.getCenterX());
        return source.getCenterX() + Math.cos(Math.atan(slope)) * source.getRadius();
    },
    source.centerXProperty(),
    source.centerYProperty(),
    target.centerXProperty(),
    target.centerYProperty(),
    source.radiusProperty(),
));

对其他三个线属性重复上述操作。

我同意@John Kugelman 的回答。

我快速尝试了一下,看起来您的实际公式(使用 sin/tan)对我来说并不像预期的那样有效。 所以我重新考虑了逻辑并提出了以下解决方案。该解决方案基于“给定直线 AB,在距离 d 处找到直线上的点 C”的概念。这里A和B是圆心。

所以想法是:

  • 我们构建一个 DoubleBinding 来获取圆心之间的线的长度 (l)。
  • 然后我们计算半径为'r'和'l-r'的点。这些点位于两个圆的边缘。
  • 最后,我们通过绑定新的点来构建一条线。

请找到以下代码:

class GraphEdge extends Line {
    public GraphEdge(GraphNode source, GraphNode target) {
        DoubleBinding lineLength = Bindings.createDoubleBinding(() -> {
            double xDiffSqu = (target.getCenterX() - source.getCenterX()) * (target.getCenterX() - source.getCenterX());
            double yDiffSqu = (target.getCenterY() - source.getCenterY()) * (target.getCenterY() - source.getCenterY());
            return Math.sqrt(xDiffSqu + yDiffSqu);
        }, source.centerXProperty(), source.centerYProperty(), target.centerXProperty(), target.centerYProperty());

        DoubleBinding sTx = pointBinding(source, target, lineLength, false, Circle::getCenterX);
        DoubleBinding sTy = pointBinding(source, target, lineLength, false, Circle::getCenterY);
        DoubleBinding eTx = pointBinding(source, target, lineLength, true, Circle::getCenterX);
        DoubleBinding eTy = pointBinding(source, target, lineLength, true, Circle::getCenterY);

        setStroke(Color.BLUE);
        setStrokeWidth(2);
        startXProperty().bind(sTx);
        startYProperty().bind(sTy);
        endXProperty().bind(eTx);
        endYProperty().bind(eTy);
    }

    private DoubleBinding pointBinding(Circle startDot, Circle endDot, DoubleBinding lineLength, boolean isFarEnd, Function<Circle, Double> refPoint) {
        return Bindings.createDoubleBinding(() -> {
            double dt = isFarEnd ? lineLength.get() - endDot.getRadius() : startDot.getRadius();
            double t = dt / lineLength.get();
            double startPoint = refPoint.apply(startDot);
            double endPoint = refPoint.apply(endDot);
            double dy = ((1 - t) * startPoint) + (t * endPoint);
            return dy;
        }, lineLength);
    }
}

下面是完整的工作演示:

import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.event.EventHandler;
import javafx.geometry.Bounds;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.scene.shape.Line;
import javafx.stage.Stage;

import java.util.function.Function;

public class DoubleBindingsDemo extends Application {

    @Override
    public void start(Stage stage) throws Exception {
        StackPane root = new StackPane();
        root.setPadding(new Insets(20));
        Pane pane = new Pane();
        pane.setStyle("-fx-border-width:1px;-fx-border-color:black;");
        root.getChildren().add(pane);
        Scene sc = new Scene(root, 600, 600);
        stage.setScene(sc);
        stage.show();

        GraphNode greenNode = new GraphNode("green");
        GraphNode redNode = new GraphNode("red");
        GraphEdge edge = new GraphEdge(greenNode, redNode);
        pane.getChildren().addAll(greenNode, redNode, edge);
    }

    class GraphNode extends Circle {
        double sceneX, sceneY, centerX, centerY;

        public GraphNode(String color) {
            double radius = 30;
            setRadius(radius);
            setStyle("-fx-fill:" + color + ";-fx-stroke-width:2px;-fx-stroke:black;-fx-opacity:.5");
            setCenterX(radius);
            setCenterY(radius);
            setOnMousePressed(e -> {
                sceneX = e.getSceneX();
                sceneY = e.getSceneY();
                centerX = getCenterX();
                centerY = getCenterY();
            });

            EventHandler<MouseEvent> dotOnMouseDraggedEventHandler = e -> {
                // Offset of drag
                double offsetX = e.getSceneX() - sceneX;
                double offsetY = e.getSceneY() - sceneY;

                // Taking parent bounds
                Bounds parentBounds = getParent().getLayoutBounds();
                double dotRadius = getRadius();
                double maxCx = parentBounds.getWidth() - dotRadius;
                double maxCy = parentBounds.getHeight() - dotRadius;

                double cxOffset = centerX + offsetX;
                double cyOffset = centerY + offsetY;
                if (cxOffset < dotRadius) {
                    setCenterX(dotRadius);
                } else if (cxOffset < maxCx) {
                    setCenterX(cxOffset);
                } else {
                    setCenterX(maxCx);
                }

                if (cyOffset < dotRadius) {
                    setCenterY(dotRadius);
                } else if (cyOffset < maxCy) {
                    setCenterY(cyOffset);
                } else {
                    setCenterY(maxCy);
                }
            };
            setOnMouseDragged(dotOnMouseDraggedEventHandler);
        }
    }

    class GraphEdge extends Line {
        public GraphEdge(GraphNode source, GraphNode target) {
            DoubleBinding lineLength = Bindings.createDoubleBinding(() -> {
                double xDiffSqu = (target.getCenterX() - source.getCenterX()) * (target.getCenterX() - source.getCenterX());
                double yDiffSqu = (target.getCenterY() - source.getCenterY()) * (target.getCenterY() - source.getCenterY());
                return Math.sqrt(xDiffSqu + yDiffSqu);
            }, source.centerXProperty(), source.centerYProperty(), target.centerXProperty(), target.centerYProperty());

            DoubleBinding sTx = pointBinding(source, target, lineLength, false, Circle::getCenterX);
            DoubleBinding sTy = pointBinding(source, target, lineLength, false, Circle::getCenterY);
            DoubleBinding eTx = pointBinding(source, target, lineLength, true, Circle::getCenterX);
            DoubleBinding eTy = pointBinding(source, target, lineLength, true, Circle::getCenterY);

            setStroke(Color.BLUE);
            setStrokeWidth(2);
            startXProperty().bind(sTx);
            startYProperty().bind(sTy);
            endXProperty().bind(eTx);
            endYProperty().bind(eTy);
        }

        private DoubleBinding pointBinding(Circle startDot, Circle endDot, DoubleBinding lineLength, boolean isFarEnd, Function<Circle, Double> refPoint) {
            return Bindings.createDoubleBinding(() -> {
                double dt = isFarEnd ? lineLength.get() - endDot.getRadius() : startDot.getRadius();
                double t = dt / lineLength.get();
                double startPoint = refPoint.apply(startDot);
                double endPoint = refPoint.apply(endDot);
                double dy = ((1 - t) * startPoint) + (t * endPoint);
                return dy;
            }, lineLength);
        }
    }
}