私はプロパティ バインディングに比較的慣れていないので、設計上の問題に対処する方法について高レベルのアドバイスを探しています。ここでは簡単な例を説明します。
問題の説明
この例の目標は、ユーザーがパンおよびズーム可能な 2D 空間でインタラクティブにボックス/長方形の領域を指定できるようにすることです。ボックスが描かれている 2D 画面空間は、2D の「実空間」にマッピングされます (たとえば、電圧対時間デカルト空間、または GPS など)。ユーザーは、ビューポートをいつでも垂直/水平にズーム/パンできる必要があります。これにより、これら 2 つの空間間のマッピングが変更されます。
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 ノードの組み込みの scale/translate プロパティを使用してマッピングを実装することでした。ズームは、概念的な長方形自体を大きくするだけで、境界線や角のハンドルを太くするべきではありません。
(以下では、矢印は因果関係/影響/依存関係を表します。たとえば、A ---> B
プロパティ B がプロパティ A にバインドされていることを意味する場合もあり (または、イベント ハンドラー A がプロパティ B を設定することを意味する場合もあります)、<----->
双方向のバインドを表す場合があります。--+-->
複数の入力オブザーバブルに依存するバインディングを表すことができるような尾の矢印)。
それで私の質問は次のようになりました:私は次のどれをすべきですか?
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 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
この図は私には非常に理にかなっていますが、* での「双方向」の 3 方向バインディングのようなものは直接可能ではないと思います。しかし、おそらくそれをエミュレート/回避する簡単な方法はありますか? それとも、まったく別のアプローチを取る必要がありますか?