【问题标题】:Bidirectional property binding via expression or some approach to work around it通过表达式或某种解决方法的双向属性绑定
【发布时间】:2016-03-27 17:44:35
【问题描述】:

我对属性绑定比较陌生,我正在寻找一些关于如何解决设计问题的高级建议,我将尝试在此处描述一个简单的示例。

问题描述

本示例的目标是允许用户在可平移和可缩放的 2D 空间中以交互方式指定框/矩形区域。描绘盒子的 2D 屏幕空间映射到 2D “真实空间”(例如电压与时间的笛卡尔空间,或 GPS 或其他)。用户应该能够随时垂直/水平缩放/平移他的视口,从而改变这两个空间之间的映射。

screen-space <-------- user-adjustable mapping --------> real-space

用户通过拖动边框/角来指定其视口中的矩形,如本演示所示:

class InteractiveHandle extends Rectangle {

    private final Cursor hoverCursor;
    private final Cursor activeCursor;
    private final DoubleProperty centerXProperty = new SimpleDoubleProperty();
    private final DoubleProperty centerYProperty = new SimpleDoubleProperty();

    InteractiveHandle(DoubleProperty x, DoubleProperty y, double w, double h) {
        super();
        centerXProperty.bindBidirectional(x);
        centerYProperty.bindBidirectional(y);
        widthProperty().set(w);
        heightProperty().set(h);
        hoverCursor = Cursor.MOVE;
        activeCursor = Cursor.MOVE;
        bindRect();
        enableDrag(true,true);
    }

    InteractiveHandle(DoubleProperty x, ObservableDoubleValue y, double w, ObservableDoubleValue h) {
        super();
        centerXProperty.bindBidirectional(x);
        centerYProperty.bind(y);
        widthProperty().set(w);
        heightProperty().bind(h);
        hoverCursor = Cursor.H_RESIZE;
        activeCursor = Cursor.H_RESIZE;
        bindRect();
        enableDrag(true,false);
    }

    InteractiveHandle(ObservableDoubleValue x, DoubleProperty y, ObservableDoubleValue w, double h) {
        super();
        centerXProperty.bind(x);
        centerYProperty.bindBidirectional(y);
        widthProperty().bind(w);
        heightProperty().set(h);
        hoverCursor = Cursor.V_RESIZE;
        activeCursor = Cursor.V_RESIZE;
        bindRect();
        enableDrag(false,true);
    }

    InteractiveHandle(ObservableDoubleValue x, ObservableDoubleValue y, ObservableDoubleValue w, ObservableDoubleValue h) {
        super();
        centerXProperty.bind(x);
        centerYProperty.bind(y);
        widthProperty().bind(w);
        heightProperty().bind(h);
        hoverCursor = Cursor.DEFAULT;
        activeCursor = Cursor.DEFAULT;
        bindRect();
        enableDrag(false,false);
    }

    private void bindRect(){
        xProperty().bind(centerXProperty.subtract(widthProperty().divide(2)));
        yProperty().bind(centerYProperty.subtract(heightProperty().divide(2)));
    }

//make a node movable by dragging it around with the mouse.
  private void enableDrag(boolean xDraggable, boolean yDraggable) {
    final Delta dragDelta = new Delta();
    setOnMousePressed((MouseEvent mouseEvent) -> {
        // record a delta distance for the drag and drop operation.
        dragDelta.x = centerXProperty.get() - mouseEvent.getX();
        dragDelta.y = centerYProperty.get() - mouseEvent.getY();
        getScene().setCursor(activeCursor);
    });
    setOnMouseReleased((MouseEvent mouseEvent) -> {
        getScene().setCursor(hoverCursor);
    });
    setOnMouseDragged((MouseEvent mouseEvent) -> {
        if(xDraggable){
            double newX = mouseEvent.getX() + dragDelta.x;
            if (newX > 0 && newX < getScene().getWidth()) {
                centerXProperty.set(newX);
            }
        }
        if(yDraggable){
            double newY = mouseEvent.getY() + dragDelta.y;
            if (newY > 0 && newY < getScene().getHeight()) {
                centerYProperty.set(newY);
            }
        }
    });
    setOnMouseEntered((MouseEvent mouseEvent) -> {
        if (!mouseEvent.isPrimaryButtonDown()) {
            getScene().setCursor(hoverCursor);
        }
    });
    setOnMouseExited((MouseEvent mouseEvent) -> {
        if (!mouseEvent.isPrimaryButtonDown()) {
            getScene().setCursor(Cursor.DEFAULT);
        }
    });
  }
//records relative x and y co-ordinates.
  private class Delta { double x, y; }

}

