【问题标题】:bindBidirectional with function?与功能绑定双向?
【发布时间】:2020-06-06 01:37:56
【问题描述】:

我知道您可以使用 NumberBindings 来将 x 绑定到 y,这样如果 x 为 10,则 y 为 20。我想做一些更复杂的事情,我不确定是否可以使用绑定来实现那个目标。

我想要两组三个滑块,它们在笛卡尔坐标或球坐标中平移 3D 空间中的向量。我希望看到的是,每当修改笛卡尔滑块中的一个值时,球形滑块上的值也会相应更改,反之亦然。因此,更改一组三个中的一个值会导致更改另一组三个中的三个值。

我想尝试使用 ChangeListeners,但我进入循环,更改 A 导致重新计算 B 导致重新计算 A,并且我得到了这个网站的同名错误。我还想尝试让一个矢量对象双向绑定到两组滑块,但我遇到了同样的问题。

我想到的另一件事是让 ChangeListener 首先查看一组中的任何滑块是否被用户更改与被侦听器更改,但我还没有弄清楚如何做到这一点,甚至如果是这样,我怀疑这可能不是一种有效的方法。我要改用 ActionListener 吗?

这是我能做到的吗?

编辑:添加了 MWE

import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.value.ChangeListener;

public class BindingProblemMWE {
    private DoubleProperty xProperty;
    public DoubleProperty xProperty() {
        if (xProperty == null) {xProperty = new SimpleDoubleProperty(0);}
        return xProperty;
    }

    private DoubleProperty yProperty;
    public DoubleProperty yProperty() {
        if (yProperty == null) {yProperty = new SimpleDoubleProperty(0);}
        return yProperty;
    }

    private DoubleProperty zProperty;
    public DoubleProperty zProperty() {
        if (zProperty == null) {zProperty = new SimpleDoubleProperty(0);}
        return zProperty;
    }

    private DoubleProperty azProperty;
    public DoubleProperty azProperty() {
        if (azProperty == null) {azProperty = new SimpleDoubleProperty(0);}
        return azProperty;
    }

    private DoubleProperty elProperty;
    public DoubleProperty elProperty() {
        if (elProperty == null) {elProperty = new SimpleDoubleProperty(0);}
        return elProperty;
    }

    private DoubleProperty rhoProperty;
    public DoubleProperty rhoProperty() {
        if (rhoProperty == null) {rhoProperty = new SimpleDoubleProperty(0);}
        return rhoProperty;
    }

    private ChangeListener<Number> recalculateSpherical() {
        return (obs, ov, nv) -> {

            double x = xProperty().doubleValue();
            double y = yProperty().doubleValue();
            double z = zProperty().doubleValue();

            azProperty.set(calculateAz(x,y,z));
            elProperty.set(calculateEl(x,y,z));
            rhoProperty.set(calculateRho(x,y,z));
        };
    }

    private ChangeListener<Number> recalculateCartesian() {
        return (obs, ov, nv) -> {
            double az = azProperty().doubleValue();
            double el = elProperty().doubleValue();
            double rho = rhoProperty().doubleValue();

            xProperty.set(calculateX(az, el, rho));
            yProperty.set(calculateY(az, el, rho));
            zProperty.set(calculateZ(az, el, rho));
        };
    }

    private void initialize() {

        xProperty().addListener(recalculateSpherical());
        yProperty().addListener(recalculateSpherical());
        zProperty().addListener(recalculateSpherical());

        azProperty().addListener(recalculateCartesian());
        elProperty().addListener(recalculateCartesian());
        rhoProperty().addListener(recalculateCartesian());

        xProperty().set(1);
        yProperty().set(1);
        zProperty().set(1);

    }

    private static double calculateX(double az, double el, double rho) {
        return rho*Math.sin(el)*Math.cos(az);}

    private static double calculateY(double az, double el, double rho) {
        return rho*Math.sin(el)*Math.sin(az);}

    private static double calculateZ(double az, double el, double rho) {
        return rho*Math.cos(el);}

    private static double calculateAz(double x, double y, double z) {
        return Math.atan2(y, x);}

    private static double calculateEl(double x, double y, double z) {
            return Math.atan2(Math.sqrt(x*x + y*y), z);}

