JavaFX中的自定义双向绑定

3

我正在尝试实现一个涉及两个字段计算的GUI。我的模型有2个属性和一个绑定。

ObjectProperty<BigDecimal> price = new SimpleObjectProperty<>();
ObjectProperty<BigDecimal> quantity= new SimpleObjectProperty<>();
ObjectBinding<BigDecimal> totalPrice = new ObjectBinding<BigDecimal>() {
    { bind(price,quantity);}
    protected BigDecimal computeValue() {
        if (price.get() == null || quantity.get() == null) return null;
        return price.get().multiply(quantity.get());
    }
};

我的GUI有3个TextField,分别对应价格、数量和总价。通常情况下,我会将我的属性和TextField之间进行常规绑定,如下所示:

priceTextField.textProperty().bindBidirectional(myModel.priceProperty(), new NumberStringConverter());

现在有点棘手的是,如果用户修改了价格或数量,它必须更新总价(这是绑定目前所做的)。但我希望能够做到以下几点:如果用户更新了总价,则按固定价格重新计算相应的数量。

所以问题是:我如何创建这样的流程 => 总价绑定在价格和数量上,但数量绑定在总价和价格上。当我在totalPriceTextfield中输入内容时,它应该更新quantityTextField,反之亦然。

谢谢。

****** 编辑 ******** 这里有一段丑陋的代码,只是为了说明我想要实现的功能(注意:我知道我可以使用Binding.multiply等方法,但我需要为将来的项目实现计算函数)。

public class TestOnBindings {

    private DoubleProperty price = new SimpleDoubleProperty(10.0);
    private DoubleProperty quantity = new SimpleDoubleProperty(1.0);
    private DoubleProperty total = new SimpleDoubleProperty(1.0);

    private DoubleBinding totalBinding = new DoubleBinding() {
        {bind(quantity,price);}
        @Override
        protected double computeValue() {
            return quantity.get()*price.get();
        }
    };

    private DoubleBinding quantityBinding = new DoubleBinding() {
        {bind(total,price);}
        @Override
        protected double computeValue() {
            return total.get()/price.get();
        }
    }; 


    public TestOnBindings(){
        total.bind(totalBinding); //should really not do that, looks ugly
        quantity.bind(quantityBinding); //now you're asking for troubles
    }


    public void setPrice(Double price){
        this.price.set(price);
    }


    public void setQuantity(Double quantity){
        this.quantity.set(quantity);
    }

    public void setTotal(Double total){
        this.total.set(total);
    }


    public Double getTotal(){
        return total.get();
    }


    public Double getQuantity(){
        return quantity.get();
    }

    public static void main(String[] args) {
        TestOnBindings test = new TestOnBindings();
        test.setQuantity(5.0);

        System.out.println("Total amount = " + test.getTotal());
    }

}

还有一个明显的错误:

异常线程中出现了 "main" java.lang.RuntimeException: 无法设置绑定值。 在 javafx.beans.property.DoublePropertyBase.set(DoublePropertyBase.java:142)。

2个回答

4

我认为我的问题已经在另一篇文章中得到了解答,这个人提供了一个简单的方法在他的博客上。

我稍微修改了他的类以适应我的需求,并且有一个非常易于使用的类:

public class CustomBinding {

    public static <A ,B> void bindBidirectional(Property<A> propertyA, Property<B> propertyB, Function<A,B> updateB, Function<B,A> updateA){
        addFlaggedChangeListener(propertyA, propertyB, updateB);
        addFlaggedChangeListener(propertyB, propertyA, updateA);
    }

    public static <A ,B> void bind(Property<A> propertyA, Property<B> propertyB, Function<A,B> updateB){
        addFlaggedChangeListener(propertyA, propertyB, updateB);
    }