public class InteractiveBox extends Group {

    private static final double sideHandleWidth = 2;
    private static final double cornerHandleSize = 4;
    private static final double minHandleFraction = 0.5;
    private static final double maxCornerClearance = 6;
    private static final double handleInset = 2;

    private final Rectangle rectangle;

    private final InteractiveHandle ihLeft;
    private final InteractiveHandle ihTop;
    private final InteractiveHandle ihRight;
    private final InteractiveHandle ihBottom;

    private final InteractiveHandle ihTopLeft;
    private final InteractiveHandle ihTopRight;
    private final InteractiveHandle ihBottomLeft;
    private final InteractiveHandle ihBottomRight;

    InteractiveBox(DoubleProperty xMin, DoubleProperty yMin, DoubleProperty xMax, DoubleProperty yMax){
        super();

        rectangle = new Rectangle();
        rectangle.widthProperty().bind(xMax.subtract(xMin));
        rectangle.heightProperty().bind(yMax.subtract(yMin));
        rectangle.xProperty().bind(xMin);
        rectangle.yProperty().bind(yMin);

        DoubleBinding xMid = xMin.add(xMax).divide(2);
        DoubleBinding yMid = yMin.add(yMax).divide(2);
        DoubleBinding hx = (DoubleBinding) Bindings.max(
                 rectangle.widthProperty().multiply(minHandleFraction)
                ,rectangle.widthProperty().subtract(maxCornerClearance*2)
        );
        DoubleBinding vx = (DoubleBinding) Bindings.max(
                 rectangle.heightProperty().multiply(minHandleFraction)
                ,rectangle.heightProperty().subtract(maxCornerClearance*2)
        );
        ihTopLeft = new InteractiveHandle(xMin,yMax,cornerHandleSize,cornerHandleSize);
        ihTopRight = new InteractiveHandle(xMax,yMax,cornerHandleSize,cornerHandleSize);
        ihBottomLeft = new InteractiveHandle(xMin,yMin,cornerHandleSize,cornerHandleSize);
        ihBottomRight = new InteractiveHandle(xMax,yMin,cornerHandleSize,cornerHandleSize);
        ihLeft   = new InteractiveHandle(xMin,yMid,sideHandleWidth,vx);
        ihTop    = new InteractiveHandle(xMid,yMax,hx,sideHandleWidth);
        ihRight  = new InteractiveHandle(xMax,yMid,sideHandleWidth,vx);
        ihBottom = new InteractiveHandle(xMid,yMin,hx,sideHandleWidth);

        style(ihLeft);
        style(ihTop);
        style(ihRight);
        style(ihBottom);
        style(ihTopLeft);
        style(ihTopRight);
        style(ihBottomLeft);
        style(ihBottomRight);

        getChildren().addAll(rectangle
                ,ihTopLeft, ihTopRight, ihBottomLeft, ihBottomRight
                ,ihLeft, ihTop, ihRight, ihBottom
        );

        rectangle.setFill(Color.ALICEBLUE);
        rectangle.setStroke(Color.LIGHTGRAY);
        rectangle.setStrokeWidth(2);
        rectangle.setStrokeType(StrokeType.CENTERED);
    }

    private void style(InteractiveHandle ih){
        ih.setStroke(Color.TRANSPARENT);
        ih.setStrokeWidth(handleInset);
        ih.setStrokeType(StrokeType.OUTSIDE);
    }
}   

public class Summoner extends Application {

    DoubleProperty x = new SimpleDoubleProperty(50);
    DoubleProperty y = new SimpleDoubleProperty(50);
    DoubleProperty xMax = new SimpleDoubleProperty(100);
    DoubleProperty yMax = new SimpleDoubleProperty(100);