    private static double calculateRho(double x, double y, double z) {
        return Math.sqrt(x*x + y*y + z*z);}


    public static void main(String [] args) {
        BindingProblemMWE mwe = new BindingProblemMWE();
        mwe.initialize();
    }
}

【问题讨论】:

    标签: java javafx listener


    【解决方案1】:

    Tomas Mikula 有一个名为 ReactFX 的库,其中包含通常工作得更好的 JavaFX 属性版本,并且具有您正在寻找的功能。查看org.reactfx.value 包中的ValVar 接口。不过,看起来这不是在积极维护中。

    您在示例中看到堆栈溢出异常的原因是对每个单独坐标的更改会提示更改其他表示中的坐标。当然,这些个别的变化并不是彼此相反的,所以你最终会陷入一个无限循环的变化。 (例如,更改x 会导致az 发生变化,从而导致球形表示与x 中的变化所代表的点不同,因此这会导致x 的进一步变化等.) 如果您将每个表示的三个更改“原子化”,那么理论上您将停止无限递归,只要这两个函数彼此完全相反。

    实现这一点的最佳方法可能是在此处使用单个对象而不是三个不同的值。这具有额外的优势,即当您更改 3D 空间中的点时,您会观察到一次更改而不是三个更改。所以在这里我会使用ObjectProperty&lt;T&gt; 来表示一些合适的类型T

    最后要注意的一点是,当您在示例中使用浮点运算时,您仍然容易受到堆栈溢出异常的影响,因为计算中的数值错误会阻止函数与每个函数完全相反其他。
    当这些发生变化时设置和取消设置标志的想法是正确的方法;你只需要弄清楚该标志的保留位置。在此示例中,我创建了一个单独的 BidirectionalBinding 类,它允许您存储标志并通过类中的方法解除两个侦听器的绑定。

    import java.util.function.Function;
    
    import javafx.beans.property.ObjectProperty;
    import javafx.beans.property.Property;
    import javafx.beans.property.SimpleObjectProperty;
    import javafx.beans.value.ChangeListener;
    
    public class BidirectionalBinding<T,U> {
    
        private final Property<T> source ;
        private final Property<U> target ;
    
        private boolean changing = false ;
        private ChangeListener<? super T> sourceListener;
        private ChangeListener<? super U> targetListener;
    
        public BidirectionalBinding(Property<T> source, Property<U> target, 
                Function<T,U> mapping, Function<U,T> inverseMapping) {
            this.source = source ;
            this.target = target ;
    
            target.setValue(mapping.apply(source.getValue()));
    
            sourceListener = (obs, oldSourceValue, newSourceValue) -> {
                if (! changing) {
                    changing = true ;
                    target.setValue(mapping.apply(newSourceValue));
                }
                changing = false ;
            };
            source.addListener(sourceListener);
    
            targetListener = (obs, oldTargetValue, newTargetValue) -> {
                if (! changing) {
                    changing = true ;
                    source.setValue(inverseMapping.apply(newTargetValue));
                }
                changing = false ;
            };
            target.addListener(targetListener);
    
        }
    
        public void unbind() {
            source.removeListener(sourceListener);
            target.removeListener(targetListener);
        }
    
        public Property<T> getSource() {
            return source;
        }
    
        public Property<U> getTarget() {
            return target;
        }
    
        public static class Cartesian {
            private final double x ;
            private final double y ;
            private final double z ;
            public Cartesian(double x, double y, double z) {
                super();
                this.x = x;
                this.y = y;
                this.z = z;
            }
            public double getX() {
                return x;
            }
            public double getY() {
                return y;
            }
            public double getZ() {
                return z;
            }
            @Override
            public String toString() {
                return String.format("[x=%f, y=%f, z=%f]", x, y, z);
            }
            @Override
            public boolean equals(Object o) {
                if (! (o instanceof Cartesian)) return false ;
                if (o == this) return true ;
                Cartesian other = (Cartesian) o ;
                return x == other.x && y == other.y && z == other.z ;
            }
    
        }
    
        public static class Spherical {
            private final double az ;
            private final double el ;
            private final double rho ;
            public Spherical(double az, double el, double rho) {
                super();
                this.az = az;
                this.el = el;
                this.rho = rho;
            }
            public double getAz() {
                return az;
            }
            public double getEl() {
                return el;
            }
            public double getRho() {
                return rho;
            }
            @Override
            public String toString() {
                return String.format("[az=%f, el=%f, rho=%f]", az, el, rho);
            }
            @Override
            public boolean equals(Object o) {
                if (! (o instanceof Spherical)) return false ;
                if (o == this) return true ;
                Spherical other = (Spherical) o ;
                return az == other.az && el == other.el && rho == other.rho ;
            }           
    
        }
    
        // test case:
        public static void main(String[] args) {
            ObjectProperty<Cartesian> cartesian = new SimpleObjectProperty<>(new Cartesian(0,0,0));
            ObjectProperty<Spherical> spherical = new SimpleObjectProperty<>(new Spherical(0,0,0));
    
            Function<Cartesian, Spherical> cartesianToSpherical = cart -> {
                double x = cart.getX();
                double y = cart.getY();
                double z = cart.getZ();
                double az = Math.atan2(y, x);
                double el = Math.atan2(Math.sqrt(x*x + y*y), z);
                double rho = Math.sqrt(x*x + y*y + z*z) ;
                return new Spherical(az, el, rho);
            };
    
            Function<Spherical, Cartesian> sphericalToCartesian = spher -> {
                double az = spher.getAz();
                double el = spher.getEl();
                double rho = spher.getRho();
                double x = rho*Math.sin(el)*Math.cos(az);
                double y = rho*Math.sin(el)*Math.sin(az);
                double z = rho*Math.cos(el);
                return new Cartesian(x, y, z);
            };
    
            BidirectionalBinding<Cartesian, Spherical> binding = new BidirectionalBinding<>(cartesian, spherical,
                    cartesianToSpherical, sphericalToCartesian);
    
            System.out.println(cartesian.get());
            System.out.println(spherical.get());
            System.out.println("\nSetting cartesian to [1,1,1]");
            cartesian.set(new Cartesian(1, 1, 1));
            System.out.println(cartesian.get());
            System.out.println(spherical.get());
            System.out.println("\nSetting sphercial to [pi/4, pi/4, 1]");
            spherical.set(new Spherical(Math.PI/4, Math.PI/4, 1));
            System.out.println(cartesian.get());
            System.out.println(spherical.get());
    
            binding.unbind();
        }
    }
    

    如果您想在每个表示中为三个坐标维护三个单独的属性,那么我认为您仍然可以使用这种通用策略,但您可能需要一个将六个属性作为字段的特定类并且是特定于这个问题,而不是像这个例子那样是通用的。

    使用 ReactFX(以及我定义的 CartesianSpherical 类),我认为您可以执行类似的操作

    ObjectProperty<Cartesian> cartesian = new SimpleObjectProperty<>(new Cartesian(0,0,0));
    Property<Spherical> = Var.mapBidirectional(cartesian, cartesianToSpherical, sphericalToCartesian);
    

    如果 Tomas 仍然活跃在此站点上,他可能会给您一些额外的想法(并且可能比我建议的实现更强大)。

    【讨论】:

    • 我刚刚找到了一个非常相似的解决方案,并开始实施它!请参阅carl-witt.de/customized-bidirectional-bindings-in-javafx 但是,我认为它遇到了您描述的浮点问题。我没有得到堆栈溢出,但它确实运行了很多次(大概它会收敛)。解决方案是将if(alreadyCalled) 更改为if(alreadyCalled || almostEqual(oldValue, newValue)),其中almostEqual 是类型A 和B 的bindBidirectional 的一些函数输入? (使用引用示例中的措辞)
    • @FrankHarris 我认为我的每次更改只运行一次。如果它运行了很多次,可能是因为你的滑块导致了很多变化。 IIRC,滑块有一个 valueChanging 属性(本质上,用户仍在滑动滑块),您可以在它仍在变化时检查而不更新点。
    猜你喜欢
    • 2011-02-16
    • 2011-08-27
    • 2013-02-06
    • 2023-03-27
    • 2016-02-10
    • 2012-07-10
    • 1970-01-01
    • 2017-11-02
    相关资源
    最近更新 更多