3

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

问题描述

此示例中的目标是允许用户在可平移和可缩放的 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),并且<----->可能表示双向绑定。尾箭头,例如--+-->可以表示依赖于多个输入 observable 的绑定。)

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

  • real-space-properties ---+--> screen-space-properties
  • real-space-properties <--+--- screen-space properties
  • 或不同的东西,使用<---->

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

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

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

4

1 回答 1

0

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

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();
                    }
                }
            }
        };
    }
}
于 2015-12-21T22:42:43.377 回答