    @Override
    public void start(Stage primaryStage) {
        InteractiveBox box = new InteractiveBox(x,y,xMax,yMax);
        Pane root = new Pane();
        root.getChildren().add(box);
        Scene scene = new Scene(root, 300, 250);
        primaryStage.setScene(scene);
        primaryStage.show();

    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }
}

在用户指定矩形后,其坐标(在实际空间中)被传递到程序的不同部分或由程序的不同部分读取。

我的理由

我的第一直觉是使用 JavaFX 节点中内置的缩放/平移属性来实现映射,但我们希望边框和手柄具有一致的大小/外观,而不管缩放状态如何;缩放应该只扩大概念矩形本身,而不是加厚边框或角手柄。

(在下文中,箭头代表因果关系/影响/依赖关系。例如,A ---&gt; B 可能表示属性 B 绑定到属性 A(或者可能表示事件处理程序 A 设置属性 B),&lt;-----&gt;可以表示双向绑定。像--+--&gt; 这样的多尾箭头可以表示依赖于多个输入可观察对象的绑定。)

所以我的问题变成了:我应该做什么?

  • real-space-properties ---+--&gt; screen-space-properties
  • real-space-properties &lt;--+--- screen-space properties
  • 或其他不同的东西,使用&lt;----&gt;

一方面,我们在屏幕空间中有鼠标事件和渲染矩形本身。根据上面的演示,这主张一个自包含的交互式矩形(我们可以在外部观察(以及如果我们愿意的话)操纵其屏幕空间位置/尺寸属性)。

mouse events -----> screen-space properties ------> depicted rectangle
                          |
                          |
                          --------> real-space properties -----> API

另一方面,当用户调整平移/缩放时,我们希望保留矩形在实际空间(而不是屏幕空间)中的属性。这主张使用 pan&zoom-state 属性将屏幕空间属性绑定到真实空间属性:

                  pan/zoom properties
                         |
                         |
real-space properties ---+--> screen-space properties ------> depicted rectangle
        |
        |
        -------> API

如果我尝试将上述两种方法放在一起,我会遇到问题:

                                    mouse events
                                         |
                  pan/zoom properties    |
                         |               |
                         |               v
real-space properties <--+--> screen-space properties ------> depicted rectangle
        |             * 
        |
        -------> API

这张图对我来说很有意义,但我不认为直接在 * 处的那种“双向”三向绑定是可能的。但是是否有一种简单的方法来模拟/解决它?还是我应该采取完全不同的方法?

