通过表达式的双向属性绑定或某种方法来解决它

时间:2015-12-21 15:36:24

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

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

问题描述

此示例中的目标是允许用户在可平移和可缩放的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 ---> B可能意味着属性B绑定到属性A(或者它可能意味着事件处理程序A设置属性B),并且{ {1}}可以表示双向绑定。多尾箭头(例如<----->)可以表示依赖于多个输入可观察量的绑定。)

所以我的问题变成了:我应该做以下哪一项?

  • --+-->
  • real-space-properties ---+--> screen-space-properties
  • 或其他不同的内容,使用real-space-properties <--+--- screen-space properties

一方面,我们在屏幕空间中有鼠标事件和渲染的矩形。这支持了一个自包含的交互式矩形(我们可以根据上面的演示观察其屏幕空间位置/尺寸属性(以及操作,如果我们想要的话)。

<---->

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

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

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

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

这个图对我来说很有意义,但我不认为直接可以在*处进行“双向”3向绑定。但是,是否有一种简单的方法来模仿/解决它?或者我应该采取完全不同的方法?

1 个答案:

答案 0 :(得分:0)

这是缩放&amp;上的矩形的示例。具有恒定笔划宽度的pannable窗格。您只需将比例因子定义为窗格的属性,将其绑定到调用类中的属性,并将其划分为绑定到矩形的笔触宽度的属性。

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();
                    }
                }
            }
        };
    }
}