    private static <X,Y> void addFlaggedChangeListener(ObservableValue<X> propertyX, WritableValue<Y> propertyY, Function<X,Y> updateY){
        propertyX.addListener(new ChangeListener<X>() {
            private boolean alreadyCalled = false;

            @Override
            public void changed(ObservableValue<? extends X> observable, X oldValue, X newValue) {
                if(alreadyCalled) return;
                try {
                    alreadyCalled = true;
                    propertyY.setValue(updateY.apply(newValue));
                }
                finally {alreadyCalled = false; }
            }
        });
    }
}

然后将其应用于我的示例...仍需要进行微调,但它可以完成工作。

public class TestOnBindings {

    private DoubleProperty price = new SimpleDoubleProperty(10.0);
    private DoubleProperty quantity = new SimpleDoubleProperty(1.0);
    private DoubleProperty total = new SimpleDoubleProperty(1.0);

    public TestOnBindings(){
        CustomBinding.<Number,Number>bindBidirectional(quantity, total, 
                (newQuantity)-> newQuantity.doubleValue() * price.get(),
                (newTotal)-> newTotal.doubleValue() /price.get());

        CustomBinding.<Number,Number>bind(price, total, 
                (newPrice)-> newPrice.doubleValue() * quantity.get());
    }

    public void setPrice(Double price){this.price.set(price);}
    public void setQuantity(Double quantity){this.quantity.set(quantity);}
    public void setTotal(Double total){this.total.set(total);}

    public Double getTotal(){return total.get();}
    public Double getQuantity(){return quantity.get();}
    public Double getPrice(){return price.get();}


    public static void main(String[] args) {
        TestOnBindings test = new TestOnBindings();

        test.setQuantity(5.0);

        System.out.println("Quantity = " + test.getQuantity());
        System.out.println("Price = " + test.getPrice());
        System.out.println("Total = " + test.getTotal());

        test.setTotal(60.0);

        System.out.println("---------------------------------------------");
        System.out.println("Quantity = " + test.getQuantity());
        System.out.println("Price = " + test.getPrice());
        System.out.println("Total = " + test.getTotal());

        test.setPrice(5.0);

        System.out.println("---------------------------------------------");
        System.out.println("Quantity = " + test.getQuantity());
        System.out.println("Price = " + test.getPrice());
        System.out.println("Total = " + test.getTotal());
    }

}

2

你真的需要使用绑定吗?这样做就可以了。绑定只是一种花哨的监听器。

public TestOnBindings(){
    price.addListener((obs,ov,nv) -> {
        total.set(price.doubleValue()*quantity.doubleValue());
    });
    //could be same listener as price if total is a complicated function
    quantity.addListener((obs,ov,nv) -> {
        total.set(price.doubleValue()*quantity.doubleValue());
    });
    total.addListener((obs,ov,nv) -> {
        quantity.set(total.doubleValue()/price.doubleValue());
    });
}

2
嗨,布莱恩,是的,你说得对,谢谢你的评论。唯一需要注意的是要避免陷入无休止的刷新循环。 - BlackLabrador
我考虑过这个问题,但在测试时并没有发生。如果这是一个问题,你可以编写非匿名侦听器,在设置总数的侦听器中只需执行 total.removeListener(...);,然后重新添加即可。 - brian
TestOnBindings test = new TestOnBindings(); test.setQuantity(5.0); System.out.println("总金额 = " + test.getTotal()); test.setTotal(60.0); System.out.println("数量金额 = " + test.getQuantity());最终输出结果为100/6/60(初始价格为10,数量为1,总额为1),这表明存在循环。感谢您的所有建议。 - BlackLabrador
我刚刚测试了错误和无限循环,没有检查数学。现在它可以工作了,只是一个愚蠢的数学错误。我把qty = tot / qty。我找不到参考资料,但有一种机制可以阻止监听器循环。 - brian
是的,你的代码不会导致无限循环。但是,在处理 GUI 时,由于前面提到的无限循环问题,以下代码会导致 StackOverflowError。slider1.valueProperty().addListener((obs,nv,ov)->{slider2.setValue(nv.doubleValue());}); slider2.valueProperty().addListener((obs,nv,ov)->{slider1.setValue(nv.doubleValue());}); - cw'

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接