【问题讨论】:

    标签: java data-binding javafx javafx-8 property-binding


    【解决方案1】:

    以下是缩放和可平移窗格上具有恒定笔划宽度的矩形示例。您只需将比例因子定义为窗格的属性,将它绑定到调用类中的属性,然后将其划分为绑定到矩形笔画宽度的属性。

    import javafx.animation.KeyFrame;
    import javafx.animation.KeyValue;
    import javafx.animation.Timeline;
    import javafx.application.Application;
    import javafx.beans.property.DoubleProperty;
    import javafx.beans.property.SimpleDoubleProperty;
    import javafx.event.EventHandler;
    import javafx.scene.Group;
    import javafx.scene.Scene;
    import javafx.scene.control.ScrollPane;
    import javafx.scene.control.ScrollPane.ScrollBarPolicy;
    import javafx.scene.input.MouseButton;
    import javafx.scene.input.MouseEvent;
    import javafx.scene.input.ScrollEvent;
    import javafx.scene.layout.AnchorPane;
    import javafx.scene.layout.Pane;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.Rectangle;
    import javafx.scene.shape.StrokeType;
    import javafx.stage.Stage;
    import javafx.util.Duration;
    
    public class ZoomAndPanExample extends Application {
    
        private ScrollPane scrollPane = new ScrollPane();
    
        private final DoubleProperty zoomProperty = new SimpleDoubleProperty(1.0d);
        private final DoubleProperty strokeWidthProperty = new SimpleDoubleProperty(1.0d);
        private final DoubleProperty deltaY = new SimpleDoubleProperty(0.0d);
    
        private final Group group = new Group();
    
        public static void main(String[] args) {
            Application.launch(args);
        }
    
        @Override
        public void start(Stage primaryStage) {
    
            scrollPane.setPannable(true);
            scrollPane.setHbarPolicy(ScrollBarPolicy.NEVER);
            scrollPane.setVbarPolicy(ScrollBarPolicy.NEVER);
            AnchorPane.setTopAnchor(scrollPane, 10.0d);
            AnchorPane.setRightAnchor(scrollPane, 10.0d);
            AnchorPane.setBottomAnchor(scrollPane, 10.0d);
            AnchorPane.setLeftAnchor(scrollPane, 10.0d);
    
            AnchorPane root = new AnchorPane();
    
            Rectangle rect = new Rectangle(80, 60);
    
            rect.setStroke(Color.NAVY);
            rect.setFill(Color.web("#000080", 0.2));
            rect.setStrokeType(StrokeType.INSIDE);
            rect.strokeWidthProperty().bind(strokeWidthProperty.divide(zoomProperty));
    
            group.getChildren().add(rect);
            // create canvas
            PanAndZoomPane panAndZoomPane = new PanAndZoomPane();
            zoomProperty.bind(panAndZoomPane.myScale);
            deltaY.bind(panAndZoomPane.deltaY);
            panAndZoomPane.getChildren().add(group);
    
            SceneGestures sceneGestures = new SceneGestures(panAndZoomPane);
    
            scrollPane.setContent(panAndZoomPane);
            panAndZoomPane.toBack();
            scrollPane.addEventFilter( MouseEvent.MOUSE_CLICKED, sceneGestures.getOnMouseClickedEventHandler());
            scrollPane.addEventFilter( MouseEvent.MOUSE_PRESSED, sceneGestures.getOnMousePressedEventHandler());
            scrollPane.addEventFilter( MouseEvent.MOUSE_DRAGGED, sceneGestures.getOnMouseDraggedEventHandler());
            scrollPane.addEventFilter( ScrollEvent.ANY, sceneGestures.getOnScrollEventHandler());
    
            root.getChildren().add(scrollPane);
            Scene scene = new Scene(root, 600, 400);
            primaryStage.setScene(scene);
            primaryStage.show();
        }
    
        class PanAndZoomPane extends Pane {
    
            public static final double DEFAULT_DELTA = 1.3d;
            DoubleProperty myScale = new SimpleDoubleProperty(1.0);
            public DoubleProperty deltaY = new SimpleDoubleProperty(0.0);
            private Timeline timeline;
    
    
            public PanAndZoomPane() {
    
                this.timeline = new Timeline(60);
    
                // add scale transform
                scaleXProperty().bind(myScale);
                scaleYProperty().bind(myScale);
            }
    
    
            public double getScale() {
                return myScale.get();
            }
    
            public void setScale( double scale) {
                myScale.set(scale);
            }
    
            public void setPivot( double x, double y, double scale) {
                // note: pivot value must be untransformed, i. e. without scaling
                // timeline that scales and moves the node
                timeline.getKeyFrames().clear();
                timeline.getKeyFrames().addAll(
                    new KeyFrame(Duration.millis(200), new KeyValue(translateXProperty(), getTranslateX() - x)),
                    new KeyFrame(Duration.millis(200), new KeyValue(translateYProperty(), getTranslateY() - y)),
                    new KeyFrame(Duration.millis(200), new KeyValue(myScale, scale))
                );
                timeline.play();
    
            }
    
        /** 
         * fit the rectangle to the width of the window
         */
            public void fitWidth () {
                double scale = getParent().getLayoutBounds().getMaxX()/getLayoutBounds().getMaxX();
                double oldScale = getScale();
    
                double f = (scale / oldScale)-1;
    
                double dx = getTranslateX() - getBoundsInParent().getMinX() - getBoundsInParent().getWidth()/2;
                double dy = getTranslateY() - getBoundsInParent().getMinY() - getBoundsInParent().getHeight()/2;
    
                double newX = f*dx + getBoundsInParent().getMinX();
                double newY = f*dy + getBoundsInParent().getMinY();
    
                setPivot(newX, newY, scale);
    
            }
    
            public void resetZoom () {
                double scale = 1.0d;
    
                double x = getTranslateX();
                double y = getTranslateY();
    
                setPivot(x, y, scale);
            }
    
            public double getDeltaY() {
                return deltaY.get();
            }
            public void setDeltaY( double dY) {
                deltaY.set(dY);
            }
        }
    
    
        /**
         * Mouse drag context used for scene and nodes.
         */
        class DragContext {
    
            double mouseAnchorX;
            double mouseAnchorY;
    
            double translateAnchorX;
            double translateAnchorY;
    
        }
    
        /**
         * Listeners for making the scene's canvas draggable and zoomable
         */
        public class SceneGestures {
    
            private DragContext sceneDragContext = new DragContext();
    
            PanAndZoomPane panAndZoomPane;
    
            public SceneGestures( PanAndZoomPane canvas) {
                this.panAndZoomPane = canvas;
            }
    
            public EventHandler<MouseEvent> getOnMouseClickedEventHandler() {
                return onMouseClickedEventHandler;
            }
    
            public EventHandler<MouseEvent> getOnMousePressedEventHandler() {
                return onMousePressedEventHandler;
            }
    
            public EventHandler<MouseEvent> getOnMouseDraggedEventHandler() {
                return onMouseDraggedEventHandler;
            }
    
            public EventHandler<ScrollEvent> getOnScrollEventHandler() {
                return onScrollEventHandler;
            }
    
            private EventHandler<MouseEvent> onMousePressedEventHandler = new EventHandler<MouseEvent>() {
    
                public void handle(MouseEvent event) {
    
                    sceneDragContext.mouseAnchorX = event.getX();
                    sceneDragContext.mouseAnchorY = event.getY();
    
                    sceneDragContext.translateAnchorX = panAndZoomPane.getTranslateX();
                    sceneDragContext.translateAnchorY = panAndZoomPane.getTranslateY();
    
                }
    
            };
    
            private EventHandler<MouseEvent> onMouseDraggedEventHandler = new EventHandler<MouseEvent>() {
                public void handle(MouseEvent event) {
    
                    panAndZoomPane.setTranslateX(sceneDragContext.translateAnchorX + event.getX() - sceneDragContext.mouseAnchorX);
                    panAndZoomPane.setTranslateY(sceneDragContext.translateAnchorY + event.getY() - sceneDragContext.mouseAnchorY);
    
                    event.consume();
                }
            };
    
            /**
             * Mouse wheel handler: zoom to pivot point
             */
            private EventHandler<ScrollEvent> onScrollEventHandler = new EventHandler<ScrollEvent>() {
    
                @Override
                public void handle(ScrollEvent event) {
    
                    double delta = PanAndZoomPane.DEFAULT_DELTA;
    
                    double scale = panAndZoomPane.getScale(); // currently we only use Y, same value is used for X
                    double oldScale = scale;
    
                    panAndZoomPane.setDeltaY(event.getDeltaY()); 
                    if (panAndZoomPane.deltaY.get() < 0) {
                        scale /= delta;
                    } else {
                        scale *= delta;
                    }
    
                    double f = (scale / oldScale)-1;
    
                    double dx = (event.getX() - (panAndZoomPane.getBoundsInParent().getWidth()/2 + panAndZoomPane.getBoundsInParent().getMinX()));
                    double dy = (event.getY() - (panAndZoomPane.getBoundsInParent().getHeight()/2 + panAndZoomPane.getBoundsInParent().getMinY()));
    
                    panAndZoomPane.setPivot(f*dx, f*dy, scale);
    
                    event.consume();
    
                }
            };
    
            /**
             * Mouse click handler
             */
            private EventHandler<MouseEvent> onMouseClickedEventHandler = new EventHandler<MouseEvent>() {
    
                @Override
                public void handle(MouseEvent event) {
                    if (event.getButton().equals(MouseButton.PRIMARY)) {
                        if (event.getClickCount() == 2) {
                            panAndZoomPane.resetZoom();
                        }
                    }
                    if (event.getButton().equals(MouseButton.SECONDARY)) {
                        if (event.getClickCount() == 2) {
                            panAndZoomPane.fitWidth();
                        }
                    }
                }
            };
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2011-06-25
      • 2020-04-13
      • 2016-08-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-06-20
      相关资源
      最近更新 更多